参考链接
介绍回流与重绘(Reflow & Repaint),以及如何进行优化?
第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化
重排 (回流) 和重绘
浏览器解析过程
- 解析代码:解析 HTML 代码生成 DOM 树(DOM)
- 样式计算:解析 CSS 代码进行样式计算(Computed Style),生成 CSSOM 树(CSSOM)
- 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)
- 布局:计算出 render 树的布局得到 Layout Tree(layout/reflow)
- 绘制:遍历 Layout Tree 创建绘制记录表(Paint Records)记录绘制顺序(paint/repaint)
- 分层:遍历 Layout 树生成分层树 Layer Tree(layer)
- 切分图块:合成器线程根据 Layer Tree 和 绘制记录表进行分图层,并把图层切分为更小的图块(tiles)
- 栅格化:栅格线程将图块(tiles)栅格化后生成 “draw quads” 图块信息(raster)
- 合成:将 “draw quads” 图块信息发送给合成器线程,生成一个合成器帧(composite)
- 显示:浏览器将合成器帧发送给 GPU 进行渲染后,最后显示在屏幕上(display)
(见文尾补充)
触发回流 (reflow)
定义
通过 JavaScript 或 CSS 改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style)、布局(Layout)、绘制(Paint)以及后面的所有流程,这种行为称为重排 / 回流。
触发条件
1、布局流相关操作
- 盒模型的相关操作会触发重新布局
- 定位相关操作会触发重新布局
- 浮动相关操作会触发重新布局
2、其他
- 浏览器窗口大小发生改变(resize事件)
- 元素尺寸或位置发生改变(边距、宽高、边框等)
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化(font-size)
- 添加或者删除可见的
DOM
元素 - 激活
CSS
伪类(例如::hover
) - 查询某些属性或调用某些方法
- offsetTop、offsetLeft、 offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
这些属性和方法有一个共性,就是需要通过即时计算得到。
因此浏览器为了获取这些值,也会进行回流。(见 浏览器优化机制 部分补充)
(我觉得初次渲染应该当作布局,而非重排,重排定义中的 “重” 应该是重新进行)
触发重绘 (repaint)
定义
当我们改变某个元素的颜色等属性时,不会重新触发布局(layout),但还是会重新触发样式计算(Computed Style)和绘制(paint),这种行为称为重绘。
回流一定会触发重绘,但重绘不一定会引起回流。
触发条件
1、重排
2、容易造成重绘操作的 CSS
- text-decoration、color、border-style、border-radius、box-shadow、outline、background
浏览器优化机制
每次回流都会对浏览器造成额外的计算消耗,所以浏览器对于回流和重绘有一定的优化机制。
浏览器通常都会将多次回流操作放入一个队列中,等过了一段时间或操作达到了一个阈值,然后才会挨个执行,这样能减少一些计算消耗。
但是在获取布局信息操作的时候,会强制将队列清空,也就是强制回流,通过重新计算来保证返回值的正确性,比如访以下属性或操作以下方法时:
- offsetTop、offsetLeft、 offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
- 其他
这些属性或方法都需要得到最新的布局信息,所以浏览器必须去执行回流。因此,在项目中,尽量避免使用上述属性或方法,如果非要使用的时候,也尽量将值缓存起来,而不是一直获取。
优化
- 减少使用某些会强制同步布局的属性或方法,可用变量存储
- 样式集中改变,使用类来添加新样式
- 使用
visibility: hidden
替换display: none
(前者只引起重绘,后者会触发回流) - DOM 离线操作:先把 DOM 设为
display:none
(有一次 Reflow),然后修改再显示 - 通过
documentFragment
创建一个 DOM 文档片段,在它上面批量操作 DOM,完成后再添加到文档中,只触发一次回流documentFragment
不是真实DOM的一部分,它的变化不会触发DOM树的重新渲染
- 不要使用
table
布局,可能很小的一个小改动会造成整个table
重新布局 - 使用
absolute
或fixed
使元素脱离文档流(制作复杂动画时减小对其他元素的影响)
- 动画实现速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
- 开启 GPU 加速。利用 CSS 属性
transform
、opacity
、will-change
等,比如改变元素位置,使用 translate 会比使用绝对定位改变其 left 或 top 更高效,因为它不会触发重排或重绘,transform 使浏览器为元素创建一个 GPU 图层,这使得动画元素在一个独立的层中进行渲染,当元素内容没有改变就没必要进行渲染。 - 频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层
补充
网络线程获取到网络数据后会发生什么?(概况版)
- 浏览器进程中的网络线程请求获取到 HTML 数据后,通过 IPC 将数据传送给渲染器进程的主线程
- 主线程将 html 解析构造 DOM 树
- 进行样式计算(Computed Style)
- 根据 DOM 树和生成好的样式生成 Layout Tree
- 通过遍历 Layout Tree生成绘制(paint)顺序表
- 接着遍历Layout Tree 生成了 Layer Tree
- 然后主线程将 Layer Tree 和绘制顺序信息一起传给合成器线程
- 合成器线程按规则进行分图层
- 并把图层分为更小的图块(tiles)传给栅格线程进行栅格化
- 栅格化完成后,合成器线程会获得栅格线程传过来的”draw quads”图块信息
- 根据这些信息合成器线程合成了一个合成器帧
- 然后将该合成器帧通过 IPC 传回给浏览器进程
- 浏览器进程再传到 GPU 进行渲染,最后展示到屏幕上
DOM Tree 和 Layout Tree 并不是一一对应的。
设置了 display: none
的节点不会出现在 Layout Tree上,
在 before 伪类中添加了 content 值的元素,content 里的内容会出现在 Layout Tree 上,而不会出现在 DOM 树里,
这是因为 DOM 是通过 HTML 解析获得并不关心样式,而 Layout Tree 是根据 DOM 和计算好的样式来生成,
Layout Tree 是和最后展示在屏幕上的节点是对应的
普通图层和复合图层
可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层
以及复合图层
首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层
,里面不管添加多少元素,其实都是在同一个复合图层中)
其次,absolute
布局(fixed
也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层
。
然后,可以通过 硬件加速
的方式,声明一个 新的复合图层
,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响 默认复合层
里的回流重绘)
简单理解:「GPU中,各个复合图层是单独绘制的,所以互不影响」,这也是为什么某些场景硬件加速效果很好
使用 requestAnimationFrame
**window.requestAnimationFrame()**
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
使用 requestAnimationFrame
替代 setTimeout
或 setInterval
来执行动画之类的视觉变化,避免轻易造成丢帧导致卡顿。
重排和重绘都会占用主线程,JS也在主线程上运行,会出现抢占执行时间的问题
如果在运行动画时还有大量的JS 需要执行,而布局、绘制和 JS 执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有剩余时间,JS 线程就会拿到主线程的使用权。
如果JS执行时间过长,就会导致在下一帧开始时候,JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿。
优化手段一
通过 requestAnimationFrame()
这个API来帮助解决问题,该方法会在每一帧被调用,通过 API 的回调,可以把 JS 的任务分成一些更小的任务块(分到每一帧),在每一帧时间用完之前暂停 JS 执行,归还主线程,这样在下一帧开始时,主线程就可以按时执行布局和绘制。
优化手段二
栅格化的整个流程是不占用主线程的,CSS 中的 Transform 属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所有不会受到主线程的中 JS 执行的影响。
更重要的是,经过 Transform 实现的动画由于不需要经过布局绘制样式计算等操作,所以节省了很多运算时间。