CSS入门笔记[1]:浅析浏览器的渲染机制

264 阅读6分钟

浏览器如何使用 HTML、CSS 和 JavaScript 在屏幕上渲染像素,将完整页面呈现给用户的呢?

在浏览器接收到服务器响应之后,浏览器会按照 关键渲染路径(Critical Rendering Path) 将收到的 HTML、CSS 和 JavaScript 进行必要的处理,从而将它们转变成渲染的像素,并最终呈现给用户。

了解浏览器渲染的过程与原理,很大程度上是为了优化关键渲染路径,但优化应该是针对具体问题的解决方案,所以优化没有一定之规。

例如为了保障首屏内容的最快速显示,通常会提到渐进式页面渲染,但是为了渐进式页面渲染,就需要做资源的拆分,那么以什么粒度拆分、要不要拆分,不同页面、不同场景策略不同。具体方案的确定既要考虑体验问题,也要考虑工程问题。

浏览器渲染页面的过程

浏览器对内容的渲染,可以分为下面五个步骤:

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

浏览器渲染页面前需要先构建 DOM 和 CSSOM 树。因此,要确保尽快将 HTML 和 CSS 都提供给浏览器。

处理 HTML 标记并构建 DOM 树

DOM 树的建立,包括了以下四步:

  1. 转换: 浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符。
  2. 令牌化: 浏览器将字符串转换成W3C HTML5 标准规定的各种令牌,例如,<html><body>,以及其他尖括号内的字符串。每个令牌都具有特殊含义和一组规则。
  3. 词法分析: 发出的令牌转换成定义其属性和规则的“对象”。
  4. DOM 构建: 最后,由于 HTML 标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,body 是 paragraph 对象的父项,依此类推。

流程结束后,浏览器最终会输出页面的文档对象模型 (DOM),之后浏览器对页面进行的每一步处理都会用到它。

如果我们打开 Chrome DevTools 并在页面加载时记录时间线,就可以看到 Chrome 浏览器执行该步骤实际花费的时间。在本例中,将全部 HTML 字节转换成 DOM 树大约需要 5 毫秒。对于较大的页面,这一过程需要的时间可能会显著增加。特别在希望创建流畅动画时,如果浏览器需要处理大量 HTML,最终的效果会很受影响。

处理 CSS 标记并构建 CSSOM 树

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

当浏览器在构建上述文档的 DOM 时,浏览器在文档的 head 部分会遇到一个 link 标记,该标记引用一个外部 CSS 样式表:style.css。由于预见到需要利用该资源来渲染页面,它会立即发出对该资源的请求,并返回以下内容:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

此时,浏览器会重复建立 DOM 的过程,为 CSS 建立对应的“CSS 对象模型”(CSSOM) 的树结构。

想要了解 CSS 处理所需的时间,我们可以在 Chrome 浏览器的DevTools 中记录时间线并寻找“Recalculate Style”事件:与 DOM 解析不同,该时间线不显示单独的“Parse CSS”条目,而是在这一个事件下一同捕获解析和 CSSOM 树构建,以及计算的样式的递归计算。

将 DOM 与 CSSOM 合并成一个渲染树

为构建渲染树,浏览器大体上完成了下列工作:

  1. 从 DOM 树的根节点开始遍历每个可见节点。
    • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
    • 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,被设置了“display: none”属性的节点。
    • visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。
  2. 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。
  3. 发射可见节点,连同其内容和计算的样式。

该阶段结束后,浏览器输出的渲染树同时包含了屏幕上的所有可见内容及其样式信息。有了渲染树,我们就可以进入“布局”阶段。

根据渲染树来布局,以计算每个节点的几何信息

到目前为止,我们了解了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算各个节点在设备视口内的确切位置和大小。因此在“布局”阶段,浏览器将采用渲染树,并根据设备视口,将渲染树重新排布在视口内。这一阶段也称为“自动重排”。

为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸:所有相对测量值都转换为屏幕上的绝对像素。

最后,既然我们知道了哪些节点可见、它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素。这一步通常称为“绘制”或“栅格化”,即将各个节点绘制到屏幕

参考资料

渲染关键路径

Chrome 浏览器渲染性能优化

Mobile Analysis in PageSpeed Insights

Web Fundamentals

MDN - HTML element reference