构建对象模型

Ilya Grigorik
Ilya Grigorik

浏览器必须先构造 DOM 树和 CSSOM 树,然后才能渲染页面。因此,我们需要确保尽快将 HTML 和 CSS 都提供给浏览器。

摘要

  • 字节 → 字符 → 令牌 → 节点 → 对象模型。
  • HTML 标记会转换为文档对象模型 (DOM);CSS 标记会转换为 CSS 对象模型 (CSSOM)。
  • DOM 和 CSSOM 是独立的数据结构。
  • 利用 Chrome 开发者工具中的“Performance”面板,我们能够捕获和检查 DOM 和 CSSOM 的构建和处理开销。

文档对象模型 (DOM)

<!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>

试试看

我们先从可能的最简单情况入手:一个包含一些文本和单张图片的纯 HTML 网页。浏览器如何处理此页面?

DOM 构建流程

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

DOM 树

整个流程的最终输出是我们这个简单页面的文档对象模型 (DOM),浏览器会使用它对页面进行所有进一步处理。

浏览器每次处理 HTML 标记时,都会完成上述所有步骤:将字节转换为字符,确定令牌,将令牌转换为节点,然后构建 DOM 树。整个过程可能需要一些时间,尤其是当我们有大量 HTML 需要处理时。

在开发者工具中跟踪 DOM 构建

如果您打开 Chrome 开发者工具并在页面加载时记录时间轴,就可以看到执行此步骤的实际时间。在上例中,将一段 HTML 区块转换为 DOM 树大约需要 5 毫秒的时间。对于较大的页面,此过程可能需要更长的时间。在制作流畅的动画时,如果浏览器必须处理大量 HTML,这很容易成为瓶颈。

DOM 树会捕获文档标记的属性和关系,但不会告知我们元素在渲染时的外观。这就是 CSSOM 的责任。

CSS 对象模型 (CSSOM)

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

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

我们本来可以直接在 HTML 标记中声明样式(内嵌),但让 CSS 独立于 HTML,这使我们将内容和设计视为不同的关注点:设计人员可以专注于 CSS,开发者可以专注于 HTML,等等。

与 HTML 一样,我们需要将接收到的 CSS 规则转换为可供浏览器理解和处理的内容。因此,我们会重复上述 HTML 流程,但针对的是 CSS 而非 HTML:

CSSOM 构建步骤

CSS 字节会依次转换为字符、令牌、节点,最后链接到一个称为“CSS 对象模型”(CSSOM) 的树形结构中:

CSSOM 树

为什么 CSSOM 具有树结构?在计算页面上任何对象的最后一组样式时,浏览器会先从适用于该节点的最通用规则开始(例如,如果该节点是 body 元素的子项,则应用所有 body 样式),然后通过应用更具体的规则(即规则“向下级联”)以递归方式优化计算出的样式。

为了更具体地加以列明,我们以上面的 CSSOM 树为例。<span> 标记中包含的任何位于 body 元素内的文本,字体大小为 16 像素,文本为红色 - font-size 指令会从 body 向下级联到 span。不过,如果 span 标记是段落 (p) 标记的子标记,则不会显示其内容。

另请注意,上面的树不是完整的 CSSOM 树,只显示了我们决定在样式表中替换的样式。每个浏览器都提供一组默认样式(也称为“用户代理样式”),即我们不提供任何自定义样式时所看到的样式,我们的样式会直接替换这些默认值。

如需了解 CSS 处理所需的时间,您可以在开发者工具中记录时间轴,并查找“Recalculate Style”事件:与 DOM 解析不同,该时间轴不会显示单独的“Parse CSS”条目,而是会捕获解析和 CSSOM 树构建过程,以及在这一次事件下以递归方式计算所计算的样式。

在开发者工具中跟踪 CSSOM 构建

我们的小样式表需要大约 0.6 毫秒的处理时间,影响页面上的 8 个元素;虽然不多,但同样会产生开销。那么,这八个元素从何而来呢?CSSOM 和 DOM 是独立的数据结构! 事实证明,浏览器隐藏了一个重要步骤。接下来,我们看看将 DOM 和 CSSOM 关联在一起的渲染树

反馈