准备
为了理解重排与重绘,我们首先需要了解一点浏览器渲染的基础知识 ~
网页生成分为五步:
1. HTML 被 HTML 解析器解析成 DOM 树
2. CSS 被 CSS 解析器解析成 CSSOM 树
3. 结合 DOM 树和 CSSOM 树,生成一颗渲染树(Render Tree)
4. 将所有渲染树的所有子节点进行平面合成,生成布局(flow) -- 重排(也叫回流)
5. 将布局绘制(paint)在屏幕上 -- 重绘
其中 4 5 步是最耗时的,这两步就是我们常说的 渲染
网页生成时候,至少会渲染一次,在用户访问的过程中,可能会不断的重新渲染,即执行上述第 4 5 步,或者只执行第 5 步。
简单来说:
重排指重新生成布局,重新排列元素。例如margin: 10px 变成 margin: 20px,会导致重排;
重绘指重新描绘已经生成的布局。例如颜色改变,不会改变布局,只是重新描绘颜色而已;
重排必定会导致重绘,重绘不一定有重排。因为重排在重绘前执行。所以上述 margin 改变 ,重排完成后触发重绘,成本是很高的。
为什么 width height margin 会引起重排+重绘,color transform 仅会导致重绘呢?
CSS的最终表现分为以下四步:
Recalculate Style -> Layout -> Paint Setup and Paint -> Composite Layers
查找并计算样式 -> 排布 -> 绘制 -> 组合层
width height margin 位于 Layout 层,color transform 位于 Composite Layers
遵循从前到后顺序影响的原则:Layout 改变必定触发 Paint Setup and Paint 和 Composite Layers ,color transform 仅会触发 Composite Layers
所以 ~ 我们得到了答案
1 重排
1.1 影响范围
全局范围:从根节点 html 开始对整个 Render Tree 进行重新布局
局部范围 :对 Render Tree 的某小部分进行重新布局
/* 更改下例 <p> 的 width height,会对h2,h3,body,html都产生影响*/
<html>
<body>
<div>
<h2>hello</h4>
<p>word</p>
<h3>!</h5>
</div>
</body>
</html>
/* 更改下例 <p> 的 width height,若不超出div的范围,则只在div这个dom内触发重排,产生局部影响*/
<html>
<body>
<div style="width:100px;height:100px">
<h2>hello</h4>
<p>word</p>
<h3>!</h5>
</div>
</body>
</html>
1.2 引起重排的属性和方法
width | height | margin | padding |
display | border | position | overflow |
clientWidth | clientHeight | clientTop | clientLeft |
offsetWudth | offsetHeight | offsetTop | offsetLeft |
scrollWidth | scrollHeight | scrollTop | scrollLeft |
scrollIntoView() | scrollTo() | getComputedStyle() | |
getBoundingClientRect() | scrollIntoViewIfNeeded() |
2 重绘
2.1 引起重绘的属性
color | border-style | visibility | background |
text-decoration | background-image | background-position | background-repeat |
outline-color | outline | outline-style | border-radius |
outline-width | box-shadow | background-size |
3 性能优化
原则:减少重排次数,重排范围
参考:https://segmentfault.com/a/1190000017491520
根据浏览器的渲染队列机制,我们在js文件中尽量少用以下样式请求:(下列会强制刷新队列,让浏览器立即执行重排加重绘)
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle(), currentStyle()
浏览器渲染机制:
浏览器把引起重排重绘的操作放入队列中,等到队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器再批量执行这些操作;
// 例如:以下仅会触发1次重排+重绘 div.style.left = '10px'; div.style.top = '10px';
但是有些操作,就会使浏览器立即执行渲染任务,比如以下例子:
// 例如:下列会触发2次重排+重绘 // 因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘 div.style.left = '10px'; console.log(div.offsetLeft); div.style.top = '10px'; console.log(div.offsetTop);
优化举例:
3.1 样式集中改变
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// good
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
建议通过改变 class 或者 csstext 属性集中改变样式
3.2 分离读写操作
div.style.left = '10px';
div.style.top = '10px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
上面触发2次重排+重绘的代码,这次只触发了一次重排
3.3 缓存布局信息
// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
3.4 离线改变 dom
-
隐藏要操作的 dom
在要操作 dom 之前,通过 display 隐藏 dom,当操作完成之后,才将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘
// 修改 dom 样式前
dom.display = 'none'
// 修改 dom 样式后
dom.display = 'block'
- 通过使用 DocumentFragment 创建一个
dom
碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排 - 复制节点,在副本上工作,然后替换它
3.5 position 属性为 absolute 或 fixed
position 属性为 absolute 或 fixed 的元素(脱离文档流),重排开销比较小,不用考虑它对其他元素的影响
3.6 优化动画
- 可以把动画效果应用到 position 属性为 absolute 或 fixed 的元素上,减少对其他元素的影响
- 动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:比如实现一个动画,以1个像素为单位移动这样最平滑,但是 reflow 就会过于频繁,大量消耗 CPU 资源,如果以3个像素为单位移动则会好很多。
- 启用 GPU加速
此部分来自 优化CSS重排重绘与浏览器性能
GPU(图像加速器):
GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。
GPU 加速通常包括以下几个部分:Canvas2D,布局合成,CSS3 转换(transitions),CSS3 3D 变换(transforms),WebGL 和视频 (video)。
/*
* 根据上面的结论
* 将 2d transform 换成 3d
* 就可以强制开启 GPU 加速
* 提高动画性能
*/
div {
transform: translate3d(10px, 10px, 0);
}
4 diff算法
首先奉上本人结合Vue v-for 讲解diff算法的博客~
大家都知道 React 中状态变化后,UI层会及时响应,但是状态变化后并不会立即去计算并渲染DOM数的变化部分,是先建立一个虚拟DOM,改动首先同步到虚拟DOM中,最后在批量同步到真实DOM中。不是每次改动都操作真实DOM。
为什么不能每次改变都直接去操作DOM树?
这便是因为在浏览器中,每一次DOM操作都有可能引起重绘或重排(回流),这太昂贵了,如果每一次改变都直接对DOM进行操作,这会带来性能问题,而批量操作只会触发一次DOM更新。
《Flutter实战》这本书上作者提出了一个思考题,我认为不错,在此分享,欢迎大家留言交流!
思考题:Diff 操作和 DOM 批量更新难道不应该是浏览器的职责吗?第三方框架中去做合不合适?
未完待续 ~