你真的了解回流和重绘吗

    |     2020年4月14日   |   web前端技术, 优化   |     0 条评论   |    1146

回流和重绘是每个 Web 开发者都会听到的词,但具体两步各自做什么,很多人并不清楚。下面从浏览器渲染过程讲到如何减少它们,并配上可点的 demo。

渲染过程来自 MDN:解析 HTML/CSS → 生成渲染树 → Layout(回流,算几何)→ Painting(重绘,算像素)→ 交给 GPU 显示。回流一定会带着重绘,重绘不一定回流。

概念 算什么 典型触发
回流 Layout 位置、大小等几何信息 增删可见 DOM、改尺寸/位置、窗口变化
重绘 Paint 可见节点的绝对像素 改颜色、背景等不影响几何的样式
强制同步布局 读 offsetWidth 等立刻清空队列 循环里边读边写最伤

一、浏览器的渲染过程

若只想看优化手段,可跳到后面「减少回流和重绘」。完整链路如下:

4088852130-5afbe6c95934b_articlex

  • 解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM 树
  • 两者结合生成渲染树(Render Tree)
  • Layout(回流):根据渲染树得到节点几何信息(位置、大小)
  • Painting(重绘):结合几何信息得到节点绝对像素
  • Display:像素发给 GPU。GPU 会把多个合成层合并;CSS3 硬件加速则是新建合成层,这里不展开

生成渲染树

4223770356-5abdb235cdd7d_articlex

浏览器大致会:从 DOM 根开始遍历每个可见节点;为每个可见节点匹配 CSSOM 规则;按节点和样式组成渲染树。不可见节点包括 script、meta、link,以及 display:none。visibility 和 opacity 隐藏的节点仍在渲染树上。渲染树只包含可见节点。

回流

还需要计算节点在视口内的确切位置和大小。浏览器从渲染树根开始遍历。例如:

<!DOCTYPE html>

<html>

  <head>

    <meta name="viewport" content="width=device-width,initial-scale=1">

    <title>Critial Path: Hello world!</title>

  </head>

  <body>

    <div style="width: 50%">

      <div style="width: 50%">Hello world!</div>

    </div>

  </body>

</html>

第一个 div 是视口宽度的 50%,第二个是父节点的 50%。回流阶段会按视口实际宽度换成像素:

1189033455-5aec8b9d3bcb2_articlex

重绘

知道哪些节点可见、样式和几何信息之后,把每个节点变成屏幕像素,这一步叫重绘。

二、何时发生回流重绘

页面布局或几何信息变化时就要回流,例如:

  • 添加或删除可见的 DOM 元素
  • 元素位置变化
  • 尺寸变化(margin、border、height、width 等)
  • 内容变化,比如文本变了,或图片换成不同尺寸
  • 页面第一次渲染(不可避免)
  • 浏览器窗口尺寸变化

回流一定触发重绘,重绘不一定回流。改变范围大时可能整页重排,例如出现滚动条或改了根节点。

三、浏览器的优化机制

每次重排都有额外计算,所以多数浏览器会把修改放进队列,攒一段时间或达到阈值再批量执行。但读取布局信息会强制刷新队列,例如:

  • offsetTop / offsetLeft / offsetWidth / offsetHeight
  • scrollTop / scrollLeft / scrollWidth / scrollHeight
  • clientTop / clientLeft / clientWidth / clientHeight
  • getComputedStyle()、getBoundingClientRect
  • 更完整的列表见 https://gist.github.com/pauli…

这些 API 必须返回最新布局,浏览器只能先清空队列。改样式时尽量避开;要用就先把值缓存起来。

四、减少回流和重绘

最小化重绘和重排

合并多次 DOM / 样式修改再一次处理。下面三次改几何,理论上三次回流;现代浏览器常优化成一次,但若中间读了布局信息,仍可能三次:

const el = document.getElementById('test');

el.style.padding = '5px';

el.style.borderLeft = '1px';

el.style.borderRight = '2px';

可以合并改变:

  • 使用 cssText
  • 修改 CSS class
const el = document.getElementById('test');

el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
const el = document.getElementById('test');

el.className += ' active';

批量修改 DOM

步骤:让元素脱离文档流 → 多次修改 → 再放回。第一步和第三步可能回流,但中间修改因不在渲染树上而不会回流。三种办法:隐藏后再显示;用 document fragment 在文档外建子树再拷回去;拷到脱离文档的节点上改完再替换。

直接循环插入每次都会回流:

function appendDataToElement(appendToElement, data) {

    let li;

    for (let i = 0; i < data.length; i++) {

        li = document.createElement('li');

        li.textContent = 'text';

        appendToElement.appendChild(li);

    }

}



const ul = document.getElementById('list');

appendDataToElement(ul, data);

隐藏元素、应用修改、再显示(展示和隐藏各一次重绘):

function appendDataToElement(appendToElement, data) {

    let li;

    for (let i = 0; i < data.length; i++) {

        li = document.createElement('li');

        li.textContent = 'text';

        appendToElement.appendChild(li);

    }

}

const ul = document.getElementById('list');

ul.style.display = 'none';

appendDataToElement(ul, data);

ul.style.display = 'block';

使用文档片段:

const ul = document.getElementById('list');

const fragment = document.createDocumentFragment();

appendDataToElement(fragment, data);

ul.appendChild(fragment);

拷贝到脱离文档的节点再替换:

const ul = document.getElementById('list');

const clone = ul.cloneNode(true);

appendDataToElement(clone, data);

ul.parentNode.replaceChild(clone, ul);

针对上述情况有一个demo测性能,结果并不理想——浏览器本来就会排队合并多次修改,所以这套不必优先考虑。

避免触发同步布局

循环里每次读 box.offsetWidth 再写 p 的 width,会迫使浏览器每轮都先应用上一次样式:

function initP() {

    for (let i = 0; i < paragraphs.length; i++) {

        paragraphs[i].style.width = box.offsetWidth + 'px';

    }

}

先把宽度读出来再循环:

const width = box.offsetWidth;

function initP() {

    for (let i = 0; i < paragraphs.length; i++) {

        paragraphs[i].style.width = width + 'px';

    }

}

对比demo差距更明显。

复杂动画用绝对定位脱离文档流

复杂动画会频繁回流重绘,用绝对定位脱离文档流,避免带动父元素和后续兄弟。可看这个例子:控制台会输出当前帧数。

760752144-5c0f11b283530_articlex

帧数一直到不了 60;点按钮设成绝对定位后可以稳定 60。

五、CSS3 硬件加速

更希望根本不要回流重绘。使用 CSS3 硬件加速,transform、opacity、filters 这些动画可以不引起回流重绘;background-color 等仍会,但性能往往也好一些。本文只谈用法。

常见触发属性:transform、opacity、filters、will-change。

用 Chrome Performance 抓一段回流重绘,见这个例子:

4015752135-5c0f11b277b7d_articlex

动画进行时没有发生回流重绘。注意:给太多元素开硬件加速会占内存;GPU 绘制字体可能抗锯齿失效,动画结束不关掉会产生字体模糊。

六、参考文献

一句话总结:改几何就会回流,回流一定重绘;合并写样式、缓存布局读数,复杂动画再考虑脱离文档流或 GPU 合成层。

转载请注明来源:你真的了解回流和重绘吗
本文链接地址:https://ai.zhousir.top/?p=2636
回复 取消