浏览器是如何渲染页面的?
当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
整个渲染流程分为多个阶段,分别是 HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、绘制。每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。这样,整个渲染流程就形成了一套组织严密的生产流水线。
渲染的第一步是解析 HTML
在解析 HTML 的过程中,遇到 CSS 会解析 CSS,遇到 JS 会执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析线程,优先下载 HTML 中的外部 CSS 文件和外部 JS 文件。
当主线程解析到 link
标签时,如果外部的 CSS 文件尚未下载和解析完成,主线程不会等待,而是继续解析后续的 HTML。这是因为 CSS 的下载和解析工作在预解析线程中进行,从而避免了 CSS 阻塞 HTML 解析。
当主线程解析到 script
标签时,会停止解析 HTML,等待 JS 文件下载完成,并将全局代码解析执行后,才能继续解析 HTML。这是因为 JS 代码的执行可能会修改当前的 DOM 树,因此必须暂停 DOM 树的生成。这也是 JS 阻塞 HTML 解析的根本原因。
完成第一步后,浏览器会生成 DOM 树和 CSSOM 树,默认样式、内部样式、外部样式、行内样式都会包含在 CSSOM 树中。
最佳实践:
为了优化解析效率,应尽量将 CSS 放在 <head>
中,并将关键 CSS 内联,以减少渲染阻塞。对于 JS 文件,建议将其放置在页面底部或使用 async
和 defer
属性,以避免阻塞 HTML 解析。
渲染的下一步是样式计算
主线程会遍历生成的 DOM 树,为每个节点计算出其最终样式,称为 Computed Style。
在这个过程中,许多预设值会被转换为绝对值,例如 red
会变为 rgb(255,0,0)
;相对单位会转换为绝对单位,如 em
会变为 px
。
完成这一步后,会得到一棵带有样式的 DOM 树。
最佳实践:
减少复杂的选择器和避免频繁的样式重计算可以提升样式计算的性能。使用简洁的 CSS 选择器,并尽量避免在运行时频繁修改样式。
接下来是布局,布局完成后会得到布局树
布局阶段会遍历 DOM 树的每个节点,计算每个节点的几何信息,例如节点的宽高和相对于包含块的位置。
大多数情况下,DOM 树和布局树并不一一对应。例如,display: none
的节点不会生成布局树;使用伪元素选择器生成的节点虽然在 DOM 树中不存在,但会在布局树中生成。此外,匿名行盒和匿名块盒等也会导致 DOM 树与布局树的不对应。
最佳实践:
避免频繁的 DOM 操作和复杂的布局结构可以减少布局计算的开销。尽量使用 CSS Flexbox 或 Grid 布局,以简化布局逻辑,并减少重排的可能性。
下一步是分层
主线程会使用复杂的策略对布局树进行分层。
分层的好处在于,当某一层发生变化时,只需处理该层的后续步骤,从而提升效率。
滚动条、堆叠上下文、transform
、opacity
等样式都会影响分层结果,也可以通过 will-change
属性进一步控制分层。
最佳实践:
合理使用 transform
和 opacity
可以利用 GPU 加速,提升渲染性能。使用 will-change
属性可以提前告知浏览器某些元素将要发生变化,从而优化渲染,但应谨慎使用以避免过度分层导致内存开销。
再下一步是绘制
主线程会为每个层单独生成绘制指令集,用于描述该层内容的绘制方式。
最佳实践:
优化绘制指令集的复杂度可以减少绘制时间。尽量减少不必要的绘制操作,使用硬件加速的 CSS 属性,以提升绘制性能。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作由合成线程完成
合成线程首先对每个图层进行分块,将其划分为更小的区域。它会从线程池中调取多个线程来完成分块工作。
最佳实践:
优化图层的数量和大小可以提升合成效率。避免创建过多的图层,合并小的图层以减少合成开销。
分块完成后,进入光栅化阶段
合成线程将块信息交给 GPU 进程,以高速完成光栅化。
GPU 进程会开启多个线程进行光栅化,并优先处理靠近视口区域的块。
光栅化的结果是生成一块一块的位图。
最佳实践:
利用 GPU 加速可以显著提升渲染性能。尽量减少需要光栅化的复杂图形,优化图像资源以降低 GPU 负载。
最后一个阶段是绘制
合成线程获取每个层和每个块的位图后,生成一个个「指引(quad)」信息。
这些指引标识位图应绘制到屏幕的哪个位置,并考虑旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这也是 transform
效率高的原因。
合成线程将 quad 提交给 GPU 进程,GPU 进程通过系统调用将其提交给 GPU 硬件,完成最终的屏幕显示。
最佳实践:
利用 CSS transform
和 opacity
进行动画和交互,可以避免触发重排和重绘,从而提升动画性能和流畅度。
什么是 reflow?
reflow 的本质是重新计算布局树。当进行影响布局树的操作后,需要重新计算布局树,这会引发布局计算。
为了避免连续多次操作导致布局树的反复计算,浏览器会合并这些操作,在 JavaScript 代码全部执行完成后再进行统一计算。因此,修改属性导致的 reflow 是异步完成的。
然而,当 JavaScript 获取布局属性时,可能会导致无法获取最新的布局信息,浏览器在权衡后会决定立即进行 reflow。
最佳实践:
避免在循环中频繁读取和修改布局属性,以防止强制同步布局。可以批量进行 DOM 读取和写入操作,减少 reflow 的次数。
什么是 repaint?
repaint 的本质是根据分层信息重新计算绘制指令。当修改了可见样式后,需要重新计算绘制指令,这会引发 repaint。
由于元素的布局信息也属于可见样式,因此 reflow 一定会引起 repaint。
最佳实践:
尽量减少不必要的样式修改,避免触发 repaint。使用 CSS 类切换代替直接修改样式属性,以优化绘制性能。
为什么 transform 的效率高?
因为 transform
不会影响布局和绘制指令,它仅影响渲染流程的最后一个绘制阶段。
由于绘制阶段在合成线程中进行,transform
的变化几乎不会影响渲染主线程。反过来,无论渲染主线程多么忙碌,也不会影响 transform
的变化。
最佳实践:
使用 transform
进行元素的位移、缩放、旋转等操作,可以充分利用 GPU 加速,提升渲染性能和动画流畅度。