【译】理解关键渲染路径

原文链接:Understanding the Critical Rendering Path

作者:Ire Aderinokun

翻译:野草

本文发表于前端早读课【第875期】

有一个很经典的面试题:当你在浏览器输入一个网址并按下回车之后发生了什么?今天我们就来说说当浏览器从服务器获取了HTML文件之后经历了什么。事实上,从获取HTML文件直到浏览器以像素点的方式在屏幕中绘制出页面的内容确实经历了很多步骤,这些步骤我们称之为关键渲染路径(Critial Rendering Path)

理解关键渲染路径是提高页面性能的关键所在。总体来说,关键渲染路径分为六步。

  • 创建DOM树(Constructing the DOM Tree)
  • 创建CSSOM树(Constructing the CSSOM Tree)
  • 执行脚本(Running JavaScript)
  • 生成渲染树(Creating the Render Tree)
  • 生成布局(Generating the Layout)
  • 绘制(Painting)

创建DOM树

DOM(文档对象模型)树是HTML页面完全解析后的一种表示方式。从根元素<html>开始,页面上每个元素或者文本都会创建一个对应的节点。每个节点都包含了这个元素的所有属性,并且嵌套在元素内的元素会被解析成外层元素对应的节点的子节点。比如,元素<a>标签对应的节点会有一个属性为href对应的子节点。

我们来看下面这个简单的HTML结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<title>Understanding the Critical Rendering Path</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Understanding the Critical Rendering Path</h1>
</header>
<main>
<h2>Introduction</h2>
<p>Lorem ipsum dolor sit amet</p>
</main>
<footer>
<small>Copyright 2017</small>
</footer>
</body>
</html>

它将会被解析生成以下的DOM树。

幸运的是,HTML可以部分执行并显示,也就是说,浏览器并不需要等待整个HTML全部解析完毕才开始显示页面。但是,其他的资源有可能阻塞页面的渲染,比如CSS,JavaScript等。

创建CSSOM树

CSSOM(CSS对象模型)树是对附在DOM结构上的样式的一种表示方式。它与DOM树的呈现方式相似,只是每个节点都带上样式 ,包括明确定义的和隐式继承的。

在上述的HTML页面中,style.css文件代码如下:

1
2
3
4
5
6
7
8
9
body { font-size: 18px; }
header { color: plum; }
h1 { font-size: 28px; }
main { color: firebrick; }
h2 { font-size: 20px; }
footer { display: none; }

由此会生成以下的CSSOM树。

CSS是一种渲染阻塞资源(render blocking resource),它需要完全被解析完毕之后才能进入生成渲染树的环节。CSS并不像HTML那样能执行部分并显示,因为CSS具有继承属性, 后面定义的样式会覆盖或者修改前面的样式。如果我们只使用样式表中部分解析好的样式,我们可能会得到错误的页面效果。所以,我们只能等待CSS完全解析之后,才能进入关键渲染路径的下一环节。

需要注意的是,只有CSS文件适用于当前设备的时候,才能造成渲染阻塞。标签<link rel=”stylesheet”>接受media属性,该属性规定了此处的CSS文件适用于哪种设备。如果我们有个设备属性值为orientation: landscape(横向)的样式,当我们竖着浏览页面的时候,这个CSS资源是不会起作用的,也就不会阻塞渲染的过程了。

因为JavaScript脚本的执行必须等到CSSOM生成之后,所以说CSS也会阻塞脚本(script blocking)

执行JavaScript

JavaScript是一种解析阻塞资源(parser blocking resource),它能阻塞HTML页面的解析。

当页面解析到<script>标签,不管脚本是內联的还是外联,页面解析都会暂停,转而加载JavaScript文件(外联的话)并且执行JavaScript。这也是为什么如果JavaScript文件有引用HTML文档中的元素,JavaScript文件必须放在那个元素的后面。

为了避免JavaScript文件阻塞页面的解析,我们可以在<script>标签上添加async属性,使得JavaScript文件异步加载。

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

生成渲染树

渲染树是DOM和CSSOM的结合,是最终能渲染到页面的元素的树形结构表示。也就是说,它包含能在页面中最终呈现的元素,而不包含那些用CSS样式隐藏的元素,比如带有display: none;属性的元素。

所以,上述例子的渲染树如下所示。

生成布局

布局决定了视口的大小,为CSS样式提供了依据,比如百分比的换算或者视口的总像素值。视口大小是由meta标签的name属性为viewport的内容设置所决定的,如果缺少这个标签,默认的视口大小为980px。

最常见的viewport设置是自适应于设备尺寸,设置如下:

1
<meta name="viewport" content="width=device-width,initial-scale=1">

假设用户访问一个显示在设备宽度为1000px的页面,一半的视口大小就是500px,10%就是100px,以此类推。

绘制

最后,页面上可见的内容就会转化为屏幕上的像素点。

绘制过程所需要花费的时间取决于DOM的大小以及元素的CSS样式。有些样式比较耗时,比如一个复杂的渐变背景色比起简单的单色背景需要更多的时间来渲染。

总结

我们可以利用Chrome开发者工具下的Timeline去观察整个关键路径渲染的过程。

我们还是拿上面这个最简单的HTML例子(添加了<script>标签)

<html>
<head>  
  <title>Understanding the Critical Rendering Path</title>
  <link rel="stylesheet" href="style.css">
</head>  
<body>  
  <header>
      <h1>Understanding the Critical Rendering Path</h1>
  </header>
  <main>
      <h2>Introduction</h2>
      <p>Lorem ipsum dolor sit amet</p>
  </main>
  <footer>
      <small>Copyright 2017</small>
  </footer>
  <script src="main.js"></script>
</body>  
</html>

我们查看页面加载过程的Event log选项,我们就得到以下的结果:

  • 发送请求(Send Request) —— 发送GET请求获取index.html
  • 解析HTML(Parse HTML),再次发送请求 —— 开始解析HTML文件,创建DOM结构,发送请求获取style.css和main.js
  • 解析样式文件(Parse Stylesheet) —— 根据style.css生成CSSOM树
  • 执行脚本(Evaluate Script) —— 执行main.js
  • 生成布局(Layout) —— 基于HTML页面中的meta viewport标签生成布局
  • 绘制(Paint) —— 在浏览器页面绘制像素点