# 页面渲染流程 浏览器渲染进程(浏览器内核)拿到内容后,渲染步骤大致可以分为以下几步: **1.解析 HTML**:解析 HTML 并构建 DOM 树 **2.解析 CSS**:解析 CSS 并构建 CSSOM 树(样式树) **3.合成渲染树**:将DOM与CSSOM合并成一个Render Tree(渲染树) **4.布局计算**:根据渲染树的结构,计算每个节点在屏幕上的大小、位置等属性,生成布局信息 Layout。这个过程会发生回流和重绘 **5.分层**:创建复合图层,进行硬件加速,提高性能 **6.绘制页面**:将生成的布局信息交给浏览器的绘图引擎,通过 GPU 加速将像素Paint到屏幕上 **7.回流和重绘**:如果页面发生改变,浏览器需要重新计算布局和绘制,这可能导致性能问题。因此我们应尽量避免频繁的DOM操作和调整元素样式,以减少不必要的回流和重绘。 ![](https://img.kancloud.cn/14/7f/147fd0493508472c2f570260ea618d1e_1690x590.png) ## 1、解析 HTML,构建 DOM 树 解析过程中遇到 CSS 解析 CSS、遇到 JS 执行 JS,为了提高解析效率,浏览器在开始解析前会启动一个预解析的线程,率先下载 HTML 中的外部CSS和外部JS文件。 如果主线程解析到 link 位置,此时外部 CSS 文件还没下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为 CSS 的下载解析工作是在预解析线程中进行的,这就是 CSS 不会阻塞 HTML 解析的根本原因。 如果主线程解析到 script 位置,会停止解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。 **构建 DOM 树的过程** **1)转换为字符**:浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符 **2)令牌化**:将字符串转换成 Token,例如:html、head、body等。Token 中会标识出当前 Token 是 “开始标签” 或是 “结束标签” 亦或是 “文本” 等信息 **3)词法分析**:转换过程中,不是等所有 Token 都转换完成后再去生成节点对象,而是一边生成 Token,一边消耗 Token 来生成节点对象。换句话说,每个 Token 被生成后,会立刻消耗这个 Token 创建出节点对象。注意:带有结束标签标识的 Token 不会再去创建节点对象。 **4)DOM树构建**:由于 HTML 标记定义不同标记之间的关系,创建的对象会链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系。 ## 2、解析 CSS,构建 CSSOM 树 构建 CSSOM 树的过程与构建 DOM 树的过程非常相似,同样字节 -> 字符串 -> Token -> Node -> CSSOM CSS匹配HTML元素是一个相当复杂且有性能问题的事情,浏览器要递归CSSOM树,确定每一个节点的样式到底是什么。所以 DOM 树要小,CSS 尽量用id和class,切勿过度层叠下去。 ## 3、合成渲染树(样式计算) * 当我们得到DOM树和CSSOM树后,就需要将这两棵树组合为Render tree * 这一过程结束后,渲染树只会包含需要显示的节点和这些节点的样式信息,若某个节点是display: none的,那么就不会出现在渲染树中 * 主线程会遍历 DOM 树,依次为树中的每个节点计算出它最终的样式,这一过程称为Computed Style。在这一过程中很多预设值会变成绝对值:如red -> rgb(255, 0, 0)、em -> px等 * 在计算完样式后,可以通过 JS 的window.getComputedStyle(element)方法来获取计算后的样式,非常方便。 ## 4、布局 * 浏览器生成渲染树以后,就会根据渲染树来进行布局(也叫回流 )。布局阶段浏览器会弄清楚各个节点在页面中的确切 位置和大小,这一行为也被称为自动重排 * 布局流程的输出是一个盒模型,所有相对测量值都将转换为屏幕上的绝对像素 * 大部分时候,DOM 树和布局树并非一一对应。 比如: ![](https://img.kancloud.cn/34/22/34224824dc2b84fd2018487893cef540_1472x414.png) ![](https://img.kancloud.cn/81/ab/81ab574c3f51c9c15c2a59130666dcc8_1514x582.png) * display:none 的节点没有几何信息,因此不会生成到布局树; * 伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。 * 匿名行盒、匿名块盒等都会导致 DOM 树和布局树无法一一对应。 ## 5、分层(普通图层、复合图层) * 主线程会使用一套复杂的策略对整个布局树进行分层。 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。 * 可通过f12 -> 更多工具 -> layers 查看分层情况 * 普通图层: * 普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中) * absolute 布局(fixed 也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层 * 复合图层: * 可以通过硬件加速方式,声明一个新的复合图层,它会单独分配资源。(当然也会脱离文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘) * 在 GPU 中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒。 * 可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息。 * 如何变成复合图层(硬件加速,提升性能)? * 最常用的:translate3d、translateZ * opacity属性/过渡动画:需要动画执行的过程中才会创建合成层,动画未开始、动画结束后元素还会回到之前的状态 * will-change 属性(这个比较偏僻):一般配合 opacity 和 translate 使用,作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(最好用完后就释放) * video、iframe、canvas、webgl等元素 * 其他:如以前的 flash 插件 * 尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡 * 使用硬件加速时,尽可能的使用 index,防止浏览器默认给后续的元素创建复合层渲染 * webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且 releative 或 absolute 属性相同的),会默认变为复合层渲染,如果处理不当会极大的影响性能 * 隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层 ## 6、绘制页面 **6.1、主线程会为每个层单独产生绘制指令集**,用于描述这一层的内容该如何画出来。 ![](https://img.kancloud.cn/dd/b6/ddb673e57d3533d5b7268d16ef134f06_1404x506.png) 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成 **6.2、分块**:合成线程首先对每个图层进行分块,将其划分为更多的小区域(懒加载提升性能)。 它会从线程池中拿取多个线程来完成分块工作。 ![](https://img.kancloud.cn/80/0e/800e138e0c2d80302a3d035da145f089_1488x412.png) 这些块的大小一般不会特别大,通常是 256 * 256 或 512 * 512 这个规格。这样可以大大加速页面的首屏展示。非视口内的图块数据要进入 GPU内存,考虑到浏览器内存上传到 GPU内存的操作比较慢,Chrome 采用了一个策略: 在首次合成图块时只采用一个低分辨率的图片,这样首屏展示的时候只是展示出低分辨率的图片,以提升首屏加载性能。 **6.3、栅格化:将图块转换为位图** * 浏览器渲染进程中还专门维护了一个栅格化线程池,专门负责把图块转换为位图数据 * 合成线程会选择视口附近的图块(tile),把它交给栅格化线程池生成位图 * 生成位图的过程实际上都会使用 GPU 加速,生成的位图最后发送给合成线程。(GPU加速使用的是浏览器的GPU进程,是跨进程的,生成的位图会存储在GPU内存中) ![](https://img.kancloud.cn/a9/98/a9989f78a8e7d0cce612a0272e2dd764_1142x857.png) **6.4、合成和显示** * 栅格化操作完成后,合成线程会生成一个绘制命令,即DrawQuad,并发送给浏览器主进程。 * 浏览器主进程中的viz组件接收到这个命令,把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡。 * 显示器有一个固定的刷新频率,一般是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区和后缓冲区对换位置,如此循环更新。 * 因此:假设某个动画大量占用内存时,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象。 ## 7、回流和重绘 * 回流的本质是重新计算layout树。当进行了影响布局树的操作后,会引发布局计算。为避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,待JS代码全部执行完成后再进行统一计算。所以改动属性造成的回流是异步完成的。 * 因此,当 JS 获取布局属性时,就可能无法获取最新的布局信息。浏览器在反复权衡下,最终决定获取属性(如dom.clientWidth)立即回流 * 重绘的本质是重新根据分层信息计算了绘制指令。当改动了可见样式后,就需要重新计算,引发重绘 * 由于元素的布局信息也属于可见样式,所以回流一定会引起重绘 **总结:** * 回流也叫重排,当 DOM结构发生变化 或者 元素样式发生改变时,浏览器需要重新计算样式和渲染树,这个过程比较消耗性能 * 重绘,指元素的外观样式发生变化(比如改变 背景色,边框颜色,文字颜色 color 等 ),但是布局没有变,此时浏览器只需要应用新样式绘制元素就可以了,比回流消耗的性能小一些 * 什么情况会引起回流? * 页面的首次渲染、浏览器的窗口大小发生变化、元素内容发生变化、元素的尺寸或位置发生变化、元素的字体大小发生变化、添加或删除可见的 DOM元素、激活 CSS伪类、查询某些属性或者调用某些方法 **优化方案:** * 使用 CSS 动画代替 JavaScript 动画:CSS 动画利用 GPU 加速,在性能方面通常比 JavaScript 动画更高效。使用 CSS 的 transform 和 opacity 属性来创建动画,而不是改变元素的布局属性,如宽度、高度等。 * 使用 translated3d 开启硬件加速:将元素的位移属性设置为 translated3d( 0,0,0 ),可以强制使用 GPU 加速。有助于避免回流,并提高动画流畅度。 * 避免频繁操作影响布局的样式属性:当需要对元素进行多次样式修改时,可以考虑将这些修改合并为一次操作。通过添加/移除 css 类来一次性改变多个样式属性,而不是逐个修改。 * 使用 requestAnimationFrame:通过使用 requestAnimationFrame 方法调度动画帧,可以确保动画在浏览器的重绘周期内执行,从而避免不必要的回流。这种方式可确保动画在最佳时间点进行渲染。 * 使用文档片段(Document Fragment):当需要在 DOM 中插入大量新元素时,可以先将这些元素添加到文档片段中,然后再将整个文档片段一次性插入到 DOM 中。这样可以减少回流和重绘的次数。(vue 虚拟 dom 的做法) * 使元素脱离文档流:position: absolute/position: fixed/float:left(只是减少回流,不是避免回流) * 使用 visibility:hidden 代替 display: none:visibility:hidden 不会触发回流,因为元素仍然占据空间,只是不可见。而 display: none 会将元素从渲染树中移除,引起回流。 ## 8、注意事项 * CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕后才会进入下一个阶段构建渲染树。(这点与浏览器优化有关,防止 css 规则不断改变,避免了重复的构建)(CSS 加载虽然不会阻塞 DOM 树解析,但会阻塞 Render 树的渲染) * 通常情况下 DOM 和 CSSOM 是并行构建的,但是当浏览器遇到一个 script 标签时,DOM 构建将暂停,直至 JS 脚本下载完成并执行后才会继续解析 HTML。因为 JavaScript 可以使用诸如 document.write() 更改整个 DOM 结构之类的东西来更改文档的形状,因此 HTML 解析器必须等待 JavaScript 运行才能恢复 HTML 文档解析。 * 如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,建议将 script 标签放在 body 标签底部。 * 如果主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是CSS 不会阻塞 HTML 解析的根本原因。