参考链接

讲清楚重排或回流、重绘

面试官:怎么理解回流跟重绘?什么场景下会触发?

介绍回流与重绘(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

(见文尾补充

CSS 重排和重绘 - 图1

CSS 重排和重绘 - 图2

CSS 重排和重绘 - 图3

触发回流 (reflow)

定义

通过 JavaScript 或 CSS 改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style)、布局(Layout)、绘制(Paint)以及后面的所有流程,这种行为称为重排 / 回流

触发条件

CSS Triggers

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),这种行为称为重绘

回流一定会触发重绘,但重绘不一定会引起回流。

触发条件

CSS Triggers

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 重新布局
  • 使用 absolutefixed 使元素脱离文档流(制作复杂动画时减小对其他元素的影响)
  • 动画实现速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • 开启 GPU 加速。利用 CSS 属性 transformopacitywill-change 等,比如改变元素位置,使用 translate 会比使用绝对定位改变其 left 或 top 更高效,因为它不会触发重排或重绘transform 使浏览器为元素创建一个 GPU 图层,这使得动画元素在一个独立的层中进行渲染,当元素内容没有改变就没必要进行渲染。
  • 频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层

补充

网络线程获取到网络数据后会发生什么?(概况版)

  1. 浏览器进程中的网络线程请求获取到 HTML 数据后,通过 IPC 将数据传送给渲染器进程的主线程
  2. 主线程将 html 解析构造 DOM 树
  3. 进行样式计算(Computed Style)
  4. 根据 DOM 树和生成好的样式生成 Layout Tree
  5. 通过遍历 Layout Tree生成绘制(paint)顺序表
  6. 接着遍历Layout Tree 生成了 Layer Tree
  7. 然后主线程将 Layer Tree 和绘制顺序信息一起传给合成器线程
  8. 合成器线程按规则进行分图层
  9. 并把图层分为更小的图块(tiles)传给栅格线程进行栅格化
  10. 栅格化完成后,合成器线程会获得栅格线程传过来的”draw quads”图块信息
  11. 根据这些信息合成器线程合成了一个合成器帧
  12. 然后将该合成器帧通过 IPC 传回给浏览器进程
  13. 浏览器进程再传到 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 替代 setTimeoutsetInterval 来执行动画之类的视觉变化,避免轻易造成丢帧导致卡顿。

重排和重绘都会占用主线程,JS也在主线程上运行,会出现抢占执行时间的问题

如果在运行动画时还有大量的JS 需要执行,而布局、绘制和 JS 执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有剩余时间,JS 线程就会拿到主线程的使用权。

CSS 重排和重绘 - 图4

如果JS执行时间过长,就会导致在下一帧开始时候,JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿。

CSS 重排和重绘 - 图5

优化手段一

通过 requestAnimationFrame() 这个API来帮助解决问题,该方法会在每一帧被调用,通过 API 的回调,可以把 JS 的任务分成一些更小的任务块(分到每一帧),在每一帧时间用完之前暂停 JS 执行,归还主线程,这样在下一帧开始时,主线程就可以按时执行布局和绘制。

CSS 重排和重绘 - 图6

优化手段二

栅格化的整个流程是不占用主线程的,CSS 中的 Transform 属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所有不会受到主线程的中 JS 执行的影响。

更重要的是,经过 Transform 实现的动画由于不需要经过布局绘制样式计算等操作,所以节省了很多运算时间。

CSS 重排和重绘 - 图7