浏览器渲染过程

对象模型

文档对象模型 (DOM)

1
2
3
4
5
6
7
8
9
10
11
<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 页面。浏览器如何处理此页面?

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

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

CSS 对象模型 (CSSOM)

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

与处理 HTML 时一样,我们需要将收到的 CSS 规则转换成某种浏览器能够理解和处理的东西。因此,我们会重复 HTML 过程,不过是为 CSS 而不是 HTML:

CSS 字节转换成字符,接着转换成令牌和节点,最后链接到一个称为“CSS 对象模型”(CSSOM) 的树结构内:

渲染树构建、布局及绘制

CSSOM 树和 DOM 树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。优化上述每一个步骤对实现最佳渲染性能至关重要。

第一步是让浏览器将 DOM 和 CSSOM 合并成一个“渲染树”,网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSSOM 样式信息。

浏览器的渲染过程

  • 处理 HTML 标记并构建 DOM 树。
  • 处理 CSS 标记并构建 CSSOM 树。
  • 将 DOM 与 CSSOM 合并成一个渲染树。
  • 根据渲染树来布局(layout),以计算每个节点的几何信息(在屏幕中的位置)。
  • 将各个节点绘制(paint)到屏幕上。

阻塞渲染的 CSS

默认情况下,CSS 被视为阻塞渲染(不阻塞 DOM 树构建,但可能引起白屏)的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。请务必精简您的 CSS,尽快提供它,并利用媒体类型和查询来解除对渲染的阻塞。

  • 默认情况下,CSS 被视为阻塞渲染的资源。
  • 我们可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。
  • 浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。

使用 JavaScript

JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。 不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。 为了实现最佳性能,可以让您的 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。

  • JavaScript 可以查询和修改 DOM 与 CSSOM。
  • JavaScript 执行会阻止 CSSOM。
  • 除非将 JavaScript 显式声明为异步,否则它会阻止构建 DOM(可能只显示一部分网页)。

向 script 标记添加异步关键字可以指示浏览器在等待脚本可用期间不阻止 DOM 构建,这样可以显著提升性能

defer 和 async

没有 defer 或 async

保证先后顺序。解析:HTML 解析器遇到它们时,将阻塞(取停止解析),待脚本下载完成并执行后,继续解析标签之后的文档。

<script src="script.js"></script>

有 defer

HTML5 规范要求保证先后顺序(然而,实际中并不一定会按照顺序执行)。解析:HTML 解析器遇到它们时,不阻塞(脚本将被异步下载),待文档解析完成之后,执行脚本。

<script defer src="myscript.js"></script>

有 async

不保证先后顺序。解析:HTML 解析器遇到它们时,不阻塞(脚本将被异步下载,一旦下载完成,立即执行它),并继续解析之后的文档。

<script async src="script.js"></script>

兼容性列表

DOMContentLoaded 事件

当onload事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。

当DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。

分析渲染性能

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
</body>
</html>

为了执行 JavaScript 文件,我们还需要进行阻止并等待 CSSOM;回想一下,JavaScript 可以查询 CSSOM,因此在下载 style.css 并构建 CSSOM 之前,浏览器将会暂停

如果网页中的代码不需要阻止网页的渲染,我们就可以向 script 标记添加“async”属性来解除对解析器的阻止:

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js" async></script>
</body>
</html>

优化建议

优化 JavaScript 的使用

  • 首选使用异步 JavaScript 资源

异步资源不会阻塞文档解析器,让浏览器能够避免在执行脚本之前受阻于 CSSOM。通常,如果脚本可以使用 async 属性,也就意味着它并非首次渲染所必需。可以考虑在首次渲染后异步加载脚本。

  • 避免同步服务器调用
  • 延迟解析 JavaScript

为了最大限度减少浏览器渲染网页的工作量,应延迟任何非必需的脚本(即对构建首次渲染的可见内容无关紧要的脚本)。

  • 避免运行时间长的 JavaScript

运行时间长的 JavaScript 会阻止浏览器构建 DOM、CSSOM 以及渲染网页,所以任何对首次渲染无关紧要的初始化逻辑和功能都应延后执行。如果需要运行较长的初始化序列,请考虑将其拆分为若干阶段,以便浏览器可以间隔处理其他事件。

优化 CSS 的使用

CSS 是构建渲染树的必备元素,首次构建网页时,JavaScript 常常受阻于 CSS。确保将任何非必需的 CSS 都标记为非关键资源(例如打印和其他媒体查询),并应确保尽可能减少关键 CSS 的数量,以及尽可能缩短传送时间。

  • 将 CSS 置于文档 head 标签内

尽早在 HTML 文档内指定所有 CSS 资源,以便浏览器尽早发现 <link> 标记并尽早发出 CSS 请求。

  • 避免使用 CSS import

一个样式表可以使用 CSS import (@import) 指令从另一样式表文件导入规则。不过,应避免使用这些指令,因为它们会在关键路径中增加往返次数:只有在收到并解析完带有 @import 规则的 CSS 样式表之后,才会发现导入的 CSS 资源。

  • 内联阻塞渲染的 CSS

为获得最佳性能,您可能会考虑将关键 CSS 直接内联到 HTML 文档内。这样做不会增加关键路径中的往返次数,并且如果实现得当,在只有 HTML 是阻塞渲染的资源时,可实现“一次往返”关键路径长度。

Reflow 和 Repaint

reflow

在渲染过程中称为回流,发生在 Render Tree 阶段,它主要是用来确定每个元素在屏幕上的几何属性,需要大量计算每个元素的位置。在代码里每改变一个元素的几何属性,均会发生一次回流过程。

1
2
3
4
5
let styles = document.getElementsByTagName('body')[0].style;
styles.margin = '40px'; // reflow,repaint
styles.border = '40px solid #f00'; // reflow,repaint
styles.padding = '40px'; // reflow,repaint
styles.width = '300px'; // reflow,repaint

repaint

在渲染过程中称为重绘,发生在 reflow 过程之后,当元素的几何属性确定之后便要开始将元素绘制在屏幕上展示。repaint 执行过程就是将元素的颜色、背景等属性绘制出来。在代码里没改变一次元素的颜色等属性时均会对相关元素执行一次重绘。

1
2
3
let styles = document.getElementsByTagName('body')[0].style;
styles.color = '#fff'; // repaint
styles.backgroundColor = '#f00'; // repaint

添加预定义的 class,减少逐行修改元素样式的行为可以减少 reflow 和 repaint

参考资料:

Critical Rendering Path 关键渲染路径
Asynchronous and deferred JavaScript execution explained
浏览器渲染那些事之 Reflow、Repaint