JavaScript 框架中的资源内嵌

改进整个 JavaScript 生态系统的 Largest Contentful Paint。

作为 Aurora 项目的一部分,Google 一直使用流行的 Web 框架,以确保这些框架根据核心网页指标获得良好的性能。Angular 和 Next.js 已经推出了字体内嵌功能,本文的第一部分对此进行了说明。我们要介绍的第二项优化是关键 CSS 内联,它现在在 Angular CLI 中默认处于启用状态,并且正在 Nuxt.js 中实现。

字体内嵌

在分析了数百个应用后,Aurora 团队发现,开发者往往会通过在 index.html<head> 元素中引用字体,在应用中添加这些字体。以下示例展示了在包含 Material 图标时的显示效果:

<!doctype html>
<html lang="en">
<head>
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  ...
</html>

虽然此模式完全有效和功能,但它会阻止应用的呈现并引入一个额外的请求。为了更好地了解具体情况,请查看上面 HTML 中引用的样式表的源代码:

/* fallback */
@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/font.woff2) format('woff2');
}

.material-icons {
  /*...*/
}

请注意 font-face 定义如何引用 fonts.gstatic.com 上托管的外部文件。加载应用时,浏览器首先必须下载 head 中引用的原始样式表。

一张图片,显示网站如何向服务器发出请求并下载外部样式表
首先,网站加载字体样式表。

接下来,浏览器会下载 woff2 文件,最后,它能够继续呈现应用。

一张图片,显示已发出的两项请求,一个针对字体样式表,另一个针对字体文件。
接下来,系统会发出加载字体的请求。

一种优化方法是在构建时下载初始样式表,并将其内嵌在 index.html 中。这样可在运行时跳过与 CDN 的整个往返过程,从而缩短阻塞时间。

构建应用时,系统会向 CDN 发送请求,这样会提取样式表并将其内嵌在 HTML 文件中,从而向网域添加 <link rel=preconnect>。采用这种方法,我们可以得到以下结果:

<!doctype html>
<html lang="en">
<head>
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin >
  <style type="text/css">
  @font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/font.woff2) format('woff2');}.material-icons{/*...*/}</style>
  ...
</html>

Next.js 和 Angular 现已支持字体内嵌

当框架开发者在底层工具中实现优化时,它们可以让现有应用和新应用更轻松地启用优化功能,从而为整个生态系统带来改进。

从 Next.js v10.2 和 Angular v11 开始,这项改进默认处于启用状态。两者都支持内嵌 Google 和 Adobe 字体。Angular 预计会在 v12.2 中引入后者。

您可以在 GitHub 上找到 Next.js 中的字体内嵌实现,并观看介绍在 Angular 环境中进行此优化的视频

内嵌关键 CSS

另一项增强功能涉及通过内嵌关键 CSS 来改进 First Contentful Paint (FCP)Largest Contentful Paint (LCP) 指标。一个网页的关键 CSS 包括网页初次呈现时使用的所有样式。如需详细了解该主题,请参阅推迟非关键 CSS

我们发现许多应用正在同步加载样式,这阻止了应用呈现。一种快速解决方法是异步加载样式。不要使用 media="all" 加载脚本,而应将 media 属性的值设置为 print,并在加载完成后将属性值替换为 all

<link rel="stylesheet" href="..." media="print" onload="this.media='all'">

不过,这种做法可能会导致未设置样式的内容闪烁。

页面似乎在样式加载时闪烁。

上面的视频展示了网页的渲染,会异步加载其样式。之所以发生闪烁,是因为浏览器会先开始下载样式,然后呈现后面的 HTML。浏览器下载样式后,会触发 link 元素的 onload 事件,将 media 属性更新为 all,并将样式应用于 DOM。

在呈现 HTML 和应用样式期间,网页会部分取消样式设置。 浏览器使用这些样式时会出现闪烁,这是糟糕的用户体验,还会导致 Cumulative Layout Shift (CLS) 回归。

关键 CSS 内嵌以及异步样式加载,可以改善加载行为。critters 工具通过查看样式表中的选择器并将其与 HTML 进行比对,确定网页上使用了哪些样式。找到匹配项时,它会将相应的样式视为关键 CSS 的一部分,并内嵌它们。

让我们看一个示例:

错误做法
<head>
   <link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'">
</head>
<body>
  <section>
    <button class="primary"></button>
  </section>
</body>
/* styles.css */
section button.primary {
  /* ... */
}
.list {
  /* ... */
}

内嵌之前的示例。

在上面的示例中,动物将读取并解析 styles.css 的内容,之后它会将两个选择器与 HTML 进行匹配,并发现我们使用 section button.primary。最后,critter 会在页面的 <head> 中内嵌相应的样式,从而:

正确做法
<head>
  <link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'">
  <style>
  section button.primary {
    /* ... */
  }
  </style>
</head>
<body>
  <section>
    <button class="primary"></button>
  </section>
</body>

内嵌后的示例。

在 HTML 中内联关键 CSS 后,您会发现页面闪烁问题已消失:

CSS 内嵌后的网页加载。

关键 CSS 内联现已在 Angular 中提供,并在 v12 中默认处于启用状态。如果您使用的是 v11,请在 angular.jsoninlineCritical 属性设置为 true 以启用它。如需在 Next.js 中启用此功能,请将 experimental: { optimizeCss: true } 添加到您的 next.config.js 中。

总结

在这篇博文中,我们谈到了 Chrome 与 Web 框架之间的一些协作。如果您是框架作者,并深知我们在技术中解决的一些问题,那么我们希望这些发现可以对您进行类似的性能优化提供启发。

详细了解改进内容。如需查看我们已针对核心网页指标所做的优化工作的完整列表,请参阅 Aurora 简介一文。