Web 性能优化-页面重绘和回流

527 阅读6分钟

浏览器渲染页面的过程

image

从图我们可以看出 浏览器的渲染过程如下:

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小),根据几何信息把元素放在它该出现的位置
  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,展示在页面上。 image

回流/重排 Reflow

当 render tree 中的一部分(或全部)因为元素的规模尺寸、布局、显示/隐藏等改变而需要重新构建,这个过程称作回流(reflow)。页面第一次加载的时候,至少发生一次回流。

何时发生回流

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 激活 CSS 伪类,比如 :hover
  • 操作 class 属性
  • 改变字体,比如修改网页默认字体。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器resize(因为回流是根据视口的大小来计算元素的位置和大小的)
  • 计算 offsetWidth 和 offsetHeight 属性。
<body style="width:50vw">
  <div style="width:50%">
   <div style="width:50%">
     Hello word
   </div>
  </div>
</body>

重绘

当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,(但宽、高、位置等不变),比如 如:outline, visibility, color, background-color......等,这个过程叫做重绘(repaint)

重绘与回流

在回流的时候,浏览器会使 render tree 中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。因此回流必将引起重绘,而重绘不一定会引起回流。
我们可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边(left)移到了右边(right),那我们是不是要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)。

var ele = document.body
ele.style.padding = '15px'; // 回流+重绘
ele.border = '1px solid red'; // 回流+重绘
ele.background = "#fef3fe"; // 重绘

如果向上述代码中那样,浏览器不停地回流+重绘,很可能性能开销非常大,实际上浏览器会优化这些操作,将所有引起回流和重绘的操作放入一个队列中,等待队列达到一定的数量或者时间间隔,就 flush 这个队列,一次性处理所有的回流和重绘。

虽然有浏览器优化,但是当我们向浏览器请求一些 style 信息的时候,浏览器为了确保我们能拿到精确的值,就会提前 flush 队列

  1. offsetTop/Left/Width/Height
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. width,height
  5. getComputedStyle(), 或者 IE的 currentStyle

调试

F12 打开控制台 -> DevTools -> Show console drawer -> Rendering -> 勾选 Paint flashing。

image

  1. Paint flashing: 高亮(绿色)显示重绘的页面区域
  2. Layer boders: 显示图层边框(橙色/实时)和图块(青色。我们知道页面是由多个”图层”组合的, 最终显示给用户看的就是多图层叠加在一起的效 果,css 的 z-index机制就可以很好的体现着一点。
  3. FPS meter: 显示绘制每秒帧数,帧速率分布和GPU内存。或许玩游戏的朋友对这些参数会比较熟悉,该选项更多的是用来分析页面交互和动画性能.
  4. Scrolling performance issues: 突出显示可以减慢滚动的元素(蓝绿色),包括touch&whell事件处理程序和其他主线程滚动情况。主要用来分析滚动性能问题。

减少回流与重绘

Opera提出 reflow和repaint是减缓JavaScript的三大主要原因之一”

image

如何减少

  1. 减少不必要的DOM深度。因为无论你改变DOM节点树上任何一个层级都会影响节点树的每个层级——从根结点一直到修改的子节点。不必要的节点深度将导致执行回流时花费更多的时间。
  2. 精简css,去除没有用处的css
  3. 如果你想让复杂的表现发生改变,例如动画效果,那么请在这个流动线之外实现它。使用position-absolute或position-fixed来实现它。
  4. 避免不必要的复杂的css选择符,尤其是使用子选择器,或消耗更多的CPU去做选择器匹配。
  5. 避免使用table布局.避免使用table布局。可能您需要其它些避免使用table的理由,在布局完全建立之前,table经常需要多个关口,因为table是个和罕见的可以影响在它们之前已经进入的DOM元素的显示的元素。想象一下,因为表格最后一个单元格的内容过宽而导致纵列大小完全改变
  6. 保持 DOM 操作“原子性”: Bad

image

Good

image

  1. 批量操作借助临时变量
// bad
for (let i = 0; i < 10; i++) {
  el.style.left = el.offsetLeft + 5 + 'px'
  el.style.top = el.offsetTop + 5 + 'px' 
}
// good
let left = el.offsetLeft
let top = el.offsetTop
for (let i = 0; i < 10; i++) {
  left += 5
  top += 5 
}
el.style.left = left + 'px'
el.style.top = left + 'px'
  1. 文档碎片. 临时创建一个存放文档的容器(DocumentFragment),我们把所有要创建的节点一起放到容器中, 当节点创建完成,我们统一把容器中的内容增加到页面中(支触发一次回流) DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。 当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。、
let frag = document.createDocumentFragment();
frag.appendChild(liBox)
item.appendChild(frag)
  1. 使用classList代替className. className只要赋值,就一定出现一次rendering计算。classList的add和remove,浏览器会进行 样式名是否存在的判断,以减少重复的rendering

进阶: painting 优化

高端浏览器中 painting行为是CPU为GPU准备纹理素材,可以利用GPU的优势 1. 纹理缓存 2. 图形层

强制把需要进行动画行为的dom对象,在GPU中创建Layout缓存 translateZ ScaleZ Translate3D Scale3D Rotate3D