重绘和回流(Repaint & Reflow),一个第一眼能唬住新手(比如我)的专业名词。当我快快乐乐的搜索相关文章想好好学习天天向上的时候,却发现几乎都是在介绍xxx会触发回流重绘,却不告诉我在哪查看,搞得我一脸懵逼。于是这篇💧文章就出来了。

本文参考了非常多的资料!!比如第二个标题的图片引入就是照搬了 google developers里的内容,只是在每个内容下面增加了实现/验证的步骤而已。如果你是个带哥带姐,已经了解过相关内容,直接只查看如下链接更有意义。

# 参考列表

  1. developers.google.cn - Google提供的渲染性能相关内容 (opens new window)
  2. What forces layout / reflow (opens new window)
  3. CSS Triggers (opens new window)
  4. 高性能Web动画和渲染原理系列(3)——transform和opacity为什么高性能 (opens new window)
  5. sisterAn在 第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化下的回答 (opens new window)

# Chrome开发者工具

F12打开控制台,按ctrl shift p打开命令菜单,输入performance调用处性能分析开发者工具。

调用处性能分析开发者工具示例

# 像素管道

您在工作时需要了解并注意五个主要区域。 这些是您拥有最大控制权的部分,也是像素至屏幕管道中的关键点:

完整的像素管道

  • JavaScript。一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如用 jQuery 的 animate 函数做一个动画、对一个数据集进行排序或者往页面里添加一些 DOM 元素等。当然,除了 JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations、Transitions 和 Web Animation API。
  • 样式计算。此过程是根据匹配选择器(例如 .headline.nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
  • 布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 <body> 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。
  • 绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。
  • 合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

# 1. JS / CSS > 样式 > 布局 > 绘制 > 合成

完整的像素管道

如果您修改元素的“layout”属性,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排”页面。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

这整个流程就是所说的重排/回流/reflow了。那如何查看是否触发了回流呢?

这里我写了个 demo1 (opens new window) 来当示例(示例存在码云博客,可放心点击)。

因为每次点击Event: mousedown都会生成一条渲染队列,因此下文所有 demo的点击事件都添加了 1000ms的延迟来“独立(刷新渲染队列?)”新增样式的像素管道

点击 demo的按钮时,会通过 js给盒子#app添加一个内联样式app.style.marginLeft = '100px'。此时我们打开开发者工具performance(后续只称 performance)查看。

通过下图可以明显的看到,回流重绘都有发生,也就是完整的JS / CSS > 样式 > 布局(Layout) > 绘制(Paint) > 合成

demo1

# 2. JS / CSS > 样式 > 绘制 > 合成

无布局的像素管道。

如果您修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制。

这整个流程就是所说的重绘/repaint了。那如何查看是否触发了重绘呢?

这里依旧写了个 demo2 (opens new window) 来当示例验证一下。

点击 demo的按钮时,会通过 js给盒子#app添加一个内联样式app.style.backgroundColor = '#66ccff'。此时我们打开performance查看。

通过上图发现修改背景颜色,只会发生重绘,而不会发生回流。也就是一次JS / CSS > 样式 > 绘制(Paint) > 合成

demo1

# 3. JS / CSS > 样式 > 合成

无布局或绘制的像素管道。

如果您更改一个既不要布局也不要绘制的属性,则浏览器将跳到只执行合成。

概念我们先放着,先写个例子。假如我需要一个方块向右移动200px,此时我选择使用margin配合transitiondemo3 (opens new window)

<style>
  #app {
    width: 200px;
    height: 200px;
    background-color: #39c5bb;
    transition: 2s ease;
    margin-left: 0;
  }
</style>
 
<body>
  <button id="click">开始</button>
  <div id="app"></div>
  <script>
    const app = document.querySelector('#app')
    const button = document.querySelector('#click')

    button.addEventListener('click', () => {
      setTimeout(() => {
        app.style.marginLeft = '100px'
      }, 2000)
    })
  </script>
</body>

下图中,每一帧(这里是16.2ms)都会发生一次回流重绘,这性能可想而知。你看,动画都出现了明显的掉帧现象,动画的后几帧比我打CS时的手还要抖,动画内容再大点、再丰富点,那该多卡哦。

demo3

于是我们耳熟能详的不会触发回流重绘的transform来写写。

那么,众所周知,transform产出的动画只会发生composite合成这一步骤,性能自然是杠杠的。此时我们使用transform代替一下 demo4 (opens new window)

<style>
  #app {
    width: 200px;
    height: 200px;
    background-color: #39c5bb;
    transition: 2s ease;
    transform: translateX(0);
  }
</style>

<body>
  <button id="click">开始</button>
  <div id="app"></div>
  <script>
    const app = document.querySelector('#app')
    const button = document.querySelector('#click')

    button.addEventListener('click', () => {
      setTimeout(() => {
        app.style.transform = 'translateX(100px)'
      }, 2000)
    })
  </script>
</body>

demo4

可以看到,帧数稳稳的。动画过程中只会发生composite合成这一步骤。但动画开始前和动画结束后都有有重绘这一步。这是为什么呢?

# 硬件加速

上面的例子中,CSStransform会在GPU直接创建一个新的图层,使用GPU渲染,由合成器单独处理。不信你看:

  1. 先调出layers工具。

  1. 转动绘制面版呈 3D状,点击开始按钮查看。

demo4-2

非常的明显,在动画过程中会创建一个独立的合成层被创建。动画开始前引发一次重绘,创建一个 独立的合成层。动画结束后独立的合成层被移除,移除后会引发重绘。意思就是:

引自: justjavac在别人的问题下的回答 (opens new window)

在 Blink 和 WebKit 内核的浏览器中,对于应用了 transition 或者 animation的 opacity(差不多意思就好) 元素,浏览器会将渲染层提升为合成层。

当然了,只要你给marginLeft那个例子加上下面两行代码,直接提升为一个独立的合成层,运动时的帧数也是妥妥的。

will-change: transform;
transform: translateZ(0); /* 不兼容 will-change的情况下 */
  1. MDN — will-change

CSS (opens new window) 属性 will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。

其实也就是说,我提前告诉你我要来“检查”了,你(浏览器)先提前准备好应对,别到时候手忙脚乱的 [狗头]。

  1. translateZ(0)也很好理解,你(浏览器)也别管我用不用 3D,反正我就是需要使用GPU处理,启动硬件加速

同样的,写个 demo5 (opens new window) 验证一下

demo5

当然了,如果你滥用硬件加速或者疯狂地给元素创建层,在内存有限的设备上,对性能的影响可能远远超过创建层带来的任何好处。

# 总结

说这么多,那到底什么会触发回流重绘呢?自己总结是个笨方法,我直接站在巨人的肩膀上面!看官方的不就好了!

# 从JS方面

你大可访问这个网址 What forces layout / reflow (opens new window) (他是谁?不知道,github公司上写着 Google Chrome,这已经足够去信任了吧?),这里列出了所有在JavaScript中调用属性或方法时,将触发浏览器回流重绘的属性或方法。下图列出一部分内容:

# 从CSS方面 CSS Triggers

你大可访问这个网址 CSS Triggers (opens new window)。那这个网站要怎么用呢?

  • 红色框中,Layout布局(回流)Paint绘制(重绘)Composite合成

  • 橙色框中对应不同的内核。

  • 蓝色框分别是:change from default设置属性(一开始没设置该css,新增上去)。Sbbsequent updates 修改属性(对现有的属性值进行修改)。

我们依旧以Chrome(Blink)来举例:

下图可见,在设置属性(change from default)会触发重绘。而在修改属性(Sbbsequent updates)时,只有合成这一步。

当我兴高采烈的写下 demo6 (opens new window) 测试时,却发现,不对劲啊!

<style>
  #app {
    width: 200px;
    height: 200px;
    background-color: #39c5bb;
  }
</style>

<body>
  <div id="app"></div>
  <script>
    const app = document.querySelector('#app')
    setTimeout(() => {
      app.style.opacity = '0.5'
    }, 2000)
  </script>
</body>

好家伙,回流重绘没一个落下,全都有!查了半天资料,也不知道别人讲的是啥。

翻上去像素管道重新思考Layout回流为什么会触发。

如果您修改元素的“layout”属性,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排”页面。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

点击Event log里的Layout查看,回流是发生在document上的。

后续发现这篇文章讲出了原因:高性能Web动画和渲染原理系列(3)——transform和opacity为什么高性能 (opens new window)

行!提升图层,重新测一遍。添加代码transform: translateZ(0);

  1. 在设置属性(change from default)会触发重绘 demo7 (opens new window)
<style>
  #app {
    width: 200px;
    height: 200px;
    background-color: #39c5bb;
    transform: translateZ(0);
  }
</style>

<body>
  <div id="app"></div>
  <script>
    const app = document.querySelector('#app')
    setTimeout(() => {
      app.style.opacity = '0.5'
    }, 2000)
  </script>
</body>

  1. 在修改属性(Sbbsequent updates)时,只有合成这一步 demo8 (opens new window)
<style>
  #app {
    width: 200px;
    height: 200px;
    background-color: #39c5bb;
    transform: translateZ(0);
    opacity: 0.9;
  }
</style>

<body>
  <div id="app"></div>
  <script>
    const app = document.querySelector('#app')
    setTimeout(() => {
      app.style.opacity = '0.5'
    }, 2000)
  </script>
</body>

再试试transform属性,独立图层后和CSS Triggers对应上了。也就是说,CSS Triggers上列出的是元素是一个独立图层时属性对应的结果!

# 从面试题方面

总的来说,js谁会触发回流重绘这里 What forces layout / reflow (opens new window) 全都有。CSS谁会触发回流重绘/重绘,这里也都告诉你了,CSS Triggers。但面试的时候总不能这么说吧?

这里作者就不贴上去了,面试题直接指路 sisterAn在 第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化下的回答 (opens new window)

# 从实战方面

怎么做 Google的开发者文档已经说得明明白白了 developers.google.cn - Google提供的渲染性能相关内容 (opens new window)。总得来说:

  1. CSS
    1. 使用 transform 替代 top
    2. 将动画效果应用到position属性为absolutefixed的元素上,避免影响其他元素的布局。否则你的一次重绘,需要重新计算关联的所有布局位置。
    3. 避免使用CSS 表达式/如:calc,可能会引发回流。
    4. CSS3 硬件加速(GPU加速)。动画效果过于复杂、对布局影响过大的元素,可提升成独立图层增加性能。
    5. 使用性能更高的选择器,如类选择器。同时可以选择性使用 BEM(块、元素、修饰符) (opens new window) 规范。
  2. JS
    1. 避免频繁操作样式。最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
    2. 避免频繁操作DOM。例如你需要创建一个列表,你可以使用字符模板串``,最后再innerHTML进去
    3. 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  3. 使用requestAnimationFrame替代 setTimeoutsetInterval 来执行动画之类的视觉变化,避免轻易造成丢帧导致卡顿
  4. 监听窗口变化,浏览器滚动等性能开销特别大的功能时,添加防抖
  5. 阅读完整 [developers.google.cn - Google提供的渲染性能相关内容]。(https://developers.google.cn/web/fundamentals/performance/rendering)
  6. 学会如何使用 performance (opens new window)

# 最后

写了很多对文章乱抄的水文的吐槽,思考了一下还是都删掉了。

希望广大前端都能知道这个网站 https://developers.google.cn/web (opens new window)

如果觉得有用,点个👍吧。也许对我找工作也有点用?👾顺便加一句,卑微20届大专应届生在线求广州内推,主要技术栈是Vue,其他也不知道怎么说了 = = 不想待在只有一个前端的小公司了。

以上 demo的仓库地址:git-demo (opens new window)

博客链接: github (opens new window) gitee (opens new window)

最后更新时间: 9/16/2020, 5:05:00 AM