浏览器的渲染

渲染进程(Renderer process)几乎负责浏览器 Tab 内的所有事情,渲染进程的核心目的在于转换 HTML CSS JS 为用户可交互的 web 页面。渲染进程中主要包含以下线程:

  • Main thread 一个主线程

  • Worker thread 多个工作线程

  • Compositor thread 一个合成器线程

  • Raster thread 多个光栅化线程

渲染流程

1、解析 HTML,构建 DOM 树

当渲染进程接收到浏览器进程发起的“提交文档”消息,开始接收 HTML 数据时,主线程会解析文本字符串为 DOM,生成 DOM 树。

渲染 HTML 为 DOM 的方法由 HTML Standardarrow-up-right 定义

2、加载次级的资源

网页中常常包含诸如图片,CSS,JS 等额外的资源,这些资源需要从网络上或者 cache 中获取。主线程在构建 DOM 的过程中会逐一请求它们,为了加速,preload scanner 会同时运行,如果在 html 中存在 img link 等标签,preload scanner 会把这些请求传递给 Browser process 中的 Network thread 进行相关资源的下载。

3、 JS 的下载与执行

HTML 的解析是流式的,不过当遇到 <script> 标签时,渲染进程会停止解析 HTML,而去加载,解析和执行 JS 代码,停止解析 HTML 的原因在于 JS 可能会改变 DOM 的结构(使用诸如document.write()等 API)。

不过开发者其实也有多种方式来告知浏览器如何应对某个资源,比如说如果在 <script> 标签上添加了 asyncdefer 等属性,浏览器会异步的加载和执行 JS 代码,而不会阻塞渲染。

4、解析 CSS,进行样式计算,生成 CSSOM 树

仅仅解析 DOM 还不足以知道页面的具体样式,主线程还会基于 CSS 选择器解析 CSS 获取每一个节点最终的计算样式值,生成 CSSOM 树。即使不提供任何 CSS,浏览器对每个元素也会有一个默认的样式。

5、进行布局,构建布局树

想要渲染一个完整的页面,除了知道每个节点的具体样式,还需要知道每一个节点在页面上的位置。布局其实就是找到所有元素的几何关系的过程。具体过程如下:

通过遍历 DOM 及相关元素的计算样式(DOM 树和 CSSOM 树),主线程会构建出包含每个元素的坐标信息及盒子大小的布局树(layout tree)。布局树和 DOM 树类似,但是其中只包含页面可见的元素,如果一个元素设置了display: none;,这个元素不会出现在布局树上,伪元素虽然在 DOM 树上不可见,但是在布局树上是可见的。

6、分层,构建图层树

因为页面中有很多复杂的效果,如一些复杂的 3D 变换,页面滚动,或者使用 z-index 做 Z 轴排序等,为了更加方便的实现这些效果,主线程会遍历布局树来创建图层树(layer tree),例如添加了 will-change CSS 属性的元素,会被看做单独的一层。但是并不能无限制的给每一个元素都添加上will-change,因为组合过多的层会比在每一帧都栅格化页面中的某些小部分更慢。为了更合理的使用层,可参考有限使用合成来控制层的数量arrow-up-right

  • 拥有层叠上下文属性的元素会被提升为单独的一层:有明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等

  • 需要裁剪(clip)的地方也会创建为图层

7、绘制

即使知道了不同元素的位置及样式信息,我们还需要知道不同元素的绘制先后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树以创建绘制记录。绘制记录可以看做是记录各元素绘制先后顺序的笔记。

8、合成和显示

一旦图层树被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程会栅格化每一层。有的层可以达到整个页面的大小,因此,合成器线程将它们分成多个图块,并将每个图块发送到栅格线程,栅格线程会栅格化每一个图块并存储在 GPU 显存中。

一旦图块被光栅化,合成器线程会生成一个绘制图块的命令——“DrawQuad”,然后将该命令通过 IPC 消息传递给浏览器进程,浏览器进程里面有一个叫 viz 的组件,用来接收合成器线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

合成器线程的优点在于,其工作无关主线程,合成器线程不需要等待样式计算或者 JS 执行,这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。

渲染过程中的三个 Tree

在 Web 浏览器的渲染管道中,存在几个重要的树状结构,分别是 Render Tree、Layout Tree 和 Layer Tree。这些树结构在浏览器的渲染过程中扮演关键角色,它们管理着网页的视觉表示和布局。

Render Tree

定义

Render Tree(渲染树)是由 DOM 树和 CSSOM 树合并而成的一个树状结构。它表示了所有可见的 DOM 元素及其样式信息。不可见的元素(如<head>元素或设置了display: none的元素)不会被包括在渲染树中。

作用

  • 渲染树的主要作用是为页面的布局(Layout)和绘制(Painting)提供所需的所有视觉信息。

  • 它排除了所有不需要渲染的元素,只包含可见元素的尺寸、颜色等信息。

Layout Tree

定义

Layout Tree(布局树)通常是从 Render Tree 导出的,并加入了布局处理的额外信息,如每个元素的精确位置和大小。它反映了元素的几何结构。

作用

  • 布局树的作用是计算每个可见元素的确切位置和大小。这一步骤也被称为 Reflow(回流)。

  • 布局树中的每个节点都包含其对应元素的位置和大小信息,这对于后续的绘制步骤至关重要。

Layer Tree

定义

Layer Tree(层树)描述了页面中的复合层(compositing layers)。当页面中的某些元素需要特殊处理时(如 CSS 动画、滚动、复杂的 CSS 效果如滤镜等),浏览器会创建一个或多个层。

作用

  • 层树使得浏览器能够更高效地执行动画和滚动等操作,因为这些操作可以在不影响整个页面的情况下单独进行。

  • 每个层可以单独进行合成,然后将这些层合成到一个页面上,这个过程称为 Compositing。层的合成可以利用 GPU 加速,提高渲染性能。

区别

  • 包含元素

    • Render Tree 包含所有需要渲染的元素的样式信息,但不包括如<head>display: none的元素,但是包含 visibility: hidden 的元素。

    • Layout Tree 进一步从 Render Tree 中抽取信息,加入元素的精确位置和大小数据。不包含具有 visibility: hidden 的元素

    • Layer Tree 组织页面中的复合层,每个层可能包含一个或多个元素,便于高效的图形处理和渲染。

  • 功能和用途

    • Render Tree 用于收集渲染信息。

    • Layout Tree 用于确定元素的布局。

    • Layer Tree 用于优化渲染和动画处理。

当浏览器在解析 HTML 遇到外部引入的文件

当浏览器在解析 HTML 遇到外部引入的文件,如 CSS 和 JS 文件时,其处理原理和逻辑是根据文件类型和它们对页面渲染的影响来确定的。这些处理方式是为了优化用户体验,确保网页可以高效且正确地加载和渲染。

CSS 文件

  • 处理方式:当浏览器遇到 <link> 标签引入的 CSS 文件时,它通常会开始下载 CSS 文件,同时继续解析 HTML 文档。CSS 的加载是异步进行的,但 CSS 文件的解析会阻塞后续的渲染过程。

  • 原因:CSS 定义了网页的样式和布局。为了避免内容在没有正确样式的情况下呈现(也称为"闪烁的无样式内容",FOUC),浏览器需要先加载并解析 CSS 文件,以构建 CSSOM(CSS 对象模型)。因此,浏览器在 CSS 文件加载和解析完成之前不会渲染页面内容。

JS 文件

  • 处理方式:当浏览器遇到 <script> 标签时,它的默认行为是暂停 HTML 的解析,开始下载并执行 JavaScript 文件。这是因为 JavaScript 可以修改 DOM 和 CSSOM。

  • 原因:由于 JavaScript 可能会改变网页的结构(例如,添加或删除 DOM 元素),浏览器必须暂停 HTML 的解析来执行 JavaScript,确保页面内容的正确性。这种行为会导致页面渲染的延迟,特别是当 JavaScript 文件较大或网络条件较差时。

异步和延迟加载

  • 优化:为了减少这些阻塞性资源对加载性能的影响,开发者可以使用 asyncdefer 属性来优化 JavaScript 文件的加载和执行。

    • async 属性使得脚本可以异步加载。脚本会在加载完成后尽快执行,但不会等待 HTML 文档完全解析。

    • defer 属性延迟脚本的执行直到 HTML 文档完全解析和解析完成。

图片文件(如 JPEG, PNG, GIF)

  • 处理方式:浏览器通常会异步加载图片文件,而不会阻塞 HTML 的解析。

  • 特点:图片加载可能会影响页面的布局,尤其是在图片大小未显式指定时。浏览器可能会在图片完全加载并显示之前保留空间,或者在图片加载过程中调整布局。

字体文件(如 WOFF, TTF)

  • 处理方式:Web 字体通过 CSS 的@font-face规则引入,其加载也是异步的。但是,浏览器可能会在字体文件加载完成之前使用默认字体,或者不显示文本(根据浏览器的字体加载策略)。

  • 特点:字体加载策略(如“闪烁的文本”或“无文本”)对用户体验有显著影响。

视频和音频文件

  • 处理方式:视频和音频文件通常是异步加载的,且不会阻塞页面的其余部分的解析。它们通常在<video><audio>标签中指定。

  • 特点:这些文件类型可能会有较大的文件大小,因此对页面加载时间有显著影响。浏览器可能会先加载媒体文件的元数据(如时长和尺寸),再决定是否下载整个文件。

SVG(Scalable Vector Graphics)

  • 处理方式:SVG 通常嵌入在 HTML 中或通过<img>标签引入。作为 XML 格式的一部分,当嵌入 HTML 时,它们是与 HTML 一同解析的。

  • 特点:SVG 文件因为是矢量格式,可以在不失真的情况下缩放,常用于图标和复杂图形的展示。

JavaScript 模块

  • 处理方式:现代浏览器支持 ES6 模块,这些模块使用<script type="module">引入,并且默认是异步加载的。

  • 特点:模块化 JavaScript 允许更细粒度和高效的代码组织和加载,可以改善页面加载时间和性能。

浏览器事件循环与渲染时机

浏览器事件循环

GUI 渲染线程和 JavaScript 执行线程是通过浏览器的事件循环来协调的。事件循环负责执行任务队列中的任务,包括宏任务(如主文件代码执行、解析 HTML、事件响应(用户交互事件、网络请求回应等)、定时器回调)和微任务(如 Promise 回调,MutationObserver通知等)。

宏任务与渲染

在浏览器的事件循环中,通常会在一个宏任务执行完毕后,执行队列中的所有微任务。微任务执行完毕后,如果有必要,浏览器会进行页面渲染。这意味着,布局和绘制通常不直接作为事件循环中的宏任务来调度,而是根据需要在宏任务和微任务之后进行。

渲染时机

浏览器尝试维持平滑的视觉输出,通常目标是每秒 60 帧,即大约每 16.67 毫秒更新一次画面。因此,渲染的时机通常是在 JavaScript 引擎空闲时(即一次宏任务和相关的微任务执行完毕后),在下一帧之前。

推荐阅读


Last updated