重绘(Repaint)和回流(Reflow)

重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

以下几个动作可能会导致性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

并且很多人不知道的是,重绘和回流其实也和 Eventloop 有关。

  1. 当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
  2. 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面
  9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

以上内容来自于 HTML 文档。

前端性能优化(二)更好更快的渲染 - 图1

前端性能优化(二)更好更快的渲染 - 图2

既然我们已经知道了重绘和回流会影响性能,那么接下来我们将会来学习如何减少重绘和回流的次数。

减少重绘和回流

  • 使用 transform 替代 top
  1. <div class="test"></div>
  2. <style>
  3. .test {
  4. position: absolute;
  5. top: 10px;
  6. width: 100px;
  7. height: 100px;
  8. background: red;
  9. }
  10. </style>
  11. <script>
  12. setTimeout(() => {
  13. // 引起回流
  14. document.querySelector('.test').style.top = '100px'
  15. }, 1000)
  16. </script>
  • 尽量不要频繁的增加、修改、删除元素,可以先把 DOM 节点抽离到内存中进行复杂的操作然后再 display 到页面上。使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流。
  • 不要把节点的属性值放在一个循环里当成循环里的变量
  1. for(let i = 0; i < 1000; i++) {
  2. // 获取 offsetTop 会导致回流,因为需要去获取正确的值
  3. console.log(document.querySelector('.test').style.offsetTop)
  4. }
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。在适合用 table 布局的场合,可以设置 table-layout 为 auto 或 fixed,这样可以让 table 一行一行的渲染,这种做法也是为了限制 reflow 的影响范围。
  • 经常 reflow 的元素,比如动画,position 设为 fixed 或 absolute,使其脱离文档流,不影响其它元素的布局。
  • 权衡速度的平滑。比如实现一个动画,以 1px 为单位移动这样最平滑,但reflow 就会过于频繁,CPU 很快就会被完全占用,如果以 3 个像素为单位移动就会好很多。
  • 不要一条一条的修改 DOM 的样式,最好通过设置 class 的方式。 避免重复触发 reflow 和 repaint
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多。避免不必要的复杂的 CSS 选择器,尤其是后代选择器,因为为了匹配选择器将耗费更多的 CPU。
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于video标签来说,浏览器会自动将该节点变为图层。

前端性能优化(二)更好更快的渲染 - 图3

设置节点为图层的方式有很多,我们可以通过以下几个常用属性可以生成新图层

  • will-change
  • video、iframe 标签

节流

考虑一个场景,滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,对于这种情况我们就可以使用节流。

理解了节流的用途,我们就来实现下这个函数

  1. // func是用户传入需要防抖的函数
  2. // wait是等待时间
  3. const throttle = (func, wait = 50) => {
  4. // 上一次执行该函数的时间
  5. let lastTime = 0
  6. return function(...args) {
  7. // 当前时间
  8. let now = +new Date()
  9. // 将当前时间和上一次执行函数时间对比
  10. // 如果差值大于设置的等待时间就执行函数
  11. if (now - lastTime > wait) {
  12. lastTime = now
  13. func.apply(this, args)
  14. }
  15. }
  16. }
  17. setInterval(
  18. throttle(() => {
  19. console.log(1)
  20. }, 500),
  21. 1
  22. )

防抖

考虑一个场景,有一个按钮点击会触发网络请求,但是我们并不希望每次点击都发起网络请求,而是当用户点击按钮一段时间后没有再次点击的情况才去发起网络请求,对于这种情况我们就可以使用防抖。

理解了防抖的用途,我们就来实现下这个函数

  1. // func是用户传入需要防抖的函数
  2. // wait是等待时间
  3. const debounce = (func, wait = 50) => {
  4. // 缓存一个定时器id
  5. let timer = 0
  6. // 这里返回的函数是每次用户实际调用的防抖函数
  7. // 如果已经设定过定时器了就清空上一次的定时器
  8. // 开始一个新的定时器,延迟执行用户传入的方法
  9. return function(...args) {
  10. if (timer) clearTimeout(timer)
  11. timer = setTimeout(() => {
  12. func.apply(this, args)
  13. }, wait)
  14. }
  15. }

预加载

在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载

  1. <link rel="preload" href="http://example.com">

预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。

预渲染

可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染

  1. <link rel="prerender" href="http://example.com">

预渲染虽然可以提高页面的加载速度,但是要确保该页面大概率会被用户在之后打开,否则就是白白浪费资源去渲染。

懒执行

懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒。

懒加载

懒加载就是将不关键的资源延后加载。

懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。

懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。

参考

【1】前端面试之道