🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 回流与重绘 回流:当render tree中的一部分(或全部)因为元素的规模**尺寸**,**布局**,**隐藏**等改变而需要重新构建。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。 重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。 > 注意:回流必将引起重绘,而重绘不一定会引起回流。 > ![](https://box.kancloud.cn/d887cc6e05cce0a640f18d9a1ceafe65_720x357.png) * 蓝色部分:HTML解析和网络通信占用的时间 * 黄色部分:JavaScript语句执行所占用时间 * 紫色部分:重排占用时间 * 绿色部分:重绘占用时间 当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流: 1、添加或者删除可见的DOM元素; 2、元素位置改变; 3、元素尺寸改变(margin、padding、border、width和height) 4、内容改变,尤其是输入控件 5、页面渲染初始化; 6、浏览器窗口尺寸改变(resize事件发生时); 7、改变文字大小; 8、激活伪类,如:hover; 9、offsetWidth, width, clientWidth, scrollTop/scrollHeight的计算, 会使浏览器将渐进回流队列Flush,立即执行回流; 10、设置style属性; ~~~ var s = document.body.style; s.padding = "2px"; // 回流+重绘 s.border = "1px solid red"; // 再一次 回流+重绘 s.color = "blue"; // 再一次重绘 s.backgroundColor = "#ccc"; // 再一次 重绘 s.fontSize = "14px"; // 再一次 回流+重绘 // 添加node,再一次 回流+重绘 document.body.appendChild(document.createTextNode('abc!')); ~~~ ## 回流的原因 浏览器在实现回流时,会递归地处理frame。 每个frame的回流都有一个原因, 这个原因会随着frame逐级向下传递(传递过程中可能会改变)。 回流的原因决定了当前frame的回流行为,有这样5种原因: * 初始化(Initial)。DOM载入后的第一次回流,将会遍历所有frame。 * 渐进(Incremental)。当一个frame发生渐进回流时,意味着它前面的元素都没有变, 而是它里面的元素变了。这会引起自底向上的作用。 * 改变大小(Resize)。元素的容器边界发生变化时,此时元素内部状态没变。 在计算自顶向下的布局约束的同时,可以复用内部状态。 * 样式改变(StyleChange)。整个frame树都应得到遍历。 * Dirty。当一个容器已经缓存了多个子元素的Incremental回流时,该容器出于Dirty的状态。 前面四种原因的回流都是在Presentation Shell中立即调用的, 而最后一种回流只有Incremental回流已经到达目标frame时才进行。 (因为这时自底向上的影响才被计算出来,才能决定容器的图形显示) # 重绘和回流和 Eventloop 有关 1. 当 Eventloop 执行完 Microtasks 后,会判断`document`是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。 2. 然后判断是否有`resize`或者`scroll`事件,有的话会去触发事件,所以`resize`和`scroll`事件也是至少 16ms 才会触发一次,并且自带节流功能。 3. 判断是否触发了 media query 4. 更新动画并且发送事件 5. 判断是否有全屏操作事件 6. 执行`requestAnimationFrame`回调 7. 执行`IntersectionObserver`回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 8. 更新界面 9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行`requestIdleCallback`回调。 以上内容来自于[HTML 文档](https://link.juejin.im/?target=https%3A%2F%2Fhtml.spec.whatwg.org%2Fmultipage%2Fwebappapis.html%23event-loop-processing-model)。 # 如何减少回流、重绘 减少回流、重绘其实就是需要减少对render tree的操作(合并多次多DOM和样式的修改),并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有: 1. 直接改变className,如果动态改变样式,则使用cssText(考虑没有优化的浏览器) ~~~ // 不好的写法 var changeDiv = document.getElementById('changeDiv'); changeDiv.style.color = '#093'; changeDiv.style.background = '#eee'; changeDiv.style.height = '200px'; // 比较好的写法 div.changeDiv { background: #eee; color: #093; height: 200px; } document.getElementById('changeDiv').className = 'changeDiv'; ~~~ 2. 让要操作的元素进行”离线处理”,处理完后一起更新 a) 使用DocumentFragment进行缓存操作,引发一次回流和重绘; b) 使用display:none技术(由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。); c) 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘; d) 将需要多次重排的元素,position属性设为absolute或fixed; 3. 不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存 ~~~ // 不好的写法 for(循环) { el.style.left = el.offsetLeft + 5 + "px"; el.style.top = el.offsetTop + 5 + "px"; } // 比较好的写法 var left = el.offsetLeft, top = el.offsetTop, s = el.style; for (循环) { left += 10; top += 10; s.left = left + "px"; s.top = top + "px"; } ~~~ 4. 让元素脱离动画流,减少回流的Render Tree的规模 ~~~ $("#block1").animate({left:50}); $("#block2").animate({marginLeft:50}); ~~~ 5. 在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。 7. 不要用tables布局的另一个原因就是tables中某个元素一旦触发reflow就会导致table里所有的其它元素reflow。在适合用table的场合,可以设置table-layout为auto或fixed,这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。 参考资料 * [重绘,回流和合成,了解基本浏览器绘制帮你优化页面性能](https://zhuanlan.zhihu.com/p/23428399) * [全新Chrome Devtool Performance使用指南](https://zhuanlan.zhihu.com/p/29879682) * [页面重绘和回流以及优化](http://www.css88.com/archives/4996)