优化资源加载

在上一模块中,我们探讨了关键渲染路径背后的一些理论,以及渲染阻塞和解析器阻塞资源如何延迟页面的初始渲染。现在您已经了解了这背后的一些理论,您就可以学习一些优化关键渲染路径的技术了。

当页面加载时,其 HTML 中引用了许多资源,这些资源通过 CSS 为页面提供外观和布局,并通过 JavaScript 提供交互性。在本模块中,我们将介绍许多与这些资源以及它们如何影响页面加载时间相关的重要概念。

渲染阻塞

正如上一模块中所讨论的,CSS 是一种渲染阻塞资源,因为它会阻止浏览器渲染任何内容,直到CSS 对象模型 (CSSOM)构建完成。浏览器阻止渲染是为了防止无样式内容闪烁 (FOUC),这从用户体验的角度来看是不可取的。

在前面的视频中,有一个短暂的 FOUC,您可以在其中看到没有任何样式的页面。随后,一旦页面的 CSS 完成从网络加载,所有样式都会应用,并且未样式化的页面版本会立即被样式化的版本替换。

一般来说,FOUC 是您通常看不到的东西,但理解这个概念很重要,这样您就知道为什么浏览器会阻止页面渲染,直到 CSS 下载并应用于页面。渲染阻塞不一定是不可取的,但您确实希望通过优化 CSS 来最大限度地缩短其持续时间。

解析器阻塞

解析器阻塞资源会中断 HTML 解析器,例如没有 asyncdefer 属性的 <script> 元素。当解析器遇到 <script> 元素时,浏览器需要在继续解析 HTML 的其余部分之前评估并执行脚本。这是按设计进行的,因为脚本可能会在 DOM 仍在构建时修改或访问 DOM。

<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>

当使用外部 JavaScript 文件(没有 asyncdefer)时,解析器会从发现文件时起一直被阻塞,直到文件下载、解析和执行完毕。当使用内联 JavaScript 时,解析器也会被类似地阻塞,直到内联脚本被解析和执行。

预加载扫描器

预加载扫描器是一种浏览器优化,它以辅助 HTML 解析器的形式扫描原始 HTML 响应,以在主 HTML 解析器以其他方式发现资源之前查找并推测性地获取资源。例如,即使 HTML 解析器在获取和处理 CSS 和 JavaScript 等资源时被阻塞,预加载扫描器也允许浏览器开始下载 <img> 元素中指定的资源。

为了利用预加载扫描器,关键资源应包含在服务器发送的 HTML 标记中。以下资源加载模式无法被预加载扫描器发现

  • 使用 background-image 属性通过 CSS 加载的图片。这些图片引用在 CSS 中,并且无法被预加载扫描器发现。
  • 动态加载的脚本,以 <script> 元素标记的形式注入到使用 JavaScript 的 DOM 中,或使用 动态 import() 加载的模块。
  • 客户端使用 JavaScript 渲染的 HTML。此类标记包含在 JavaScript 资源中的字符串中,并且无法被预加载扫描器发现。
  • CSS @import 声明。

这些资源加载模式都是后期发现的资源,因此无法从预加载扫描器中受益。尽可能避免它们。但是,如果无法避免此类模式,您或许可以使用 preload 提示来避免资源发现延迟。

CSS

CSS 决定了页面的呈现和布局。如前所述,CSS 是一种渲染阻塞资源,因此优化 CSS 可能会对整体页面加载时间产生相当大的影响。

最小化

最小化 CSS 文件会减小 CSS 资源的文件大小,使其下载速度更快。这主要是通过从源 CSS 文件中删除空格和其他不可见字符等内容,并将结果输出到新优化的文件来实现的

/* Unminified CSS: */

/* Heading 1 */
h1 {
  font-size: 2em;
  color: #000000;
}

/* Heading 2 */
h2 {
  font-size: 1.5em;
  color: #000000;
}
/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}

在其最基本的形式中,CSS 最小化是一种有效的优化,可以改善您网站的 FCP,甚至在某些情况下可以改善 LCP。诸如 捆绑器之类的工具可以在生产构建中自动为您执行此优化。

删除未使用的 CSS

在渲染任何内容之前,浏览器需要下载并解析所有样式表。完成解析所需的时间还包括当前页面上未使用的样式。如果您使用的是将所有 CSS 资源合并到单个文件中的捆绑器,则您的用户可能会下载比渲染当前页面所需的 CSS 更多的 CSS。

要发现当前页面未使用的 CSS,请使用 Chrome DevTools 中的Coverage 工具

A screenshot of the coverage tool in Chrome DevTools. A CSS file is selected in its bottom pane, showing a considerable amount of CSS unused by the current page layout.
Chrome DevTools 中的 Coverage 工具对于检测当前页面未使用的 CSS(和 JavaScript)非常有用。它可以用于将 CSS 文件拆分为多个资源以供不同页面加载,而不是交付一个可能会延迟页面渲染的更大的 CSS 包。

删除未使用的 CSS具有双重效果:除了减少下载时间外,您还在优化渲染树构建,因为浏览器需要处理的 CSS 规则更少。

避免 CSS @import 声明

虽然它可能看起来很方便,但您应该避免在 CSS 中使用 @import 声明

/* Don't do this: */
@import url('style.css');

与 HTML 中 <link> 元素的工作方式类似,CSS 中的 @import 声明允许您从样式表中导入外部 CSS 资源。这两种方法之间的主要区别在于 HTML <link> 元素是 HTML 响应的一部分,因此比 @import 声明下载的 CSS 文件发现得更早。

原因是为了发现 @import 声明,必须首先下载包含它的 CSS 文件。这会导致所谓的请求链,在 CSS 的情况下,这会延迟页面初始渲染所需的时间。另一个缺点是使用 @import 声明加载的样式表无法被预加载扫描器发现,因此成为后期发现的渲染阻塞资源。

<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">

在大多数情况下,您可以使用 <link rel="stylesheet"> 元素替换 @import<link> 元素允许并发下载样式表并减少整体加载时间,而不是 @import 声明,后者连续下载样式表。

内联关键 CSS

下载 CSS 文件所需的时间会增加页面的 FCP。在文档 <head> 中内联关键样式消除了 CSS 资源的网络请求,并且在正确完成的情况下,当用户的浏览器缓存未预热时,可以改善初始加载时间。剩余的 CSS 可以异步加载,或附加在 <body> 元素的末尾。

<head>
  <title>Page Title</title>
  <!-- ... -->
  <style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
  <!-- Other page markup... -->
  <link rel="stylesheet" href="non-critical.css">
</body>

不利的一面是,内联大量 CSS 会向初始 HTML 响应添加更多字节。由于 HTML 资源通常无法缓存很长时间——甚至根本无法缓存——这意味着内联 CSS 不会为可能在外部样式表中使用相同 CSS 的后续页面缓存。测试和衡量您页面的性能,以确保权衡是值得的。

CSS 演示

JavaScript

JavaScript 驱动着 Web 上的大部分交互性,但它是有代价的。交付过多的 JavaScript 会使您的网页在页面加载期间响应缓慢,甚至可能导致响应问题,从而减慢交互速度,这两者都会让用户感到沮丧。

渲染阻塞 JavaScript

当加载没有 deferasync 属性的 <script> 元素时,浏览器会阻止解析和渲染,直到脚本下载、解析和执行完毕。同样,内联脚本也会阻止解析器,直到脚本解析和执行完毕。

asyncdefer

asyncdefer 允许外部脚本在不阻塞 HTML 解析器的情况下加载,而带有 type="module" 的脚本(包括内联脚本)会自动延迟。但是,asyncdefer 有一些重要的区别需要理解。

A depiction of various script loading mechanisms, all detailing the parser, fetch, and execution roles based on various attributes used such as async, defer, type='module' and a combination of all three.
来源:https://html.whatwg.com.cn/multipage/scripting.html

使用 async 加载的脚本在下载后立即解析和执行,而使用 defer 加载的脚本在 HTML 文档解析完成后执行——这与浏览器的 DOMContentLoaded 事件同时发生。此外,async 脚本可能会乱序执行,而 defer 脚本会按照它们在标记中出现的顺序执行。

客户端渲染

一般来说,您应该避免使用 JavaScript 渲染任何关键内容或页面的 LCP 元素。这被称为客户端渲染,并且是在单页应用程序 (SPA) 中广泛使用的一种技术。

JavaScript 渲染的标记绕过了预加载扫描器,因为客户端渲染标记中包含的资源无法被其发现。这可能会延迟关键资源的下载,例如 LCP 图片。浏览器仅在脚本执行并添加到 DOM 后才开始下载 LCP 图片。反过来,脚本只能在被发现、下载和解析后才能执行。这被称为关键请求链,应避免使用。

此外,与从服务器下载以响应导航请求的标记相比,使用 JavaScript 渲染标记更有可能生成长任务。大量使用客户端 HTML 渲染会对交互延迟产生负面影响。在页面 DOM 非常大的情况下尤其如此,这会在 JavaScript 修改 DOM 时触发大量的渲染工作。

最小化

与 CSS 类似,最小化 JavaScript 会减小脚本资源的文件大小。这可以加快下载速度,使浏览器更快地进入解析和编译 JavaScript 的过程。

此外,JavaScript 的最小化比最小化其他资源(如 CSS)更进一步。当 JavaScript 被最小化时,它不仅会剥离空格、制表符和注释等内容,而且源 JavaScript 中的符号也会被缩短。此过程有时称为丑化。要查看差异,请查看以下 JavaScript 源代码

// Unuglified JavaScript source code:
export function injectScript () {
  const scriptElement = document.createElement('script');
  scriptElement.src = '/js/scripts.js';
  scriptElement.type = 'module';

  document.body.appendChild(scriptElement);
}

当前面的 JavaScript 源代码被丑化时,结果可能看起来像以下代码片段

// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}

在前面的代码片段中,您可以看到源中人类可读的变量 scriptElement 被缩短为 t。当应用于大量脚本集合时,节省量可能相当可观,而不会影响网站生产 JavaScript 提供的功能。

如果您使用捆绑器来处理您网站的源代码,则通常会自动为生产构建完成丑化。丑化器(例如 Terser)也是高度可配置的,这使您可以调整丑化算法的激进程度以实现最大程度的节省。但是,任何丑化工具的默认值通常都足以在输出大小和功能保留之间取得适当的平衡。

JavaScript 演示

测试您的知识

在浏览器中加载多个 CSS 文件的最佳方法是什么?

CSS @import 声明。
多个 <link> 元素。

浏览器预加载扫描器有什么作用?

它是一个辅助 HTML 解析器,它检查原始标记以在 DOM 解析器可以之前发现资源,以便更早地发现它们。
检测 HTML 资源中的 <link rel="preload"> 元素。

为什么浏览器在下载 JavaScript 资源时默认会暂时阻止 HTML 解析?

因为脚本可以修改或以其他方式访问 DOM。
因为评估 JavaScript 是一项非常占用 CPU 的任务,暂停 HTML 解析可以为 CPU 提供更多带宽来完成加载脚本。
为了防止无样式内容闪烁 (FOUC)。

下一步:使用资源提示协助浏览器

现在您已经掌握了在 <head> 元素中加载的资源如何影响初始页面加载和各种指标,是时候继续前进了。在下一模块中,我们将探索资源提示,以及它们如何为浏览器提供有价值的提示,使其比没有提示的情况下更早地开始加载资源和打开与跨域服务器的连接。