面试题:回流(重排)&&重绘——满腹疑惑?从浏览器输入url到页面渲染发生了什么?

lxf2023-05-05 05:24:01

我报名参加一期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

如果有人突然问上一句“页面输入url到页面渲染之间发生了什么?”,我相信很多人会想之前的我一样疑惑,从页面输入url到页面渲染之间不就是把代码里的DOM结构和CSS样式以及JS动态执行嘛?怎么会有人问这么奇怪的问题啊。

然而,事实远非这么简单,在页面输入url到页面渲染之间其实大有文章!很多页面为什么明明有那么复杂的结构,可是渲染起来却是很快就能看到页面显示出来;而有些页面明明结构十分的简单,可是当渲染起来,却又要很久才能看到页面显示相关的结果,这里就有很大一部分原因跟我们这里讲到的从页面输入url到页面渲染之间发生的操作有关了。

从页面输入url到页面渲染之间发生了什么操作?

我们将“从页面输入url到页面渲染之间发生了的操作”分为“前世”与“今生”,这一篇文章主要讲它的“今生”!

第一步:HTML代码解析成DOM树

这里说到html解析成DOM树可能不太好理解,“树”在我们的语言里可以理解为一种特殊的结构,是一种树状的结构,与数据结构算法里的二叉树类似,

结构如图:

面试题:回流(重排)&&重绘——满腹疑惑?从浏览器输入url到页面渲染发生了什么?

那么把html代码解析成DOM结构又是怎么一回事呢?

我们都知道,html代码里面经常会出现各种标签的嵌套,如:

<div class="wrap">
    <p>hello</p>
</div>

这里就是一个div标签里嵌套了一个p标签,两层DOM结构。

为了方便大家理解,这里咱们拿一些JavaScript里的代码来做比喻,那么它的树状解析就应该解析成类似于这样的树状结构了:

var DOM = {
    target:{
        el:'div',
        class:'wrap',
        children:[
            {
                el:'p',
                class:'',
                value:'hello',
                children:[]
            }
        ]
    }
}

这是一个根为dom的树,它的第一个枝干是一个类名为'wrap'的div标签,而顺着这条枝干往下找,还能找到一条p标签的子枝干,这个子枝干有值为'hello',再往下找就没有了。

第二步:将CSS代码解析成CSSOM树

将CSS代码解析成CSSOM树的方式跟HTML是一样的,树的深度也是跟HTML里写的样式嵌套的层次有关。

解析的结构跟HTML里解析成树状结构的结果类似,同理可推。

第三步:结合DOM树 和 CSSOM树,生成一颗 render树

走到这一步,浏览器会将DOM树和CSSOM树相结合起来,形成一棵render树。

第四步:布局,将渲染树的所有节点进行平面合成

生成布局,简单来说就是将整合起来的DOM节点进行位置的摆放,即该在页面哪个位置的DOM节点放到哪里去(但是值得一提的是摆放位置不等于渲染了,故页面上此刻还没有东西),整合在一个页面里面,这一步我们可以理解为在浏览器的大脑(思维)里执行的,但是浏览器并未做出实际操作。

第五步:绘制页面到屏幕上 (render-UI)

这一步就是进行最后的渲染了,每个页面至少渲染一次。

这里有一点值得注意,那就是render-UI属于宏任务,第五步的绘制将DOM结构渲染在页面是一个异步操作,需要时间去操作,但是特殊的是它却是执行在微任务之后,下一次的宏任务之前

讲完这些大家可能觉得这好像也不难哇,其实重头戏才刚刚开始,接下来我们才要来到我们真正要注意的地方。

回流(重排)&& 重绘

回流(重排)

回流我们可以理解为重新排列布局,即重新执行“从输入url到页面渲染发生的操作”里的第四步——布局

那么在什么情况下会去执行 回流 这个操作呢?简单来说,就是JS动态控制DOM结构的时候,且有DOM结点的几何信息元素发生改变时,才会执行回流。

如: 最初,这里红色框是在“推荐区域”:

面试题:回流(重排)&&重绘——满腹疑惑?从浏览器输入url到页面渲染发生了什么?

而当点击“我的关注”之后,下面一大片的DOM结构布局都被改变成另外一个样式:

面试题:回流(重排)&&重绘——满腹疑惑?从浏览器输入url到页面渲染发生了什么?

而能执行回流的操作有很多,例如:

  1. window大小被修改
  2. 增加删除DOM结构
  3. 元素的尺寸发生变化
  4. offsetWidthoffsetHeight ,offset... ,clientWidth,client...,scrollTop,scroll...

所有导致元素几何信息发生变化的操作都会触发 回流(重排)。

重绘

对于重绘这个词我们可以更加轻易地去理解它,即重新执行渲染,可以理解为重新执行“从输入url到页面渲染发生的操作”里的第五步——绘制页面到屏幕上

那么它是执行在什么情况呢?

简单来说,所有导致元素非几何信息发生变化的操作都会触发重绘。

如:

最初我们没有点击“换一换”之前,页面是如此:

面试题:回流(重排)&&重绘——满腹疑惑?从浏览器输入url到页面渲染发生了什么?

而点击了“换一换”之后,下面的页面信息是这样的:

面试题:回流(重排)&&重绘——满腹疑惑?从浏览器输入url到页面渲染发生了什么?

这样子,你是不是就悟了呢?

它的DOM结构并没有重新进行重新布局排版,但是数据信息却重新进行了渲染,这就是重绘

注意: 回流一定会重绘,但是重绘不一定会回流!

咳咳,那么我们来看一个问题吧!

问题:

<body>
    <div id="app">   
    </div>
    
    <script>
        let el = document.getElementById('app')
        el.style.width = (el.offsetWidth+1)+'px'
        el.style.width = 1+'px'
    </script>
</body>

你们猜这里执行了几次回流?又执行了几次重绘?

偷偷告诉你,这可是一题字节的面试题呢

如果只是根据上面讲的那些,那么我估计你们会猜是3次回流,3次重绘。这样的话面试官估计会直接Pass啦。

那么又会有人说,三次回流,一次重绘,重绘这个操作会等所有的回流(重排)执行完之后一次性重绘!那我能告诉你,这还是错的!

讲到这题,那么就要讲到浏览器的优化的策略了;

浏览器的优化

即当改变元素的几何信息导致回流发生,浏览器提供了一个渲染队列用于临时存储该次回流(十分类似于执行异步代码是宏任务的挂起);

而浏览器继续执行代码,如果还有几何信息修改,继续入队,直到没有样式修改,然后浏览器会按照浏览器渲染队列来批量优化回流(重排)过程。

同时,还有一个比较重要的地方,就是offsetLeft,offset...会强制刷新渲染队列(立即执行修改任务)

那么,很多人读到这里就会迫不及待的再回去看代码,然后得出一个两次回流,两次重绘的结果,可我还是可以明确的告诉你们,这样的答案还是错的!

它应该这样去解读:

  1. 首先,我们看到(el.offsetWidth+1)+'px',这里有一个offsetWidth,是offset...系列的,那么我就就要刷新渲染队列了,可是在此之前,我们的上一个渲染队列还是空的,那么我们要不要把它算进上一个渲染队列执行呢?

答案是否定的,我们不需要把它算进上一次的队列,所以上一次渲染队列是空的,那么也不会去执行回流以及重绘的操作,而它则被加入到下一次渲染队列中;

  1. 然后我们再将它赋值给el.style.width,那么我们碰到了除offset...系列以外的能改变几何信息的元素了,那么我们将它也加入渲染队列中,此刻渲染队列有两个元素;

  2. 再往后,我们碰见了el.style.width = 1+'px',故我们也同上一样的方式将其加入到渲染队列中。

  3. 后面已经没有其它样式修改了,那么浏览器就会批量的去优化回流过程。

所以最后的结果是一次回流,一次渲染!

这就是 回流 以及 重绘 了,合理利用条件,可以大大的去减少回流以及重绘的次数,以此来减少渲染页面所用的时间,大大的提高的代码的执行效率!

怎么去减少回流和重绘呢?

大家都知道减少回流和重绘可以提高代码执行效率,那么如何去减少回流和重绘呢?

除了浏览器的优化策略,其实我们还可以通过其它方式去实现这样的效果;

那就是使元素脱离文档流 --> 改变样式 --> 回归文档流

打个比方:

如果没有浏览器的优化策略,那么以下代码执行就是4次回流,4次重绘了:

el.style.top = 20 + 'px';
el.style.width = 20 + 'px';
el.style.height = 20 + 'px';
el.style.bottom = 20 + 'px';

那么怎么使它脱离文档流呢?

这有很多方法啦,例如:display:none,先使它脱离文档流,改完样式再让它回归文档流;它不在页面上,那么改变几何信息以及样式信息自然不会进行回流和重绘了。

el.display = none
el.style.top = 20 + 'px';
el.style.width = 20 + 'px';
el.style.height = 20 + 'px';
el.style.bottom = 20 + 'px';
el.display = block

本文到这里就结束啦,各位看官动动发财的小手,点个赞叭!