重排、重绘以及浏览器的渲染机制
重排、重绘以及浏览器的渲染机制
这道题在我面试的时候多有涉及,所以现在有空就刚好整理下许多大佬的文章结合自己的思考写下这篇文章
浏览器的渲染原理:
在了解浏览器渲染过程之前,先来了解一下页面的加载流程:
过去的浏览器:
过去的浏览器的渲染流程:
- 浏览器输入的url地址经过DNS解析获得对应的IP
- 向服务器发起TCP的3次握手
- 建立链接后,浏览器向该IP地址发送http请求
- 服务器接收到请求,返回一堆 HMTL 格式的字符串代码
- 浏览器获得html代码,解析成DOM树
- 获取CSS并构建CSSOM
- 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
- 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
- 将布局绘制(paint)在屏幕上,显示出整个页面。
渲染机制主要涉及的就是第四步到到最后一步,其中第七步和第八步是最耗时的部分,这两步合起来,就是我们通常所说的渲染。
图解加载流程大概就是:
现代浏览器
现代浏览器的加载流程就有所不一样,准确来说是渲染机制流程已经有所变化,已经没有了渲染树的概念,取代的是布局树的概念。
其流程就变成了:
- 解析HTML
- 样式计算
图解渲染流程大概就是:
接下来我们先一个一个详解这个过程
解析HTML - Parse html
浏览器解析html的流程分为三步:
- 首先拿到这个字符串的 html,并且再次开一个线程,叫做预解析线程,因为下载解析 css 也是会需要时间的,如果都放在主线程来做会有时间忧虑,为了提高效率所以先开始预解析进程帮助渲染主线程进行 css 的预下载和解析,解析好了返给渲染主线程,让主线程去生成 cssom,这就是 css 不会阻塞 html 的根本原因。
- 当碰到 js 文件的时候浏览器会怎么做呢,他会先暂停浏览器的一切行为,等待预解析线程给返回提前下载好的 js,js 也是预解析线程提前下载的,如果主线程解析到
script
位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。 - 最后解析完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
这里再补充说明一下怎么解析 HTML 和 CSS
HTML:
- 浏览器从磁盘或网络读取HTML的原始字节,也就是传输的0和1这样的字节数据,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。
- 将字符串转换成Token,例如:“”、“”等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息
- 在每个Token被生成后,会立刻消耗这个Token创建出节点对象,因此在构建DOM的过程中,不是等待所有的Token都生成后才去构建DOM,而是一边生成Token一边消耗来生成节点对象。
值得注意的是,当我们后面学习 Vue 解析 Template 的时候,也可以发现采取的基本也是这一套解析方法。
CSS
解析css构建CSSOM 的过程和构建DOM的过程非常的相似。当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。
我们看到上面详解也发现了 css 预下载和解析虽然是不占主线程,但是生成 cssom 需要主线程去生成,为了 CSSOM 的完整性,也只有等构建完毕才能进入到下一个阶段,哪怕 DOM 已经构建完,它也得等 CSSOM,然后才能进入下一个阶段。
所以,CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源,我们做优化的时候也可以从这里入手,例如 css 尽量使用 id 和 class。
样式计算 - Recalculate Style
第一步我们拿到了生成的DOM 树和 CSSOM 树,这一步就是要对DOM 树中每个节点中有什么信息进行计算,这一过程中,很多预设值会变成绝对值,比如 red
会变成 rgb(255,0,0)
;相对单位会变成绝对单位,比如 em
会变成 px
,这一步完成后,会得到一棵带有计算后样式的 DOM 树。
布局 - Layout
上一步我们把每个 DOM 节点的样式都计算了出来,这一步的就是根据上一步提供的样式计 算出他的当前位置节点在哪大部分时候,DOM 树和布局树并非一一对应。比如 display:none
的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树 和布局树无法一一对应。
分层 - Layer
浏览器的分层模式在某些老版本的浏览器没有,建议更新谷歌到最新浏览器寻找分层的设计理念就是为了防止用户频繁更新页面而设计出来的,用户更改这一层的数据只会对这一层进行更新从而提升效率。滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过 will-change
属性更大程度的影响分层结果。
这里讲一下,这就是为什么现在很多隐藏和呈现用的不是 display
属性了,而是 opacity
了,性能会更好。
顺便对比下opacity、display 和 vibility
- display:none; DOM结构:浏览器不会渲染display属性为none的元素,不占据空间,意思就是页面上没有它的一席之地,你在开发者模式中选不中那个元素。 事件监听:无法进行DOM事件监听。 性能:动态改变此属性时会引起重排,性能较差。 继承:不会被子元素继承,因为子元素也不被渲染。 transtion过渡不支持display。
- visibility:hidden; DOM结构:元素被隐藏了,浏览器会渲染visibility属性为hidden的元素,占据空间,意思就是页面上有它的空间,在开发者模式中能选中那个元素。 事件监听:无法进行DOM事件监听。 性能:动态改变此属性时会引起重绘,性能较高。 继承:会被子元素继承,子元素通过设置visibility:visible;来显示自身,使子元素取消自身隐藏。 transtion:visibility会立即显示,隐藏时会延时。
- opacity:0; DOM结构:opacity属性值为0时透明度为100%,元素隐藏,占据空间,opacity值为0到1,为1时显示元素。 事件监听:可以进行DOM事件监听。 性能:提升为合成层,不会引发重绘,性能较高。 继承:会被子元素继承,子元素不能通过设置opacity:1;来取消隐藏。。 transtion:opacity可以延时显示与隐藏。
绘制 - Paint
在完成图层的构建后就进行到了绘制阶段,绘制阶段会把每个图层分成很多小的绘制指令,就好像我们现在要画画你是一点点画的出来的并不是一下都出来的,每一步都会在你脑海中呈现每一条的指令。
分块 - Tiling
分块会将每一层分成多个小的区域
光栅化 - Raster
光栅化会将每个块变成位图,优先处理靠近视口的块合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。光栅化的结果,就是一块一块的位图。
画 - Draw
合成线程计算出每个位图在屏幕上的位置,最终交给GPU呈现
这也就是为什么 transform 更好,因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。
重排/回流
概念
先讲一下重排是什么:当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
讲完上面的浏览器渲染原理,大家就可以知道重排的本质就是重新计算 layout 树;当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。浏览器在反复权衡下,最终决定获取属性立reflow。用代码表现就更清楚点了:
// bad 强制刷新 触发四次重排+重绘
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
div.style.right = div.offsetRight + 1 + 'px';
div.style.bottom = div.offsetBottom + 1 + 'px';
// good 缓存布局信息 相当于读写分离 触发一次重排+重绘
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
var curRight = div.offsetRight;
var curBottom = div.offsetBottom;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
div.style.right = curRight + 1 + 'px';
div.style.bottom = curBottom + 1 + 'px';
重排呢主要有两种情况:
- 全局范围:从根节点html开始对整个渲染树进行重新布局。
- 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
举个例子
<body>
<div id="container">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>
如果 #container
的宽高没定死,当 p
节点上发生reflow时,#container
和 body
也会重新渲染,甚至 h5
和 ol
都会收到影响, 但如果定死宽高的话, 当 p
节点上发生reflow时,只会影响 #container
内部,所以这也是一个优化策略。
优化
1.分离读写操作
上面已经讲了一种问题和优化思路,原来的操作会导致四次重排,读写分离之后实际上只触发了一次重排。
这是为什么呢,就是上面讲的浏览器会合并这些操作,也就是它的一个机制:
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
2.样式集中改变
不过这个是针对老的浏览器,现代浏览器上面已经说了会合并这些操作,所以就是提一下:
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// 当top和left的值是动态计算而成时...
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
// better
el.className += " className";
3.将 DOM 离线
“离线”意味着不在当前的 DOM 树中做修改,我们可以这样做:
- 使用 display:none,opacity同理
- 一旦我们给元素设置 display:none 时(只有一次重排重绘),当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过 display属性显示(另一次重排重绘)。通过这种方式即使大量变更也只触发两次重排。
- 通过 documentFragment 创建一个 dom 碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排。
复制节点,在副本上工作,然后替换它!
4.使用 absolute 或 fixed 脱离文档流
使用绝对定位会使的该元素单独成为布局树中 body 的一个子元素,重排开销比较小,不会对其它节点造成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能需要重绘,但是不需要重排。
5.优化动画
- 可以把动画效果应用到 position属性为 absolute 或 fixed 的元素上,这样对其他元素影响较小。
- 动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:比如实现一个动画,以1个像素为单位移动这样最平滑,但是Layout就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
- 启用GPU加速 GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。
动态渲染少用 table 布局
在进行动态数据渲染时,建议尽量避免使用 table 布局。因为一旦 table 中的元素大小或内容发生改变,整个 table 都需要重新计算,这会引起不必要的回流和重绘操作。因此,我们可以尝试使用flex、gird等布局方式来避免这种情况的发生,从而提高页面的渲染效率。
重绘(Repaints):
概念:
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
repaint 的本质就是重新根据分层信息计算了绘制指令(Paint)。当改动了可见样式后,就需要重新计算,会引发 repaint。由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。
优化
就是可以借鉴上面的避免逐行修改样式,合并样式修改、离线处理等,这个优化空间比较小。