字节面试题 -- 请说一下这一段函数发生了几次重排重绘

259 阅读2分钟

一:背景

面试小子又来啦,最近在准备23届前端春招面试字节的时候出了一道关于浏览器渲染的非常有趣的题目,考查一下和性能优化相关的知识点,在这里特地分享给大家一起看一看。

二: 面试引子

2.1 面试题目引入

好了,废话不多说,现在开始我们的正题。让我们看看是什么有趣的题目

// 请说一下这一段代码发生勒几次重排和重绘,我们如何去优化这一段代码
 const move = () => {
    const eles = document.querySelectorAll('.amin');
    const n = eles.length;
    for (let i = 0; i < eles.length; i++) {
      eles[i].style.left =  eles[i].offsetLeft + 1 + 'px';
    }
  }

首先我们一看哎,这一道题目,不就是在考我 调用 offsetLeft函数的时候会引起强制重排这一个点,我一看调用了四次就立马肯定地说是四次,然后面试官说请讲一下是怎么来的四次,我说每设置一次dom的left就会有一次重排了。面试官就说有考虑过dom操作缓冲队列嘛? 我突然一懵,不是很懂。面试官就说那你回去先好好查一下吧。( ̄ ‘i  ̄;) 接下来我们就来逐步地去分析下这一道题目。

2.2 什么时候会触发强制重排和重绘

首先我们为了要解决上面的这一个问题,需要先弄明白的是浏览器什么时候会做重排操作。

任何改变用于构建渲染树的输入信息的东西都可能导致重绘或回流,例如:

  • 添加、删除、更新 DOM 节点
  • 使用display: none(会导致重排和重绘)或者visibility: hidden(只会导致重绘,因为没有几何位置的改变)来隐藏dom节点
  • 在页面上移动、动画化 DOM 节点
  • 添加样式表,调整样式属性
  • 用户操作,例如调整窗口大小、更改字体大小或滚动 那是不是在一个周期中调用多次这些会重排的操作就会有多次的重排讷? 答案是no!!!

浏览器很聪明,它会为我们对DOM操作维护一个缓冲队列,即你的所有对DOM操作可能不会被立马执行,当队列满或者达到一定的时间之后才会去触发执行。 但是有一些dom操作它会让浏览器的这一个优化操作失效,导致强制重排策略产生。

image.png 以上的这一些操作需要及时获得实时的具体位置,一旦调用这一些位置信息的话浏览器的优化就会失效了。为此,它需要应用所有缓冲队列中的更改、刷新队列并进行重排计算操作。 

注意是当缓冲队列中有操作的时候再调用这一些指令才会导致重排操作,假如缓冲队列为空,调用这一些指令的时候不会导致重排操作

2.3 解密谜底

从上面对重排和重绘的定义来说,触发强制重排的操作有两个要素

  • dom缓冲操作队列不为空
  • 调用了图中任意一个想要获取实时位置的API 那么回归原来的题目
const move = () => {
  const eles = document.querySelectorAll('.amin');
  const n = eles.length;
  for (let i = 0; i < eles.length; i++) {
    eles[i].style.left = eles[i].offsetLeft + 1 + 'px';
  }
}

假设 eles.length为4,

当 i = 0 的时候,缓冲队列为空,此时调用offsetLeft不会触发重排,同时把这一个设置dom位置的操作塞进缓冲队列中。

当 i = 1 的时候,缓冲队列不为空,此时调用offsetLeft会清空缓冲队列,并触发重排,同时把这一个设置dom位置的操作塞进缓冲队列中。

当 i = 2 的时候,缓冲队列不为空,此时调用offsetLeft会清空缓冲队列,并触发重排,同时把这一个设置dom位置的操作塞进缓冲队列中。

当 i = 3 的时候,缓冲队列不为空,此时调用offsetLeft会清空缓冲队列,并触发重排,同时把这一个设置dom位置的操作塞进缓冲队列中。

最后函数执行完之后会最后渲染流程中看到缓冲队列中不为空会运行最后一次重排操作 由此可看,在函数执行阶段会有三次强制重排的花销加上最后一次缓冲队列清空导致重排的花销一共有四次重排的花销。

接着我们去使用浏览器给我们带来的性能工具去检测一下是否是四次。 我编写了如下的代码

image.png 然后运行性能工具之后再去点击移动按钮可以检测到下面的这一种重排的花销 image.png 好啦,到这里大家就基本了解完什么是强制重排,以及强制重排可能会给我们带来的额外地花销。 接下来我们去探索下如何做去将这一部分花销给降低下来吧。

2.4 如何优化这一部分代码

首先对于这种频繁改和取dom元素的宽高的情况,我们可以考虑下提前将取操作和写操作分离出来,这样纯取和纯设置就不会在函数运行过程中导致多次重排啦。

  const move = () => {
    const eles = document.querySelectorAll('.animation');
    const n = eles.length;
    const arr = [];
    // 提前读好dom节点的数据并缓存起来
    for (let i = 0; i < n; i++) {
      arr[i] = eles[i].offsetLeft + 1 + 'px';
    }
    // 然后再批量设置dom自己的位置信息
    for (let i = 0; i < n; i++) {
      eles[i].style.left = arr[i];
    }
  }

大家可以猜一下改造之后在整一个周期中会发生多次重排讷?

对了!!!!! 那就是 1 次啦。

不知道大家有没有猜对讷。 在函数里面不会有强制重排的情况出现,只是在最后面缓冲队列中达到一定时间触发清空操作并最后进行重排布局这一步会有花销而已。 我们从一开始的4次重排花销降到了只有1次这么大,优化效果可谓是非常地明显的,优化了大概有75%左右的重排性能。

接下来,我在用开发者工具中的性能工具给大家带来实际的重排花销验证。

image.png

三:对于渲染机制的思考和总结

以上的文章从 重排是什么强制重排什么时候会发生以及我们如何去做去避免这一些花销的发生,最后在用工具去实际验证我们的想法的思路去展开这一篇文章。

希望大家看完这一篇文章之后能对重排这一件事情不但有定性的分析还要有定量的分析~

如果大家还想了解更多关于重排的细节之类的可以看一下这一篇文章一篇文章带你看懂浏览器的重排和重绘 - 掘金 (juejin.cn)