发现和解决关键渲染路径性能瓶颈需要充分了解常见的陷阱。我们来实际操作一下,了解一下常见的性能模式,以帮助您优化网页。
优化关键渲染路径可让浏览器尽快绘制页面:更快的页面意味着更高的互动度、更多的网页浏览量和提高转化率。为了最大限度地减少访问者查看空白屏幕的时间,我们需要优化加载哪些资源以及加载顺序。
为帮助说明这一过程,我们从可能的最简单情况入手,逐步构建我们的页面,以包含其他资源、样式和应用逻辑。在此过程中,我们将对每一种情况进行优化,并找出可能出错的地方。
到目前为止,我们只关注了资源(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 中打开网络时间轴,并检查生成的资源瀑布:
不出所料,HTML 文件的下载时间约为 200 毫秒。请注意,蓝线的透明部分表示浏览器在未收到任何响应字节的情况下在网络上等待的时长,而实线部分表示在收到第一批响应字节后完成下载的时间。HTML 下载内容很小 (<4K),因此我们只需进行一次往返即可获取整个文件。因此,获取 HTML 文档需要大约 200 毫秒,其中一半的时间花费在等待网络上,另一半花费在等待服务器响应上。
当 HTML 内容可用时,浏览器会解析字节,将其转换为令牌,然后构建 DOM 树。请注意,为方便起见,开发者工具会在底部报告 DOMContentLoaded 事件的时间(216 毫秒),该时间也与蓝色竖线对应。HTML 下载结束与蓝色竖线 (DOMContentLoaded) 之间的间隔是浏览器构建 DOM 树所花费的时间,在本例中为几毫秒。
请注意,我们的“趣照”并未阻止 domContentLoaded
事件。事实证明,我们无需等待网页上的每个资源即可构建渲染树甚至绘制网页:并非所有资源对实现快速的首次渲染都至关重要。事实上,当我们谈论关键渲染路径时,通常谈论的是 HTML 标记、CSS 和 JavaScript。图片不会阻止网页的首次渲染,不过,我们还应设法尽快绘制图片。
不过,图片上的 load
事件(也称为 onload
)会被屏蔽:开发者工具会在 335 毫秒时报告 onload
事件。回想一下,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 文件的解析器,因此在下载并解析 CSS 文件之前,系统会阻止
domContentLoaded
事件:由于 JavaScript 可能会查询 CSSOM,因此我们必须先屏蔽 CSS 文件,直到该文件下载完毕,然后才能执行 JavaScript。
如果我们用内嵌脚本替换外部脚本会怎样?即使脚本直接内嵌到网页中,浏览器在构建 CSSOM 之前也无法执行脚本。简而言之,内联 JavaScript 也会阻止解析器。
也就是说,尽管内联脚本会阻止 CSS,但会不会加快网页渲染速度呢?我们来试试看,看看会发生什么。
外部 JavaScript:
内嵌 JavaScript:
我们减少了 1 个请求,但 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:
好多了!解析 HTML 后不久就会触发 domContentLoaded
事件;浏览器知道不应阻止 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 都是关键资源:只有在浏览器获取 HTML 文档之后才会获取 CSS,因此关键路径长度至少为两次往返。这两项资源共计 9 KB 的关键字节。
现在,让我们向组合中添加一个额外的 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 的关键字节
现在,我们拥有了三项关键资源,关键字节的总和达到了 11 KB,但我们的关键路径长度仍然是两次往返,因为我们可以并行传输 CSS 和 JavaScript。了解关键渲染路径的特性意味着能够识别关键资源,并了解浏览器如何安排资源的提取。让我们继续探讨示例。
在与网站开发者交流后,我们意识到我们在网页上包含的 JavaScript 不需要阻止;我们的部分分析和其他代码不需要阻止网页呈现。了解了这一点,我们就可以在脚本标记中添加“async”属性来解除对解析器的阻止:
<!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 文档),并且最短关键渲染路径长度为一次往返。