发布时间:2014 年 3 月 31 日
识别和解决关键渲染路径性能瓶颈需要充分了解常见的陷阱。引导式教程来识别常见的性能模式将帮助您优化页面。
优化关键渲染路径可以让浏览器尽快绘制页面:页面速度越快,用户的互动度就越高,页面浏览量就越多,转化率也越高。为了最大限度地减少访问者观看空白屏幕的时间,我们需要优化加载哪些资源以及以何种顺序加载。
为了帮助说明这个过程,我们从最简单的例子开始,逐步构建页面,添加额外的资源、样式和应用程序逻辑。在这个过程中,我们将优化每种情况;我们还将看到哪些地方可能会出错。
到目前为止,我们只关注资源(CSS、JS 或 HTML 文件)可用后浏览器中发生的事情。我们忽略了从缓存或网络中获取资源所需的时间。我们假设以下情况
- 到服务器的网络往返行程(传播延迟)成本为 100 毫秒。
- HTML 文档的服务器响应时间为 100 毫秒,所有其他文件的服务器响应时间为 10 毫秒。
Hello World 体验
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
从基本的 HTML 标记和单个图像开始;没有 CSS 或 JavaScript。然后,打开 Chrome DevTools 中的 Network 面板,并检查生成的资源瀑布流
正如预期的那样,HTML 文件下载大约需要 200 毫秒。请注意,蓝色线条的透明部分表示浏览器在网络上等待而未收到任何响应字节的时间长度,而实线部分表示在收到第一个响应字节后完成下载的时间。HTML 下载量很小(<4K),因此我们只需要一次往返行程即可获取完整的文件。因此,HTML 文档大约需要 200 毫秒才能获取,其中一半时间用于等待网络,另一半时间用于等待服务器响应。
当 HTML 内容可用时,浏览器会解析字节,将它们转换为令牌,并构建 DOM 树。请注意,DevTools 在底部方便地报告了 DOMContentLoaded 事件的时间(216 毫秒),这也与蓝色垂直线相对应。HTML 下载结束和蓝色垂直线 (DOMContentLoaded) 之间的差距是浏览器构建 DOM 树所需的时间——在本例中,只需几毫秒。
请注意,我们的“精彩照片”并没有阻止 domContentLoaded
事件。事实证明,我们可以构建渲染树,甚至可以在不等待页面上的每个资源的情况下绘制页面:并非所有资源对于快速首次绘制都是至关重要的。事实上,当我们谈论关键渲染路径时,我们通常谈论的是 HTML 标记、CSS 和 JavaScript。图像不会阻止页面的初始渲染——尽管我们也应该努力尽快绘制图像。
也就是说,load
事件(也称为 onload
)被图像阻止:DevTools 报告 onload
事件在 335 毫秒时发生。回想一下,onload
事件标记了页面所需所有资源都已下载和处理的点;此时,浏览器中的加载微调器可以停止旋转(瀑布流中的红色垂直线)。
在组合中添加 JavaScript 和 CSS
我们的“Hello World 体验”页面看起来很简单,但在后台却发生了很多事情。在实践中,我们需要的不仅仅是 HTML:很可能,我们将需要 CSS 样式表和一个或多个脚本来为我们的页面添加一些交互性。将两者都添加到组合中,看看会发生什么
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="timing.js"></script>
</body>
</html>
添加 JavaScript 和 CSS 之前
添加 JavaScript 和 CSS 之后
添加外部 CSS 和 JavaScript 文件会为我们的瀑布流添加两个额外的请求,所有这些请求都由浏览器在大约同一时间分派。但是,请注意,现在 domContentLoaded
和 onload
事件之间的时间差要小得多。
发生了什么?
- 与我们的纯 HTML 示例不同,我们还需要获取和解析 CSS 文件以构建 CSSOM,并且我们需要 DOM 和 CSSOM 才能构建渲染树。
- 由于页面还包含一个解析器阻塞 JavaScript 文件,因此
domContentLoaded
事件被阻止,直到 CSS 文件下载并解析完成:因为 JavaScript 可能会查询 CSSOM,所以我们必须阻止 CSS 文件,直到它下载完成,然后才能执行 JavaScript。
如果我们用内联脚本替换我们的外部脚本会怎样? 即使脚本直接内联到页面中,浏览器也无法执行它,直到 CSSOM 构建完成。简而言之,内联 JavaScript 也是解析器阻塞的。
也就是说,尽管阻塞了 CSS,但内联脚本是否会使页面渲染更快?试用一下,看看会发生什么。
外部 JavaScript
内联 JavaScript
我们减少了一个请求,但我们的 onload
和 domContentLoaded
时间实际上是相同的。为什么?好吧,我们知道 JavaScript 是内联还是外部的都无关紧要,因为一旦浏览器遇到脚本标签,它就会阻塞并等待直到 CSSOM 构建完成。此外,在我们的第一个示例中,浏览器并行下载 CSS 和 JavaScript,并且它们在大约同一时间完成下载。在这种情况下,内联 JavaScript 代码对我们没有太大帮助。但是,有几种策略可以使我们的页面渲染更快。
首先,回想一下,所有内联脚本都是解析器阻塞的,但对于外部脚本,我们可以添加 async
属性来解除对解析器的阻塞。撤消我们的内联,并尝试一下
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Async</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script async src="timing.js"></script>
</body>
</html>
解析器阻塞(外部)JavaScript
异步(外部)JavaScript
好多了!domContentLoaded
事件在 HTML 解析完成后不久触发;浏览器知道不要阻塞 JavaScript,并且由于没有其他解析器阻塞脚本,CSSOM 构建也可以并行进行。
或者,我们可以内联 CSS 和 JavaScript
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Inlined</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
请注意,domContentLoaded
时间与上一个示例中的时间实际上相同;我们没有将 JavaScript 标记为异步,而是将 CSS 和 JS 都内联到页面本身中。这使得我们的 HTML 页面更大,但好处是浏览器不必等待获取任何外部资源;一切都在页面中。
正如您所看到的,即使使用非常基本的页面,优化关键渲染路径也是一项非平凡的工作:我们需要了解不同资源之间的依赖关系图,我们需要确定哪些资源是“关键的”,并且我们必须在如何在页面上包含这些资源的不同策略之间做出选择。这个问题没有一个通用的解决方案;每个页面都是不同的。您需要在您自己的页面上遵循类似的过程,以找出最佳策略。
也就是说,看看我们是否可以退后一步,识别出一些通用的性能模式。
性能模式
最简单的页面只包含 HTML 标记;没有 CSS、没有 JavaScript 或其他类型的资源。要渲染此页面,浏览器必须启动请求,等待 HTML 文档到达,解析它,构建 DOM,然后最终在屏幕上渲染它
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
T0 和 T1 之间的时间捕获了网络和服务器处理时间。 在最佳情况下(如果 HTML 文件很小),一次网络往返行程即可获取整个文档。由于 TCP 传输协议的工作方式,较大的文件可能需要更多往返行程。因此,在最佳情况下,上述页面具有一次往返行程(最小)的关键渲染路径。
现在考虑相同的页面,但使用外部 CSS 文件
<!DOCTYPE html>
<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>
</body>
</html>
再一次,我们产生一次网络往返行程来获取 HTML 文档,然后检索到的标记告诉我们,我们还需要 CSS 文件;这意味着浏览器必须返回服务器并获取 CSS,然后才能在屏幕上渲染页面。因此,此页面在可以显示之前至少需要两次往返行程。 再次强调,CSS 文件可能需要多次往返行程,因此强调“最小”。
以下是我们用来描述关键渲染路径的一些术语
- 关键资源: 可能会阻止页面初始渲染的资源。
- 关键路径长度: 往返行程数,或获取所有关键资源所需的总时间。
- 关键字节: 首次渲染页面所需的总字节数,它是所有关键资源的传输文件大小的总和。我们的第一个示例,只有一个 HTML 页面,包含一个关键资源(HTML 文档);关键路径长度也等于一次网络往返行程(假设文件很小),并且总关键字节数只是 HTML 文档本身的传输大小。
现在将此与之前 HTML 和 CSS 示例的关键路径特征进行比较
- 2 个关键资源
- 2 次或更多次往返行程,以获得最小关键路径长度
- 9 KB 的关键字节
我们需要 HTML 和 CSS 才能构建渲染树。因此,HTML 和 CSS 都是关键资源:CSS 仅在浏览器获取 HTML 文档后才获取,因此关键路径长度至少为两次往返行程。这两种资源加起来总共有 9KB 的关键字节。
现在在组合中添加一个额外的 JavaScript 文件。
<!DOCTYPE html>
<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>
我们添加了 app.js
,它既是页面上的外部 JavaScript 资产,又是解析器阻塞(即关键)资源。更糟糕的是,为了执行 JavaScript 文件,我们必须阻塞并等待 CSSOM;回想一下,JavaScript 可以查询 CSSOM,因此浏览器会暂停,直到 style.css
下载完成并且 CSSOM 构建完成。
也就是说,在实践中,如果我们查看此页面的“网络瀑布流”,您会看到 CSS 和 JavaScript 请求几乎在同一时间启动;浏览器获取 HTML,发现这两个资源,并启动这两个请求。因此,上一张图片中显示的页面具有以下关键路径特征
- 3 个关键资源
- 2 次或更多次往返行程,以获得最小关键路径长度
- 11 KB 的关键字节
我们现在有三个关键资源,总共有 11KB 的关键字节,但我们的关键路径长度仍然是两次往返行程,因为我们可以并行传输 CSS 和 JavaScript。弄清楚关键渲染路径的特征意味着能够识别关键资源,并了解浏览器将如何安排它们的获取。
在与我们的网站开发人员聊天后,我们意识到我们包含在页面上的 JavaScript 不需要阻塞;我们在其中包含了一些分析和其他代码,这些代码不需要阻塞我们页面的渲染。有了这些知识,我们可以将 async
属性添加到 <script>
元素以解除对解析器的阻塞
<!DOCTYPE html>
<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>
异步脚本有几个优点
- 该脚本不再是解析器阻塞的,也不再是关键渲染路径的一部分。
- 由于没有其他关键脚本,因此 CSS 不需要阻塞
domContentLoaded
事件。 domContentLoaded
事件越早触发,其他应用程序逻辑就可以越早开始执行。
因此,我们优化的页面现在又回到了两个关键资源(HTML 和 CSS),最小关键路径长度为两次往返行程,总共有 9KB 的关键字节。
最后,如果 CSS 样式表仅用于打印,那会是什么样子?
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" media="print" />
</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>
由于 style.css 资源仅用于打印,因此浏览器无需阻塞它来渲染页面。因此,一旦 DOM 构建完成,浏览器就有足够的信息来渲染页面。因此,此页面只有一个关键资源(HTML 文档),并且最小关键渲染路径长度为一次往返行程。