前端巩固基础,终于写到了浏览器篇,虽然浏览器渲染原理大概答得出来,但还是想根据以前的笔记整理一下。
浏览器渲染过程
我们以 Webkit 渲染引擎为例,讲一下浏览器是怎么渲染一个网页的:
- 浏览器的渲染过程:
- 解析 HTML 构建 DOM 树,并行请求 css/image/js
- CSS 文件下载完成后被 CSS 解析器解析成 CSSOM 树
- 结合 DOM 和 CSSOM 树,生成一棵渲染树(Render Tree)
- 布局(Layout),计算出每个节点在屏幕中的位置
- 将布局显示(Painting)在屏幕上
在用户访问页面过程中,还会不断重新渲染页面,重新渲染通常是指第 4 步+第 5 步,或者只有第 5 步。
在关于前端性能优化里面,就有一点:减少重排与重绘,因为重排和重绘次数多的话,可能会影响到网页显示速度,给用户带来不流畅甚至卡顿的效果。
重绘
概念
重绘即是指当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程。
触发重绘的属性
重排(回流)
概念
当 DOM 的变化影响了元素的几何信息(DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
当网页重新布局的时候,也会将元素重新绘制,比如改变元素的宽度,这个元素乃至周边的 DOM 都需要重新绘制。
所以,重排一定会触发重绘,而重绘不一定会重排。
触发重排的属性
大致可分为:
- 盒模型相关的属性:width, height, margin, display, border 等
- 定位属性及浮动相关的属性:position, top, float 等
- 改变节点内部文字结构:text-align, overflow, font-size, line-height, vertical-align 等
- 进行获取布局信息的操作:offsetWidth, clientHeight, width, scrollTop, getComputedStyle 等
触发重排时会对周围 DOM 重新排列,影响的范围有两种:
- 全局范围:从根节点 html 开始对整个渲染树进行重新布局。
1 | <body> |
当 p 节点上发生 reflow 时,h1 和 body 也会重新渲染,甚至影响全局。
局部范围重排
- 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
一个 dom 的宽高之类的几何信息定死,然后在 dom 内部触发重排,就只会重新渲染该 dom 内部的元素,而不会影响到外界。
浏览器的渲染队列
1 | div.style.left = '5px' |
按我们上面说的,上面的代码理论上应该触发 4 次重排+重绘,但实际是只触发了一次重排,这得益于浏览器的渲染队列机制:当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
强制刷新队列
1 | div.style.top = '5px' |
上面代码会触发 4 次重排+重绘,因为在 console 中请求的这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。
因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘。在开发中应该尽量避免一行代码一个重排,即是做好分离读写操作,减少性能损耗。
- 强制刷新队列的 style 样式请求:
1 | offsetTop, offsetLeft, offsetWidth, offsetHeight |
如何减少重绘与重排
1. 分离读写操作
1 | div.style.top = '5px' |
像上面的代码,我们对它进行分离操作,从 4 次重排变成 1 次重排。
在第一个 console 的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。剩下的 console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。
2. 将样式修改合并成一次操作
1 | div.style.left = '5px' |
虽然大部分浏览器都做了渲染队列优化,但不排除老版本浏览器效率仍然地下,故我们最好还是做更好的处理,比如可以把样式集中合并一次修改,通过 class 或者 cssText 属性
1 | // bad |
3. 缓存布局信息
避免强制刷新队列
1 | box.style.width = box.clientWidth + 10 + 'px' |
按浏览器渲染队列机制,按道理回流一次。但遇到 box.clientWidth,重新渲染,所以全部是两次回流
1 | let a = box.clientWidth |
4. 离线改变 DOM
- 隐藏要操作的 DOM
在要操作 dom 之前,通过 display 隐藏 dom,然后尽量的进行多次修改操作,当操作完成之后,才将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘。
通过使用 DocumentFragment 创建一个文档碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排。
复制节点,在副本上工作,然后再替换原来的元素
5. 使用 resize 事件时,做防抖和节流处理
6. 对动画元素使用 absolute / fixed 属性。
position 属性为 absolute 或 fixed 的元素,重排开销比较小,不用考虑它对其他元素的影响
7. CSS3 属性优化动画
比如使用 CSS 的 transform 来实现动画效果,避免了回流跟重绘,直接在非主线程中执行合成动画操作。这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。那为什么能避免呢,主要是因为它创建了一个新图层。
- 图层创建的条件:
Chrome 浏览器满足以下任意情况就会创建图层:
- 拥有具有 3D 变换的 CSS 属性
- 使用加速视频解码的
<video>
节点 <canvas>
节点- CSS3 动画的节点
- 拥有 CSS 加速属性的元素(
will-change
)
一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能,但也不能生成过多的图层。
- 利用合成的好处:
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快。GPU 硬件加速(规避了回流)是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
当然也要注意,如果为太多元素使用 css3 硬件加速,会导致内存占用较大,同样也会有性能问题。
1 | /* transform为3D,则可开启GPU加速,提高动画性能 */ |
参考文章
欢迎关注我掘金账号和 Github 技术博客:
- 掘金:https://juejin.im/user/1257497033714477
- Github:https://github.com/Jacky-Summer
- 觉得对你有帮助或有启发的话欢迎 star,你的鼓励是我持续创作的动力~
- 如需在微信公众号平台转载请联系作者授权同意,其它途径转载请在文章开头注明作者和文章出处。