一、速度为什么很重要?
1.效果关乎留存用户
BBC 发现其网站的加载速度每增加一秒,就会额外损失 10% 的用户。
2.效果关乎转化次数的增加
速度较快的网站可以提高转化率并改善业务成果。
3.性能关乎用户体验
对网页加载速度延迟的压力反应类似于观看恐怖电影或解决数学问题,并且比在零售店排队等候的压力更大。
二、HTML 性能的一般注意事项
该 HTML 的初始请求需要经历多个步骤,每一步都需要一些时间。减少在每个步骤上花费的时间,可加快至第一字节的时间 (TTFB)。虽然 TTFB 不是您在网页加载速度方面应该关注的唯一指标,但较大的 TTFB 确实会导致难以达到 Largest Contentful Paint (LCP) 和 First Contentful Paint (FCP) 等指标的指定“良好”阈值。
1.尽量减少重定向
重定向会降低网页加载速度,因为它需要浏览器在新位置发出额外的 HTTP 请求,以检索资源。
2.缓存 HTML 响应
缓存 HTML 响应很困难,因为响应可能包含指向其他关键资源(如 CSS、JavaScript、图片和其他资源类型)的链接。这些资源的文件名中可能包含唯一指纹,该指纹因文件内容而异。这意味着,缓存的 HTML 文档可能会在部署后过时,因为它包含对过时子资源的引用。
较短的缓存生命周期(而不是无缓存)具有诸多优势,例如允许在 CDN 中缓存资源,减少从源服务器传送的请求数量,并在浏览器中允许资源重新验证而不是再次下载。此方法最适合在任何上下文中不会更改的静态内容,并且您可以将缓存资源的适当时间设置为您认为合适的分钟数。为静态 HTML 资源分配 5 分钟是一种安全选择,可确保定期更新不会被注意到。
3.衡量服务器响应时间
与可立即传送静态网页(无需在后端花费大量计算时间)相比,提供动态生成的响应(例如从数据库提取数据)的网页的 TTFB 可能更高。
如果用户在“字段”中遇到 TTFB 缓慢的问题,您可以使用 Server-Timing 响应标头公开有关在服务器上的时间停留的时间的信息:
Server-Timing: auth;dur=55.5, db;dur=220
4.压缩
应压缩 HTML、JavaScript、CSS 和 SVG 图片等文本响应,以缩减通过网络传输的数据量,加快下载速度。最常用的压缩算法是 gzip 和 Brotli。Brotli 比 gzip 提高了大约 15% 到 20%。
5.内容分发网络 (CDN)
内容分发网络 (CDN) 是一个分布式服务器网络,从您的源服务器缓存资源,进而从物理上距离用户更近的边缘服务器传送资源。由于靠近用户的物理位置可以缩短往返时间 (RTT),而 HTTP/2 或 HTTP/3、缓存和压缩等优化技术可让 CDN 更快地提供内容(与从源服务器提取内容相比)。在某些情况下,使用 CDN 可以显著改善您网站的 TTFB。
三、优化资源加载
1.渲染阻塞
CSS 是一种阻塞渲染的资源,因为它会在构建 CSS 对象模型 (CSSOM) 之前阻止浏览器渲染任何内容。浏览器会阻止呈现,以防止出现无样式内容闪烁 (FOUC)。一旦页面的 CSS 从网络加载完毕,系统便会应用所有样式,并且页面的无样式版本会立即替换为样式化版本。需要通过对 CSS 进行优化来最大限度地缩短其持续时长。
2.解析器屏蔽
解析器阻止资源会中断 HTML 解析器,例如没有 async 或 defer 属性的 <script>
元素。当解析器遇到<script>
元素时,浏览器需要先评估并执行脚本,然后才能继续解析 HTML 的其余部分。这是特意设计的,因为在 DOM 构建过程中,脚本可能会修改或访问 DOM。
3.预加载扫描程序
预加载扫描程序是一种浏览器优化功能,它采用辅助 HTML 解析器的形式,可扫描原始 HTML 响应,以便先找到资源并进行推测性提取,然后再让主要 HTML 解析器发现这些资源。例如,预加载扫描程序允许浏览器开始下载 <img>
元素中指定的资源,即使在提取和处理 CSS 和 JavaScript 等资源时 HTML 解析器遭到阻止,也是如此。
为了充分利用预加载扫描器,关键资源应包含在服务器发送的 HTML 标记中。预加载扫描程序无法发现以下资源加载模式:
- 由 CSS 使用 background-image 属性加载的图片。这些图片引用位于 CSS 中,并且预加载扫描器无法发现这些引用。
- 动态加载的脚本,采用
<script>
元素标记的形式,使用 JavaScript 或使用动态 import() 加载的模块注入 DOM。- 使用 JavaScript 在客户端上呈现的 HTML。此类标记包含在 JavaScript资源的字符串中,预加载扫描程序无法发现此类标记。
- CSS @import 声明。
这些资源加载模式都是最晚发现的资源,因此不会受益于预加载扫描程序。请尽可能避免。但是,如果无法避免此类模式,您或许可以使用 preload 提示来避免资源发现延迟。
4.CSS
CSS 是一种阻塞渲染的资源,因此优化 CSS 可能会对整体网页加载时间产生重大影响。
5.缩减大小
缩减 CSS 文件大小可缩减 CSS 资源的文件大小,从而加快下载速度。这主要是通过从 CSS 源文件中移除内容(如空格和其他不可见字符),然后将结果输出到新优化的文件来实现的。
CSS 缩减功能是一种有效的优化,可以提高网站的 FCP,在某些情况下甚至可能会提高 LCP。捆绑器等工具可以在正式版 build 中自动为您执行此优化。
6.移除未使用的 CSS
在呈现任何内容之前,浏览器需要下载并解析所有样式表。完成解析所需的时间还包括当前网页上未使用的样式。如果您使用捆绑器将所有 CSS 资源合并到一个文件中,则您的用户下载的 CSS 数量可能会超过呈现当前网页所需的 CSS 数量。
7.避免使用 CSS @import 声明
虽然这看起来很方便,但您应避免在 CSS 中使用 @import 声明,必须先下载包含该声明的 CSS 文件。这会产生所谓的请求链(对于 CSS 而言),它会延迟网页首次呈现所需的时间。另一个缺点是,预加载扫描器找不到使用 @import 声明加载的样式表,因而会成为延迟发现的阻塞渲染的资源。在大多数情况下,您可以使用<link rel="stylesheet">
元素替换 @import。与 @import 声明相反,@import 声明可以连续下载样式表。
8.内嵌关键 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 不会被缓存。因此,请测试和衡量网页的性能,确保值得做出权衡取舍。
9. 阻塞渲染的 JavaScript
如果加载不含 defer 或 async 属性的
10.async 与 defer
async 和 defer 允许在不阻止 HTML 解析器的情况下加载外部脚本,而使用 type=“module” 的脚本(包括内嵌脚本)则会自动推迟。不过,async 和 defer 存在一些差异,请务必了解。
使用 async 加载的脚本会在下载后立即解析和执行,而使用 defer 加载的脚本会在 HTML 文档解析完成时执行 - 这与浏览器的 DOMContentLoaded 事件同时发生。此外,async 脚本可能会不按顺序执行,而 defer 脚本则会按照标记在标记中出现的顺序执行。
11.客户端渲染
通常,您应避免使用 JavaScript 来渲染任何关键内容或网页的 LCP 元素。
由 JavaScript 呈现的标记会绕过预加载扫描程序,因为它无法发现客户端呈现的标记中包含的资源。这可能会导致 LCP 图片等关键资源的下载延迟。只有在脚本执行完毕并将元素添加到 DOM 后,浏览器才开始下载 LCP 图片。反过来,脚本只能在被发现、下载和解析后执行。这称为关键请求链,应尽量避免。
12.缩减大小
与 CSS 类似,缩减 JavaScript 大小可缩减脚本资源的文件大小。 这样可以提高下载速度,使浏览器能够更快地解析和编译 JavaScript。缩减 JavaScript 的大小比缩减其他资源(如 CSS)更进一步。在缩减 JavaScript 的大小后,它不仅会去除空格、制表符和注释等内容,而且 JavaScript 源中的符号也会被缩短。这个过程有时称为“伪装”。
四、通过资源提示协助浏览器
资源提示可以告知浏览器如何加载资源并确定资源的优先级,从而帮助开发者进一步优化网页加载时间。一组初始资源提示(例如 preconnect 和 dns-prefetch)是第一个引入的资源提示。不过,随着时间的推移,preload 和 Fetch Priority API 会相继提供额外的功能。
资源提示可以在 HTML 中指定,也可以设置为 HTTP 标头。此模块的讨论范围涵盖 preconnect、dns-prefetch 和 preload,以及 prefetch 提供的推测性提取行为。
1.preconnect
preconnect 提示用于与另一个源(您要从其中获取关键资源)建立连接。例如,您可能在 CDN 或其他跨源上托管了图片或资源:
<link rel="preconnect" href="https://example.com">
使用 preconnect,您可以预计浏览器计划在不久的将来连接到特定的跨源服务器,并且浏览器应尽快打开该连接,最好是在等待 HTML 解析器或预加载扫描程序执行此操作之前。
如果网页上有大量跨源资源,请对对当前网页最关键的资源使用 preconnect。
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
crossorigin 属性用于指示是否必须使用跨域资源共享 (CORS) 提取资源。使用 preconnect 提示时,如果从来源下载的资源使用 CORS(例如字体文件),则您需要将 crossorigin 属性添加到 preconnect 提示中。
如果您省略 crossorigin
属性,浏览器会在下载字体文件时打开新的连接,并且不会重复使用通过 preconnect
提示打开的连接。
在 Chrome 开发者工具的网络面板中看到的连接时间可视化图表。红色框中的时间表示与跨源服务器建立连接时涉及的时间,preconnect 可以通过比发现跨源资源时更快地建立连接来缓解此问题。
2.dns-prefetch
同时与许多跨源服务器建立连接可能是不合理的或不可行的。如果您担心可能过度使用 preconnect,可以使用 dns-prefetch 提示来使用开销更低的资源提示。
dns-prefetch 不会与跨源服务器建立连接,而只是提前为其执行 DNS 查找。将域名解析为其底层 IP 地址时会发生 DNS 查询。虽然设备和网络级别的 DNS 缓存有助于加快此过程,但仍然需要一些时间。
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
DNS 查找的费用相当低廉,并且由于费用相对较小,在某些情况下,DNS 查找可能比 preconnect 更适合。特别是,当有链接会转到您认为用户可能会关注的其他网站时,使用它可能是有用的资源提示。
dnstradamus 就是这样一种使用 JavaScript 自动执行此操作的工具,它会使用 Intersection Observer API 在指向其他网站的链接滚动到用户的视图时,将 dns-prefetch 提示注入当前页面的 HTML 中。
3.preload
preload 指令用于针对呈现网页所需的资源发起提前请求:
<link rel="preload" href="/lcp-image.jpg" as="image">
preload 指令应仅限于延迟发现的关键资源。最常见的用例是字体文件、通过 @import 声明提取的 CSS 文件,或可能是 Largest Contentful Paint (LCP) 候选对象的 CSS background-image 资源。在这种情况下,预加载扫描程序将不会发现这些文件,因为相关资源是在外部资源中引用的。
使用 preload 指令下载的资源将以高优先级有效下载,如果过度使用,preload 可能会以对网页加载速度产生负面影响的方式产生带宽争用。
如果您要预加载 CORS 资源(例如字体),则 preload 指令也需要 crossorigin 属性。如果您不添加 crossorigin 属性(或者为非 CORS 请求添加该属性),则浏览器将下载两次资源,浪费本来本应花费在其他资源上的带宽。
<link rel="preload" href="/font.woff2" as="font" crossorigin>
在上述 HTML 代码段中,指示浏览器使用 CORS 请求预加载 /font.woff2,即使 /font.woff2 属于同一网域。
如果 preload 指令的 元素中缺少 as 属性,该指令会下载两次该资源
4.prefetch
prefetch 指令用于针对可能会用于未来导航的资源发起低优先级请求:
<link rel="prefetch" href="/next-page.css" as="style">
该指令在很大程度上遵循与 preload 指令相同的格式,但只有 元素的 rel 属性改用 “prefetch” 值。 不过,与 preload 指令不同的是,prefetch 主要是推测性的,因为您将发起对某个资源的提取,以便将来的导航操作不一定会发生。
有时候,prefetch 是有益的 - 例如,如果您在网站上确定了大多数用户会完成的操作用户流,那么为未来网页使用一个渲染关键型资源 prefetch 有助于缩短这些网页的加载时间。
鉴于 prefetch 的推测性,使用它存在潜在的缺点,即如果用户没有转到最终需要预提取资源的页面,则用于提取资源的数据可能会被未使用的数据占用。
5.提取优先级 API
可以通过其 fetchpriority 属性使用 Fetch Priority API 来提高资源的优先级。您可以将该属性与 <link>
、<img>
和 <script>
元素一起使用。
<div class="gallery">
<div class="poster">
<img src="img/poster-1.jpg" fetchpriority="high">
</div>
<div class="thumbnails">
<img src="img/thumbnail-2.jpg" fetchpriority="low">
<img src="img/thumbnail-3.jpg" fetchpriority="low">
<img src="img/thumbnail-4.jpg" fetchpriority="low">
</div>
</div>
fetchpriority 属性在用于页面的 LCP 图片时尤其有效。通过使用此属性提高 LCP 图片的优先级,您可以相对轻松地改善网页的 LCP。
默认情况下,系统会以较低的优先级提取图片。完成布局后,如果发现图片位于初始视口内,则相应优先级将提升为 High 优先级。在上述 HTML 代码段中,fetchpriority 会立即指示浏览器以高优先级下载较大的 LCP 图片,同时以较低优先级下载不太重要的缩略图。
现代浏览器会分两个阶段加载资源。第一阶段预留供关键资源使用,并会在所有阻塞脚本下载并执行完毕后结束。在此阶段,低优先级资源可能会出现延迟,无法下载。通过使用 fetchpriority=“high”,您可以提高资源的优先级,使浏览器能够在第一阶段下载该资源。
五、图片效果
图片通常是网络上最繁重且最普遍的资源。因此,优化图片可以显著提升您网站的性能。 在大多数情况下,优化图片意味着通过发送更少的字节来缩短网络时间,但您也可以通过提供适合用户设备大小的图片来优化发送给用户的字节数。
您可以使用 <img>
或<picture>
元素或 CSS background-image
属性将图片添加到网页中。在少数情况下,可以将 <svg>
标记直接插入网页的 HTML 中,从而将 SVG 图片直接内嵌到网页中。这样,就可以在 JavaScript 中直接访问 SVG 的子元素。鉴于此模块注重图片性能,因此不涵盖 SVG 图片的这种用例。如果您在网站上使用 SVG 图片,请注意 SVG 格式是基于文本的,因此缩减和压缩等技术适用。您还可以使用基于 Node.js 的 SVG 优化工具 svgo 进行有损优化。
SVG优化器(SVGO)是一个流行的开源工具,用于压缩SVG文件。它的工作原理是安全地删除编辑元数据、注释、隐藏元素,以及默认或非最佳值1。这样做可以减少SVG文件的大小,从而提高网页加载速度,优化用户体验。
SVGO的优化原理主要基于以下几点2:
删除SVG文件中的无用信息,如编辑器源信息、注释等。 对SVG文件进行精简处理,如删除空属性、隐藏元素、无用的stroke和fill属性等。
对SVG路径数据进行优化,如将路径数据转换为相对路径和绝对路径中较短的那一个,过滤无用的分隔符,智能四舍五入等。
因此,SVGO能够有效地优化SVG文件,使其更适合在网络环境中使用。
1.图像大小
使用图片资源时,您可以执行的第一项优化是以正确的大小显示图片。在本例中,“大小”一词是指图片的尺寸。在不考虑其他变量的情况下,在 500 x 500 像素的容器中显示的图片的最佳尺寸为 500 x 500 像素。例如,使用 1000 像素的方形图片意味着图片将根据需要翻倍。
不过,选择合适的图片大小涉及许多变量,因此在各种情况下选择适当的图片大小的任务会变得非常复杂。iPhone 4 在 2010 年发布时,其屏幕分辨率 (640x960) 是 iPhone 3 (320x480) 的两倍。不过,iPhone 4 的屏幕的物理尺寸与 iPhone 3 大致相同。
以较高的分辨率显示所有内容都会使文本和图片明显缩小,确切地说是先前大小的一半。而是将 1 个像素变成了 2 个设备像素。这称为“设备像素比 (DPR)”。iPhone 4 以及随后发布的许多 iPhone 型号的 DPR 为 2。
回顾前面的示例,如果设备 DPR 为 2 并且图像显示在 500x500 像素的容器中,那么现在的 1000 像素的方形图像(称为“固有尺寸”)是最佳尺寸。同样,如果设备的 DPR 为 3,则最佳尺寸是 1500 像素的方形图片。
在大多数情况下,人眼无法从 3 的 DPR 中受益,并且您可以使用小于最佳尺寸的图片,并且大多数用户不会察觉到图片质量下降。
2.srcset
元素支持 srcset 属性,该属性可让您指定浏览器可能会使用的可能图片来源列表。指定的每个图片来源都必须包含图片网址,以及宽度或像素密度描述符。
<img
alt="An image"
width="500"
height="500"
src="/image-500.jpg"
srcset="/image-500.jpg 1x, /image-1000.jpg 2x, /image-1500.jpg 3x"
>
上述 HTML 代码段使用像素密度描述符来提示浏览器在 DPR 为 1 的设备上使用 image-500.png,在 DPR 为 2 的设备上使用 image-1000.jpg,在 DPR 为 3 的设备上使用 image-1500.jpg。
虽然给定页面选择最佳图片时,屏幕的 DPR 并不是唯一的考虑因素。页面的布局也是一个需要考虑的因素。
3.sizes
上述解决方案仅在您在所有视口上以相同的 CSS 像素大小显示图片时才有效。在许多情况下,页面的布局以及容器的大小会根据用户的设备而发生变化。
通过 sizes 属性,您可以指定一组来源大小,其中每个来源大小都由媒体条件和值组成。sizes 属性用于描述图片的预期显示尺寸(以 CSS 像素为单位)。与 srcset 宽度描述符结合使用时,浏览器可以选择最适合用户设备的图片来源。
<img
alt="An image"
width="500"
height="500"
src="/image-500.jpg"
srcset="/image-500.jpg 500w, /image-1000.jpg 1000w, /image-1500.jpg 1500w"
sizes="(min-width: 768px) 500px, 100vw"
>
在上述 HTML 代码段中,srcset 属性指定了浏览器可以选择的候选图片列表(以英文逗号分隔)。列表中的每个候选字符都包含图片的网址,后跟表示图片固有宽度的语法。图片的固有大小是其尺寸。例如,1000w 描述符表示图片的固有宽度为 1000 像素宽。
根据此信息,浏览器会评估 sizes 属性中的媒体条件;在本例中,浏览器会指示:如果设备的视口宽度超过 768 像素,则图片以 500 像素宽度显示。在较小的设备上,图片以 100vw 或全视口宽度显示。
然后,浏览器可以将这些信息与 srcset 图片来源列表相结合,以找出最佳图片。例如,如果用户使用的移动设备屏幕宽度为 320 像素,DPR 为 3,图片就会显示为 320 CSS pixels x 3 DPR = 960 device pixels。在本例中,尺寸最接近的图片是 image-1000.jpg,其固有宽度为 1000 像素 (1000w)。
如果没有 sizes 属性,srcset 宽度描述符将不起作用。同样,如果您省略 srcset 宽度描述符,sizes 属性也不会执行任何操作。
4.文件格式
浏览器支持多种不同的图片文件格式。WebP 和 AVIF 等现代图片格式可提供优于 PNG 或 JPEG 的压缩效果,因而图片文件大小会随之减小,从而缩短下载时间。通过以现代格式提供图片,您可以缩短资源的加载时间,但这可能会导致 Largest Contentful Paint (LCP) 变低。
WebP 是一种受到广泛支持的格式,适用于所有现代浏览器。WebP 通常比 JPEG、PNG 或 GIF 的压缩效果更好,同时提供有损和无损压缩。即使在使用有损压缩时,WebP 也支持 Alpha 通道透明度,而 JPEG 编解码器不提供此功能。
AVIF 是一种较新的图片格式,虽然其支持不如 WebP 那么广泛,但它在各种浏览器之间得到了相当得体的支持。AVIF 同时支持有损和无损压缩,并且在某些情况下,与 JPEG 相比,测试的节省幅度超过了 50%。AVIF 还提供广色域 (WCG) 和高动态范围 (HDR) 功能。
5.压缩
对于图片来说,有两种压缩类型:
- 有损压缩
- 无损压缩
有损压缩的工作原理是:通过量化降低图片准确性,并且可能会使用色度子采样舍弃其他颜色信息。有损压缩对噪声和颜色丰富的高密度图像(通常是内容相似的照片或图像)最为有效。这是因为有损压缩产生的伪影在此类详细图像中不太可能被发现。但是,对于包含清晰边缘(例如线条艺术、同样纯粹的细节或文本)的图像,有损压缩的效果可能不太理想。有损压缩适用于 JPEG、WebP 和 AVIF 图片。
无损压缩通过压缩图片来减小文件大小,且不会丢失数据。无损压缩根据与相邻像素的差异来描述像素。无损压缩适用于 GIF、PNG、WebP 和 AVIF 图片格式。
您可以使用 Squoosh、ImageOptim 或图片优化服务压缩图片。在压缩时,没有一种适用于所有情况的通用设置。建议的方法是尝试使用不同的压缩级别,直到在图片质量和文件大小之间找到较好的折衷方案。某些高级图片优化服务可以自动为您执行这些操作,但并非所有用户都能从经济上可行。
6.<picture>
元素
借助 <picture>
元素,您可以更灵活地指定多个候选图片:
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img
alt="An image"
width="500"
height="500"
src="/image.jpg"
>
</picture>
当您在 <picture>
元素中使用 <source>
元素时,您可以添加对 AVIF 和 WebP 图片的支持,但如果浏览器不支持现代格式,则会回退为使用更兼容的旧图片格式。使用此方法时,浏览器会选取指定的第一个匹配 <source>
元素。如果能以该格式渲染图像,它就会使用该图像。否则,浏览器会移至下一个指定的 <source>
元素。在上述 HTML 代码段中,AVIF 格式优先于 WebP 格式,如果 AVIF 和 WebP 均不受支持,则会回退到 JPEG 格式。
<picture>
元素需要嵌套在其中的 <img>
元素。alt、width 和 height 属性在 <img>
中定义,并且无论选择哪个 <source>
都可以使用。
<source>
元素还支持 media、srcset 和 sizes 属性。与前面的 <img>
示例类似,这些标记会向浏览器指明要在不同视口中选择哪个图片。
<picture>
<source
media="(min-resolution: 1.5x)"
srcset="/image-1000.jpg 1000w, /image-1500.jpg 1500w"
sizes="(min-width: 768px) 500px, 100vw"
>
<img
alt="An image"
width="500"
height="500"
src="/image-500.jpg"
>
</picture>
media 属性接受媒体条件。在前面的示例中,设备的 DPR 用作媒体条件。DPR 大于或等于 1.5 的任何设备都将使用第一个 元素。 元素告知浏览器,在视口宽度大于 768 像素的设备上,所选候选图片会以 500 像素宽显示。在较小的设备上,这会占据整个视口宽度。通过结合使用 media 和 srcset 属性,您可以更精细地控制要使用的图片。
下表对此进行了说明,其中评估了几个视口宽度和设备像素比:
DPR 为 1 的设备会下载 image-500.jpg 映像(包括大多数桌面设备用户),他们能够以 500 像素宽的外向尺寸查看该图片。另一方面,DPR 为 3 的移动设备用户下载的图片可能更大image-1500.jpg,即 DPR 为 3 的桌面设备上使用的图片相同。
<picture>
<source
media="(min-width: 560px) and (min-resolution: 1.5x)"
srcset="/image-1000.jpg 1000w, /image-1500.jpg 1500w"
sizes="(min-width: 768px) 500px, 100vw"
>
<source
media="(max-width: 560px) and (min-resolution: 1.5x)"
srcset="/image-1000-sm.jpg 1000w, /image-1500-sm.jpg 1500w"
sizes="(min-width: 768px) 500px, 100vw"
>
<img
alt="An image"
width="500"
height="500"
src="/image-500.jpg"
>
</picture>
在此示例中, 元素经过调整以包含额外的 元素,以便为具有高 DPR 的宽幅设备使用不同的图片:
执行这个额外的查询后,您可以看到 image-1000-sm.jpg 和 image-1500-sm.jpg 会显示在小视口上。这些额外的信息可让您进一步压缩图片,因为在这种大小和密度下,压缩伪影不太明显,同时还不会影响桌面设备上的图片质量。
或者,通过调整 srcset 和 media 属性,可以避免在小视口上提供大图片:
<picture>
<source
media="(min-width: 560px)"
srcset="/image-500.jpg, /image-1000.jpg 2x, /image-1500.jpg 3x"
>
<source
media="(max-width: 560px)"
srcset="/image-500.jpg 1x, /image-1000.jpg 2x"
>
<img
alt="An image"
width="500"
height="500"
src="/image-500.jpg"
>
</picture>
在上述 HTML 代码段中,移除了宽度描述符,改为使用设备像素比描述符。在移动设备上提供的图片仅限于 /image-500.jpg 或 /image-1000.jpg,即使在 DPR 为 3 的设备上也是如此。
7.如何管理复杂性
开发者往往倾向于使用尽可能多的变体来实现最合适的方案,但每增加一个图片变体都会产生代价,并会降低浏览器缓存的使用效率。只有一个变体,每个用户都会收到相同的图片,因此可以非常有效地缓存该图片。
另一方面,如果有许多变体,则每个变体都需要另一个缓存条目。如果变体的缓存条目已过期,并且需要重新从源服务器提取图片,则服务器费用可能会增加,并且可能会降低性能。
a.根据 Accept 请求标头提供图片
Accept HTTP 请求标头会告知服务器,用户的浏览器可以识别哪些内容类型。您的服务器可以使用此信息来提供最佳图片格式,而无需向 HTML 响应添加额外的字节。
if (request.headers.accept) {
if (request.headers.accept.includes('image/avif')) {
return reply.from('image.avif');
} else if (request.headers.accept.includes('image/webp')) {
return reply.from('image.webp');
}
}
return reply.from('image.jpg');
Accept 请求标头通常仅传递 HTML 资源请求支持的图片类型。如果您选择根据此标头的值传送资源,请务必在额外的 Vary 响应标头中指定该标头,以便共享缓存(例如内容分发网络 [CDN])可以针对同一网址考虑不同的响应。
上述 HTML 代码段是代码的简化版本,您可将其添加到服务器的 JavaScript 后端,以选择并提供最佳的图片格式。如果请求 Accept 标头包含 image/avif,则系统会提供 AVIF 图片。否则,如果 Accept 标头包含 image/webp,则传送 WebP 图片。如果这两个条件均不满足,系统会投放 JPEG 图片。
在几乎所有类型的网络服务器中,您都可以根据 Accept 请求标头的内容修改响应。例如,您可以使用 mod_rewrite 根据 Accept 标头重写 Apache 服务器上的图片请求。
这与图片内容分发网络 (CDN) 中的行为不同。图片 CDN 非常适合根据用户的设备和浏览器优化图片并发送最佳格式。
关键是要找到平衡点,生成合理数量的候选图片,以及衡量对用户体验的影响。不同的图片可能会给出不同的结果,而对每张图片采取的优化措施取决于其在页面中的大小以及用户使用的设备。例如,在电子商务商品详情页面上,全宽主打图片可能需要比缩略图更多变体。
8.延迟加载
您可以使用 loading 属性指示浏览器在图片显示在视口中时延迟加载图片。lazy 的属性值会指示浏览器在图片进入(或靠近)视口之前不要下载图片。这可以节省带宽,使浏览器能够优先处理渲染视口中已有的关键内容所需的资源。
9.decoding
decoding 属性会告知浏览器应如何解码图片。async 值可告知浏览器图片可异步解码,从而可能缩短渲染其他内容的时间。值 sync 会告知浏览器,相应图片应与其他内容同时呈现。auto 的默认值允许浏览器决定哪种版本最适合用户。
decoding 属性的影响可能仅在非常大的高分辨率图片上才会显现出来,因为此类图片需要较长的解码时间。通过以编程方式将图片插入 DOM 时,您还可以对采用 JavaScript 的 HTMLImageElement 实例的 decode 方法使用该方法。
六、视频表现
1.视频源文件
处理媒体文件时,您在操作系统中识别的文件(.mp4、.webm 等)称为容器。一个容器包含一个或多个数据流。在大多数情况下,这是指视频和音频流。
您可以使用编解码器压缩每个流。例如,video.webm 可以是包含使用 VP9 压缩的视频流和使用 Vorbis 压缩的音频流的 WebM 容器。
了解容器和编解码器之间的区别非常有用,因为这可以帮助您使用大幅减少的带宽压缩媒体文件,从而缩短总体网页加载时间,还有助于改进网页的 Largest Contentful Paint (LCP)。LCP 是一项以用户为中心的指标,也是三个核心网页指标之一。
压缩视频文件的一种方式是使用 FFmpeg:
ffmpeg -i input.mov output.webm
上述命令虽然与使用 FFmpeg 时的体验一样基本,但会接受 input.mov 文件,并使用默认 FFmpeg 选项输出 output.webm 文件。上述命令会输出一个可在所有现代浏览器中使用的较小视频文件。调整视频或 FFmpeg 提供的音频选项可以帮助您进一步缩减视频的文件大小。例如,如果您使用 <video>
元素替换 GIF,则应移除音轨:
ffmpeg -i input.mov -an output.webm
-an 标志用于从输出文件中移除所有音频流。如果您的视频用例不需要音频(例如,视频被用于替换动画 GIF),那么这项优化是一项重要的优化措施。因为移除音频流可减小视频文件的大小,即使源视频文件中已经存在的音频流处于静默状态,也是如此。
a.多种格式
处理视频文件时,对于不支持所有现代格式的浏览器,指定多种格式可作为后备方案。
<video>
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
</video>
2.poster 属性
视频的海报图片是使用 <video>
元素上的 poster 属性添加的,该属性可在开始播放前向用户提示视频内容:
<video poster="poster.jpg">
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
</video>
没有 poster 图片的 <video>
元素曾经不是 LCP 候选版本。 此问题已解决,视频文件的第一帧(一旦绘制)将被视为 LCP 候选。如果您的网站会大量使用视频文件,请务必在视频无法自动播放时使用 poster 属性,或确保优化视频 LCP 候选视频,以便在未使用 poster 属性时尽快显示这些视频。
3.自动播放
根据 HTTP Archive 的数据,网上 20% 的视频包含 autoplay 属性。autoplay 在必须立即播放视频时使用,例如用作视频背景或替代动画 GIF。
GIF 动画可能会非常大,特别是当它们包含许多包含复杂细节的帧时。动画 GIF 消耗数 MB 的数据量并不罕见,这会占用大量带宽,并更好地将资源用于更关键的资源。您通常应该避免使用动画图片格式,因为等效的 <video>
对此类媒体而言效率更高。
如果您的网站要求自动播放视频,您可以直接在 <video>
元素上使用 autoplay
属性:
<!-- This will automatically play a video, but
it will loop continuously and be muted: -->
<video autoplay muted loop playsinline>
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
</video>
指定了 autoplay 属性的 <video>
元素会立即开始下载,即使它们位于初始视口之外也是如此。
通过将 poster 属性与 Intersection Observer API 结合使用,您可以将网页配置为仅下载位于视口内的视频。poster 图片可能是低质量图片占位符,例如视频的第一帧。视频显示在视口中后,浏览器便开始下载视频。这可以缩短初始视口内内容的加载时间。但缺点是,如果对 autoplay 使用 poster 图片,您的用户收到的图片只会短暂显示,直到视频加载并开始播放。
4.用户启动的播放
通常,当 HTML 解析器发现 <video>
元素后,浏览器会立即开始下载视频文件。如果您在用户开始播放的位置有 <video>
元素,那么在用户与其互动之前,您可能不希望视频开始下载。
您可以使用 <video>
元素的 preload 属性影响下载的视频资源内容:
设置 preload=“none” 可告知浏览器不应预加载任何视频内容。
设置 preload=“metadata” 仅提取视频元数据,例如视频时长,可能还包括其他粗略信息。
如果您加载的视频需要用户启动播放,那么设置 preload=“none” 可能是最可取的情况。
在这种情况下,您可以通过添加 poster 图片来改善用户体验。这可以为用户提供有关视频内容的一些背景信息。此外,如果海报图片是您的 LCP 元素,您可以使用 提示和 fetchpriority=“high” 来提高 poster 图片的优先级:
<link rel="preload" as="image" href="poster.jpg" fetchpriority="high">
如果视频不是视口中的最大元素,预加载 poster 图片可能会因为带宽争用而延迟 LCP 结果,此时可用的带宽会分配给其他更关键的资源。
5.嵌入
鉴于高效优化和提供视频内容的过程非常复杂,因此有必要将这个问题分流给 YouTube 或 Vimeo 等专用视频服务。此类服务可为您优化视频分发,但嵌入来自第三方服务的视频可能会对性能产生自行影响,因为嵌入式视频播放器通常可提供大量额外资源(例如 JavaScript)。
鉴于这种现实,第三方视频嵌入可能会显著影响网页性能。根据 HTTP Archive 资料,YouTube 嵌入代码会在主线程上阻塞主线程 1.7 秒以上中位数网站。长时间阻塞主线程是一种严重的用户体验问题,可能会影响页面的 Interaction to Next Paint (INP)。不过,您可以在初始网页加载期间不立即加载嵌入,而是为嵌入创建占位符,并在用户与嵌入互动时将该占位符替换为实际的视频嵌入,从而做出妥协。
七、优化网页字体
网页字体在加载和呈现时都会影响网页性能。较大的字体文件可能需要一段时间才能下载,并且会对 First Contentful Paint (FCP) 产生负面影响,而不正确的 font-display 值可能会导致导致网页的不良布局偏移,从而造成页面的 Cumulative Layout Shift (CLS)。
1.发现广告系列
页面的网页字体是使用 @font-face 声明在样式表中定义的:
@font-face {
font-family: "Open Sans";
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
}
上述代码段定义了一个名为 “Open Sans” 的 font-family,并告知浏览器在哪里可以找到相应的网页字体资源。为了节省带宽,在确定当前页面的布局需要网页字体之前,浏览器不会下载网页字体。
h1 {
font-family: "Open Sans";
}
浏览器会在解析页面 HTML 中的 <h1>
元素时下载 “Open Sans” 字体文件。
a.preload
如果 @font-face 声明是在外部样式表中定义的,浏览器只能在下载样式表后开始下载。这会使网页字体的发现时间较晚,但也有办法帮助浏览器更快地发现网页字体。
您可以使用 preload 指令发起针对网页字体资源的提前请求。preload 指令可在网页加载早期阶段发现网页字体,浏览器会立即开始下载这些字体,无需等待样式表完成下载和解析。preload 指令不会等到网页上需要相应字体时再执行。
<!-- The `crossorigin` attribute is required for fonts—even
self-hosted ones, as fonts are considered CORS resources. -->
<link rel="preload" as="font" href="/fonts/OpenSans-Regular-webfont.woff2" crossorigin>
应谨慎使用 preload 指令。过度使用 preload 指令可能会转移其他关键资源的带宽。如果使用过度,preload 指令可能会下载当前网页不需要的字体
此外,请务必注意,字体属于 CORS 资源。因此,在预加载字体时,即使它们是自托管的,您必须指定 crossorigin 属性
b.内嵌 @font-face 声明
通过使用 <style>
元素,您可以在 HTML 的 <head>
中内嵌会阻止渲染的 CSS(包括 @font-face 声明),从而在网页加载期间提早发现字体。在这种情况下,浏览器会在网页加载的早期发现网页字体,因为它不需要等待外部样式表下载。
只有在所有阻塞渲染的资源加载完毕后,浏览器才会开始下载字体文件。这意味着,如果您已内联 @font-face 声明,但其余 CSS 在外部样式表中,那么浏览器仍然需要等待外部样式表下载完毕。
与使用 preload 提示相比,内嵌 @font-face 声明的优势在于,因为浏览器只会下载呈现当前网页所需的字体。这样可以消除下载未使用的字体的风险。
不建议将字体文件本身内嵌到 CSS(或任何其他资源)中,因为这样做所需的 base64 编码方案会产生较大的载荷,而内嵌该文件可能会由于延迟预加载扫描程序而延迟发现其他关键资源。
2.下载
发现网页字体并确保当前页面布局需要这些字体后,浏览器即可下载这些字体。网页字体的数量、编码和文件大小可能会显著影响浏览器下载和呈现网页字体的速度。
a.自行托管网页字体
网页字体可以通过第三方服务(例如 Google Fonts)提供,也可以在您的源站上自行托管。使用第三方服务时,您的网页需要先与提供商的网域建立连接,然后才能开始下载所需的网页字体。这可能会导致网页字体的发现和后续下载延迟。
使用 preconnect 资源提示可以减少此开销。借助 preconnect,您可以告知浏览器比浏览器通常尽快打开跨源连接:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
上述 HTML 代码段会提示浏览器与 fonts.googleapis.com 建立连接并与 fonts.gstatic.com 建立 CORS 连接。某些字体提供程序(如 Google Fonts)会提供来自不同来源的 CSS 和字体资源。
您可以通过自行托管网页字体来免除第三方连接的需求。在大多数情况下,自托管网页字体比从跨源下载字体更快。如果您打算自行托管网页字体,请检查您的网站是否使用了内容分发网络 (CDN)、HTTP/2 或 HTTP/3,并为网站所需的网页字体设置正确的缓存标头。
b.仅使用 WOFF2 和 WOFF2
WOFF2 可提供广泛的浏览器支持和最佳压缩效果,比 WOFF 高出 30%。文件大小缩减,下载速度更快。WOFF2 格式往往是现代浏览器完全兼容的唯一格式。
只有在需要支持旧版浏览器时,您才可能需要使用其他格式(例如 WOFF、EOT 和 TTF)。 如果您不需要支持旧版浏览器,就没有理由依赖于 WOFF2 以外的网页字体格式。
c.设置网页字体子集
网页字体通常包含各种不同的字形,用于表示不同语言中使用的各种字符。如果您的网页只以一种语言提供内容,或使用单一字母表,那么您可以通过子集内嵌来减小网页字体的大小。这通常通过指定数字或 Unicode 码位范围来实现。
子集是原始网页字体文件中包含的一组简化的字形。例如,您的网页可能会提供部分拉丁字符,而不是提供所有字形。根据所需的子集,移除字形可以显著减小字体文件的大小。
某些网页字体提供程序(如 Google Fonts)会使用查询字符串参数自动提供子集。例如,https://fonts.googleapis.com/css?family=Roboto&subset=latin 网址提供采用 Roboto 网页字体且仅使用拉丁字母的样式表。
如果您决定自行托管网页字体,则下一步是使用字形符或子字体等工具自行生成并托管这些子集。
但是,如果您没有足够的能力自行托管您自己的网页字体,可以对 Google Fonts 提供的网页字体进行子集内嵌,只需指定仅包含网站所需的 Unicode 代码点的额外 text 查询字符串参数即可。例如,如果网站上的显示网页字体仅需要少量字符,才能用于表达“Welcome”一词,您可以通过以下网址,使用 Google Fonts 请求该子集:https://fonts.googleapis.com/css?family=Monoton&text=Welcome。这样可大幅减少网站上单个字体所需的网页字体数据量(如果此类极端子集内嵌对您的网站有帮助的话)。
3.字体呈现
浏览器发现并下载网页字体后,即可渲染该字体。默认情况下,在下载使用网页字体的任何文本之前,浏览器会阻止其渲染。使用 font-display CSS 属性,您可以调整浏览器的文本渲染行为,并配置在网页字体完全加载之前应该显示(或不显示)哪些文本。
a.block
font-display 的默认值为 block。使用 block,浏览器会阻止渲染使用指定网页字体的任何文本。不同浏览器的行为会略有不同。Chromium 和 Firefox 最多会阻塞渲染 3 秒钟,然后再使用回退机制。Safari 会无限期屏蔽,直到网页字体加载完毕。
b.swap
swap 是使用最广泛的 font-display 值。swap 不会阻止渲染,它会在交换指定的网页字体之前立即以回退方式显示文本。这样,您就可以立即显示内容,而无需等待网页字体下载完成。
不过,swap 的缺点是,如果后备网页字体和 CSS 中指定的网页字体在行高、字距调整和其他字体指标方面存在很大差异,则会导致布局偏移。如果您不小心使用 preload 提示尽快加载网页字体资源,或者不考虑其他 font-display 值,这可能会影响网站的 CLS。
c.fallback
font-display 的 fallback 值在 block 和 swap 之间是一种折衷的方案。与 swap 不同,浏览器会阻止字体渲染,但只在很短的时间内交换回退文本。不过,与 block 不同的是,阻塞期极短。
在速度较快的网络上,使用 fallback 值效果最好。在此类网络上,如果网页字体可以快速下载,网页字体就是网页首次渲染时立即使用的字体。不过,如果网络速度较慢,系统会在屏蔽期过后,先显示后备文本,然后在网页字体到达时交换出后备文本。
d.optional
optional 是最严格的 font-display 值,仅当在 100 毫秒内下载时才会使用网页字体资源。如果网页字体的加载用时超过该时间,则系统不会在网页上使用该字体,而浏览器会使用后备字体进行当前导航,而网页字体会在后台下载并放入浏览器缓存中。
因此,后续页面导航可以立即使用网页字体,因为它已下载。font-display: optional 可避免 swap 中出现的布局偏移,但如果网页字体在初始网页导航中呈现的时间过晚,某些用户便会看不到它。
八、代码拆分 JavaScript
加载大型 JavaScript 资源会显著影响网页速度。将 JavaScript 拆分为多个较小的块,并仅下载网页在启动期间正常运行所需的资源,可以极大地提高网页的加载响应能力,进而改善网页的 Interaction to Next Paint (INP)。
在网页下载、解析和编译大型 JavaScript 文件时,可能会在一段时间内没有响应。页面元素是可见的,因为它们是页面的初始 HTML 的一部分,并由 CSS 设置样式。但是,为这些互动元素(以及由网页加载的其他脚本)提供支持所需的 JavaScript 可能需要解析和执行 JavaScript,才能正常运行。其结果是,用户可能会感觉互动受到严重延迟,甚至完全中断。
这通常是因为主线程处于阻塞状态,因为 JavaScript 在主线程上解析和编译。如果此过程花费的时间过长,交互式页面元素可能无法足够快地响应用户输入。解决方法之一是仅加载网页正常运行所需的 JavaScript,同时通过代码拆分技术推迟其他 JavaScript 稍后加载。本单元将重点介绍这两种技术中的后一种。
1.通过代码拆分在启动期间减少 JavaScript 解析和执行
当 JavaScript 执行用时超过 2 秒时,Lighthouse 会发出警告;如果执行时间超过 3.5 秒,则会失败。过多的 JavaScript 解析和执行都是页面生命周期中的潜在问题,因为如果用户与页面交互的时间与负责处理和执行 JavaScript 的主线程任务运行时一致,则可能会导致交互的输入延迟增加。
除此之外,在初始网页加载期间,过多的 JavaScript 执行和解析会特别带来问题,因为这是网页生命周期中用户很可能会与网页互动的环节。事实上,Total Blocking Time (TBT) 是一个加载响应性指标,与 INP 高度相关,这表明用户在初始网页加载期间更有可能尝试进行互动。
Lighthouse 审核报告可以报告执行页面请求的每个 JavaScript 文件所花费的时间,这可以帮助您精确确定哪些脚本可能适合进行代码拆分。然后,您可以使用 Chrome 开发者工具中的覆盖率工具来进一步确定在网页加载过程中未使用网页的哪些 JavaScript 部分。
代码拆分是一种可以减少页面的初始 JavaScript 载荷的实用方法。它允许您将 JavaScript 软件包拆分为两部分:
网页加载时所需的 JavaScript 无法在任何其他时间加载。
可稍后加载的其余 JavaScript,最常见的是用户与网页上的给定互动元素互动时。
您可以使用动态 import() 语法完成代码拆分。与在启动期间请求给定 JavaScript 资源的
动态 import() 是一种类似于函数的表达式,可让您动态加载 JavaScript 模块。 它是一个异步操作,可用于导入模块来响应互动或要加载其他模块的其他任何条件。动态 import() 与静态 import 语句不同,后者会立即导入模块,并要求父模块及其所有依赖项都经过解析和执行,然后才能运行。
document.querySelectorAll('#myForm input').addEventListener('focus', async () => {
// Get the form validation named export from the module through destructuring:
const { validateForm } = await import('/validate-form.js');
// Validate the form:
validateForm();
}, { once: true });
在上述 JavaScript 代码段中,只有在用户聚焦到表单的任意 字段时,系统才会下载、解析和执行 validate-form.js 模块。focuses在这种情况下,负责驱动表单验证逻辑的 JavaScript 资源仅在最有可能实际使用时才与页面相关。
可以将 webpack、Parcel、Rollup 和 esbuild 等 JavaScript 捆绑器配置为每当 JavaScript 软件包在源代码中遇到动态 import() 调用时,将它拆分成较小的区块。大多数工具都会自动执行此操作,但 esbuild 需要您选择启用这项优化功能。
React 通过其 React.lazy 语法抽象化动态 import()。在后台,它仍然依赖于动态 import(),并且模块打包器仍负责将 JavaScript 拆分为单独的区块。
2.webpack
webpack 附带一个名为 SplitChunksPlugin 的插件,可让您配置捆绑器拆分 JavaScript 文件的方式。webpack 同时识别动态 import() 和静态 import 语句。您可以通过在其配置中指定 chunks 选项来修改 SplitChunksPlugin 的行为:
chunks: async 是默认值,表示动态 import() 调用。
chunks: initial 是指静态 import 调用。
chunks: all 涵盖动态 import() 和静态导入,可让您在 async 和 initial 导入之间共享分块。
默认情况下,每当 webpack 遇到动态 import() 语句时,都会为该模块创建单独的分块:
/* main.js */
// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';
myFunction('Hello world!');
// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
// Assumes top-level await is available. More info:
// https://v8.dev/features/top-level-await
await import('/form-validation.js');
}
上述代码段的默认 webpack 配置会产生两个单独的分块:
main.js 分块(webpack 归类为 initial 分块),其中包含 main.js 和 ./my-function.js 模块。
async 区块,其中仅包含 form-validation.js(如果已配置,则资源名称中包含文件哈希)。只有当 condition 为 truthy 时,才会下载此分块。
此配置可让您推迟加载 form-validation.js 分块,直到实际需要它为止。这可以通过缩短初始网页加载期间的脚本评估时间来提高加载响应速度。当满足指定条件时,系统会下载动态导入的模块,从而下载和评估 form-validation.js 分块的脚本。例如,您可以仅为特定浏览器下载 polyfill,或者如前一示例所示,导入的模块是用户互动所必需的。
另一方面,更改 SplitChunksPlugin 配置以指定 chunks: initial 可确保仅在初始区块上拆分代码。这些是静态导入的或列在 webpack 的 entry 属性中的区块。就上面的示例而言,生成的分块将是单个脚本文件中 form-validation.js 和 main.js 的组合,从而导致初始网页加载性能可能更差。
SplitChunksPlugin 的选项也可以配置为将较大的脚本拆分为多个较小的脚本。例如,使用 maxSize 选项指示 webpack 将区块拆分为单独的文件,前提是区块超出 maxSize 指定的内容。将大型脚本文件分为较小的文件可以提高负载响应速度,因为在某些情况下,CPU 密集型脚本评估工作分为较小的任务,这些较小的任务不太可能长时间阻塞主线程。
此外,生成较大的 JavaScript 文件还意味着脚本更有可能遇到缓存失效问题。例如,如果您发布了一个同时包含框架和第一方应用代码的超大型脚本,那么如果仅更新了框架,而捆绑的资源中没有任何其他内容,则整个软件包可能会失效。
另一方面,脚本文件越小,回访者就越有可能从缓存中检索资源,从而在重复访问时加快网页加载速度。不过,与较大的文件相比,较小的文件从压缩中受益更少,并且可能会在没有准备好的浏览器缓存的情况下增加网页加载时的网络往返时间。必须注意在缓存效率、压缩效果和脚本评估时间之间取得平衡。
如果您通过在应用的 webpack 配置中指定 splitChunks: false 来停用 SplitChunksPlugin,则 ./my-function.js 会捆绑在 main.js 和 form-validation.js 中。
九、延迟加载图片和 <iframe>
元素
与其他类型的资源相比,图片和 <iframe>
元素消耗的带宽通常更高。对于 <iframe>
元素,加载和呈现其中的页面可能需要相当多的额外处理时间。
在延迟加载图片的情况下,延迟加载初始视口以外的图片可能有助于减少初始视口内更关键资源的带宽争用。这样可以在网络连接状况不佳时改善网页的 Largest Contentful Paint (LCP),而重新分配的带宽有助于提升 LCP 候选网络的加载和渲染速度。
就 <iframe>
元素而言,可以在启动期间通过延迟加载来改进网页的 Interaction to Next Paint (INP)。这是因为 <iframe>
是完全独立的 HTML 文档,拥有自己的子资源。虽然 <iframe>
元素可以在单独的进程中运行,但与其他线程共享进程的情况并不少见,这可能会造成页面对用户输入的响应速度变差。
因此,延迟加载屏幕外图片和 <iframe>
元素是一种值得持续关注的技术,只需付出很少的努力,就能获得相对良好的性能回报。
1.使用 loading 属性延迟加载图片
可以将 loading 属性添加到 <img>
元素中,以告知浏览器应如何加载它们:
“eager” 会通知浏览器应立即加载图片,即使图片位于初始视口之外也是如此。这也是 loading 属性的默认值。
“lazy” 会延迟加载图片,直到图片在与可见视口的距离内。此距离因浏览器而异,但通常设置得足够大,以便在用户滚动到图片时加载图片。
如前所述,使用 loading=“lazy” 属性时,浏览器判定需要使用相应图片的视口距离因浏览器而异。涉及的因素可能包括有效连接类型以及映像类型。
另外值得注意的是,如果您使用的是 <picture>
元素,则 loading 属性仍应应用于其子级 <img>
元素,而不是 <picture>
元素本身。这是因为 <picture>
元素是一个容器,其中包含指向不同候选图片的额外 <source>
元素,并且浏览器选择的候选图片直接应用于其子级 <img>
元素。
a.请勿延迟加载初始视口中的图片
您只能向位于初始视口之外的 <img>
元素添加 loading=“lazy” 属性。不过,在呈现网页之前了解元素在视口中的确切位置可能会很复杂。必须考虑不同的视口尺寸、宽高比和设备。
例如,桌面设备视口可能与手机上的视口大相径庭,因为前者会呈现更多的垂直空间,而这些空间或许能够适应初始视口中的图片,而这些图片不会显示在尺寸较小的设备的初始视口中。纵向模式使用的平板电脑也会显示大量的垂直空间,甚至可能比某些桌面设备更大。
不过,在某些情况下,您应该避免应用 loading=“lazy”,这非常明显。例如,如果是主打图片,或是在任何设备上,<img>
元素可能展示在首屏或布局顶部,您都应该从 <img>
元素中省略 loading=“lazy” 属性。这对于可能是 LCP 候选项的图片更为重要。
延迟加载的图片需要等待浏览器完成布局,以便了解图片的最终位置是否位于视口内。这意味着,如果可见视口中的 <img>
元素具有 loading=“lazy” 属性,则只有在所有 CSS 已下载、解析并应用到页面之后才会请求该属性,而不是在在原始标记中被预加载扫描程序发现后立即提取。
由于所有主流浏览器都支持<img>
元素上的 loading 属性,因此无需使用 JavaScript 延迟加载图片,因为向网页添加额外的 JavaScript 来提供浏览器已提供的功能会影响网页性能的其他方面(例如 INP)。
loading 属性不会影响映像的网络优先级。如需调整网络优先级,您可以使用 Fetch Priority API。请注意,可见视口中带有 fetchpriority=“high” 和 loading=“lazy” 的图片仍会等待所有 CSS 下载并解析。
2.延迟加载 <iframe>
元素
通过延迟加载 <iframe>
元素,使其在视口中可见,可以保存大量数据,并改进加载顶级页面所需的关键资源的加载。此外,由于 <iframe>
元素本质上是在顶级文档中加载的完整 HTML 文档,因此它们可能包含大量子资源(尤其是 JavaScript),如果这些框架中的任务需要大量处理时间,则可能会严重影响页面的 INP。
第三方嵌入是 <iframe>
元素的常见用例。例如,嵌入式视频播放器或社交媒体帖子通常会使用 <iframe>
元素,它们通常需要大量的子资源,这也会导致顶级页面资源的带宽争用。例如,在初始网页加载期间延迟加载 YouTube 视频的嵌入代码可节省超过 500 KiB,而延迟加载 Facebook “赞”按钮插件可节省超过 200 KiB,其中大部分是 JavaScript 文件。
无论采用哪种方式,每当网页的非首屏位置有 <iframe>
时,如果提前加载并不重要,您都应该强烈考虑延迟加载,因为这样做可以显著改善用户体验。
a.<iframe>
元素的 loading 属性
所有主要浏览器也支持 <iframe>
元素上的 loading 属性。loading 属性的值及其行为与使用 loading 属性的 <img>
元素相同:
“eager” 是默认值。它会通知浏览器立即加载 <iframe>
元素的 HTML 及其子资源。
“lazy” 会延迟加载 <iframe>
元素的 HTML 及其子资源,直到其在与视口的预定义距离内。
为避免布局偏移,Chrome 会预留空间,并在系统仍在提取延迟加载的 <iframe>
时显示占位符。不过,您仍应考虑使用 <iframe>
元素的 width 和 height 属性以及 CSS 中的其他样式,以最大限度地减少布局偏移。
b.外墙
您可以按需加载嵌入内容以响应用户互动,而不是在网页加载期间立即加载嵌入内容。这可以通过显示图片或其他适当的 HTML 元素来实现,直到用户与之互动。用户与该元素互动后,您可以将其替换为第三方嵌入内容。 这种技术称为“ Facade”。
Facade 的一个常见用例是通过第三方服务嵌入视频,在嵌入内容时,除了视频内容本身之外,可能还涉及加载许多额外且可能成本高昂的子资源,例如 JavaScript。在这种情况下,除非确实需要自动播放视频,否则视频嵌入会要求用户在播放之前点击播放按钮来与之互动。
这是显示与视频嵌入相似的静态图片的绝佳机会,并且可以在此过程中节省大量带宽。用户点击图片后,图片会被实际的 <iframe>
嵌入取代,这会触发第三方 <iframe>
元素的 HTML 及其子资源开始下载。
除了改进初始网页加载情况之外,另一个关键优势在于,如果用户从未播放视频,那么投放视频所需的资源也永远不会被下载。这是一种很好的模式,可以确保用户只下载他们实际需要的内容,而不会对用户的需求做出错误的假设。
聊天 widget 是 Facade 技术的另一个优秀用例。大多数聊天微件都会下载大量 JavaScript,这可能会对网页加载和对用户输入的响应速度产生负面影响。与预先加载任何内容一样,系统会在加载时产生费用,但对于聊天微件,并非所有用户都无意与之交互。
而使用 Facade,可以将第三方“Start Chat”按钮替换为虚假按钮。用户与其进行有意义的互动(例如,将指针悬停在其上一段时间或通过点击)后,会在需要时放入实际的可正常使用聊天 widget。
虽然您当然可以构建自己的 Facade,但也有适用于更热门的第三方的开源选项,例如,适用于 YouTube 视频的 lite-youtube-embed、适用于 Vimeo 视频的 lite-vimeo-embed 以及适用于聊天微件的 React Live Chat 加载器。
3.JavaScript 延迟加载库
如果您需要延迟加载 <video>
元素、<video>
元素 poster 图片、CSS background-image 属性加载的图片或其他不受支持的元素,可以使用基于 JavaScript 的延迟加载解决方案(例如 lazysizes 或 yall.js)执行此操作,因为延迟加载这些类型的资源并不是浏览器级别的功能。
具体而言,在没有音轨的情况下自动播放和循环播放 <video>
元素比使用动画 GIF 更高效,后者往往比具有同等视觉效果的视频资源大好几倍。即便如此,这些视频在带宽方面仍然非常重要,因此延迟加载它们是一种额外的优化,可以在很大程度上减少浪费的带宽。
其中大多数库通过使用 Intersection Observer API 以及 Mutation Observer API(如果网页的 HTML 在初始加载后发生变化)来识别元素何时进入用户视口。如果图片可见或接近视口,则 JavaScript 库会使用正确的属性(例如 src)替换非标准属性(通常是 data-src 或类似属性)。
假设您有一个可替换动画 GIF 的视频,但想使用 JavaScript 解决方案延迟加载它。不妨使用以下标记模式通过 yall.js 做到这一点:
<!-- The autoplay, loop, muted, and playsinline attributes are to
ensure the video can autoplay without user intervention. -->
<video class="lazy" autoplay loop muted playsinline width="320" height="480">
<source data-src="video.webm" type="video/webm">
<source data-src="video.mp4" type="video/mp4">
</video>
默认情况下,yall.js 会使用 “lazy” 类观察所有符合条件的 HTML 元素。在网页上加载并执行 yall.js 后,除非用户将其滚动到视口中,否则视频不会加载。这时,<video>
元素的子元素 <source>
元素的 data-src 属性将交换为 src 属性,后者会发送视频下载请求并自动开始播放视频。
十、预提取、预渲染和 Service Worker 预缓存
推迟资源加载会在初始网页加载期间减少网络和 CPU 使用率,具体方法是在需要资源的位置下载资源,而不是预先加载这些资源,因为资源可能未被使用。这可以缩短初始网页加载时间,但如果后续互动所需的资源在发生时尚未加载完毕,后续交互就可能会出现延迟。
例如,如果页面包含自定义日期选择器,您可以将日期选择器的资源推迟到用户与相应元素互动之后。不过,按需加载日期选择器的资源可能会导致延迟(或许略微但也可能不会发生延迟,具体取决于用户的网络连接和/或设备功能),直到资源下载、解析并可供执行为止。
这种平衡有点棘手 - 您不想因为加载可能不会用到的资源而浪费带宽,但延迟互动和后续网页加载可能也不理想。幸运的是,您可以使用许多工具来在这两种极端之间取得更好的平衡。如预提取资源、预渲染整个页面以及使用 Service Worker 预缓存资源。
1.短期内以低优先级预提取所需资源
您可以使用 <link rel="prefetch">
资源提示提前提取资源(包括图片、样式表或 JavaScript 资源)。prefetch 提示会告知浏览器不久的将来可能需要某个资源。
如果指定了 prefetch 提示,浏览器便能够以最低优先级发起对该资源的请求,以避免与当前页面所需的资源发生争用。
prefetch 资源提示就是一个提示。浏览器可能会根据一系列条件(关于网络质量、系统级偏好设置或其他因素)决定是否遵循 prefetch 提示。
预提取资源可以改善用户体验,因为用户不需要等待近期所需的资源下载完毕,因为可以在需要时从磁盘缓存中即时检索这些资源。
<head>
<!-- ... -->
<link rel="prefetch" as="script" href="/date-picker.js">
<link rel="prefetch" as="style" href="/date-picker.css">
<!-- ... -->
</head>
上述 HTML 代码段会告知浏览器,其空闲时可以预提取 date-picker.js 和 date-picker.css。您也可以在用户与 JavaScript 中的页面互动时动态预提取资源。
除 Safari 外,所有现代浏览器均支持 prefetch(在 Safari 中用标志提供)。如果您迫切需要以适用于所有浏览器的方式预先加载网站的资源,并且您正在使用 Service Worker。
2.预提取网页,加快后续导航速度
您还可以通过在指向 HTML 文档时指定 as=“document” 属性来预提取网页及其所有子资源:
<link rel="prefetch" href="/page" as="document">
当浏览器处于闲置状态时,可能会针对 /page 发起低优先级的请求。
通常建议避免使用 <link rel="prefetch">
预提取跨源文档。存在一个与预提取跨源文档相关的待解决问题,会导致重复请求。此外,您还应避免预提取个性化同源个性化的文档(例如,为经过身份验证的会话动态生成的 HTML 响应),因为此类资源通常不会被缓存,并且很有可能未被使用,最终会浪费带宽。
在基于 Chromium 的浏览器中,您可以使用 Speculation Rules API 预提取文档。推测规则定义为包含在网页的 HTML 中的 JSON 对象,或通过 JavaScript 动态添加:
<script type="speculationrules">
{
"prefetch": [{
"source": "list",
"urls": ["/page-a", "/page-b"]
}]
}
</script>
JSON 对象描述一项或多项操作(目前仅支持 prefetch 和 prerender),以及与该操作相关联的网址列表。在上述 HTML 代码段中,浏览器指示要预提取 /page-a 和 /page-b。与 <link rel="prefetch">
类似,推测规则是浏览器在某些情况下可能会忽略的提示。
虽然 <link rel="prefetch">
会预提取资源并将其存储在 HTTP 缓存中,但系统会处理使用推测规则加载的预提取内容,并将其存储在内存缓存中,以便在需要时更快地进行检索。
当网页在用户视口中可见时,Quicklink 等库会动态预提取或预呈现网页链接,从而改进网页导航。与预提取网页上的所有链接相比,这样可提高用户最终导航到该网页的可能性。
预提取资源可能会导致用户下载可能最终未使用的资源。预提取资源时,请务必谨慎:仅在必要时才使用预提取,而只在快速连接上使用;如果用户已启用 Save-Data 信号,则完全避免预提取。
3.预渲染页面
除了预提取资源之外,还可以提示浏览器在用户导航到网页之前预呈现网页。这可实现近乎即时的网页加载,因为系统会在后台提取和处理网页及其资源。当用户导航到相应页面后,该页面便会置于前台。
Speculation Rules API 支持预渲染:
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["/page-a", "page-b"]
}
]
}
</script>
Chrome 也支持 <link rel="prerender" href="/page">
资源提示。不过,从 Chrome 63 开始,这会启动 NoState 预提取来提取网页所需的资源,而不是呈现网页并执行 JavaScript。
完全预呈现也会在进行预呈现的网页上执行 JavaScript。鉴于 JavaScript 可能是一种相当大且计算成本高昂的资源,建议您谨慎使用“预渲染”,并且仅在您非常确定用户打算导航到预渲染的网页的情况下。
4.Service Worker 预缓存
还可以使用 Service Worker 推测性地预提取资源。Service Worker 预缓存可以使用 Cache API 提取和保存资源,使浏览器无需进入网络即可使用 Cache API 处理请求。Service Worker 预缓存使用非常有效的 Service Worker 缓存策略,称为“仅缓存”策略。此模式非常有效,因为将资源放入 Service Worker 缓存后,请求会立即提取。
仅缓存策略仅在 Service Worker 安装期间从网络中检索符合条件的资源。安装后,缓存的资源只能从 Service Worker 缓存中检索。
要使用 Service Worker 预缓存资源,您可以使用 Workbox。不过,如果您愿意,可以编写自己的代码来缓存一组预定文件。无论采用哪种方式,您决定使用 Service Worker 预缓存资源,都必须了解在 Service Worker 安装时进行预缓存。安装后,预缓存的资源便可在 Service Worker 在您的网站上控制的任何页面上进行检索。
虽然您当然可以从头开始编写自己的 Service Worker,但使用 Workbox 特别适用于预缓存,因为它可以跟踪已缓存资源的版本编号信息。之后,如果您日后更新 Service Worker,Workbox 会自动从缓存中移除过期条目,从而为您省去了自行执行此操作的麻烦。
Workbox 使用预缓存清单来确定应预缓存哪些资源。预缓存清单是文件和版本控制信息的列表,可作为要预缓存的资源的“可信来源”。
[{
url: 'script.ffaa4455.js',
revision: null
}, {
url: '/index.html',
revision: '518747aa'
}]
上述代码是一个示例清单,其中包含 script.ffaa4455.js 和 /index.html 这两个文件。如果资源在文件本身中包含版本信息(称为“文件哈希”),则 revision 属性可以保留为 null,因为文件已进行版本控制(例如,上述代码中 script.ffaa4455.js 资源的 ffaa4455 为 ffaa4455)。对于没有版本控制的资源,可以在构建时为其生成修订版本。
设置后,Service Worker 可用于预缓存静态页面或其子资源,以加快后续页面导航的速度。
workbox.precaching.precacheAndRoute([
'/styles/product-page.ac29.css',
'/styles/product-page.39a1.js',
]);
例如,在电子商务商品详情页面上,可以使用 Service Worker 预缓存呈现商品详情页面所需的 CSS 和 JavaScript,从而使用户能够更快地导航到商品详情页面。在上述示例中,product-page.ac29.css 和 product-page.39a1.js 已预缓存。workbox-precaching 中提供的 precacheAndRoute 方法会自动注册所需的处理程序,以确保在必要时从 Service Worker API 提取预缓存资源。
由于 Service Worker 受到广泛支持,因此您可以根据情况在任意现代浏览器中使用 Service Worker 预缓存。
Service Worker 使用的 Cache 接口和 HTTP 缓存不同。 Cache 接口是由 JavaScript 控制的高层级缓存,而 HTTP 缓存是由 Cache-Control 标头控制的低层级缓存。
与使用资源提示或推测规则预提取或预呈现资源类似,Service Worker 预缓存也会消耗网络带宽、存储空间和 CPU。建议仅预缓存可能会使用的资源,并在预缓存清单中指定过多的资源。如有疑问,最好预缓存过少而不是过多,并依靠运行时缓存通过多种模式之一来填充 Service Worker 缓存,以平衡速度和资源新鲜度。
十一、Web Worker 概览
JavaScript 通常被描述为单线程语言。实际上,这是指主线程,主线程是单个线程,浏览器负责执行您在浏览器中看到的大部分工作。这项工作涉及多种任务,例如编写脚本、某些类型的渲染工作、HTML 和 CSS 解析,以及其他类型的有助于改善用户体验的工作。事实上,浏览器确实会使用其他线程来执行您(开发者)通常无法直接访问的工作,例如 GPU 线程。
就 JavaScript 而言,您通常只能在主线程上执行工作,但这只是默认设置。可以在 JavaScript 中注册和使用其他线程。允许在 JavaScript 中实现多线程的功能称为 Web Workers API。
当您具有计算成本高的工作,并且无法在主线程上运行工作时,如果不会导致耗时较长的任务导致页面无响应,Web Worker 会非常有用。此类任务肯定会影响网站的 Interaction to Next Paint (INP),因此了解何时有工作可以完全在主线程以外完成会很有帮助。这样做有助于为主线程上的其他任务腾出更多空间,以便加快用户互动。
1.Web Worker 的启动方式
Web Worker 是通过实例化 Worker 类注册的。执行此操作时,您可以指定 Web 工作器代码所在的位置,浏览器将加载该代码并随后为其创建新线程。生成的线程通常称为“工作器线程”。
const myWebWorker = new Worker('/js/my-web-worker.js');
然后,您可以在工作器的 JavaScript 文件(在本例中为 my-web-worker.js)中编写代码,该代码随后会在单独的工作器线程中运行。
2.Web Worker 的限制
与在主线程上运行的 JavaScript 不同,Web 工作器无法直接访问 window 上下文,并且对其提供的 API 的访问权限会受到限制。Web Worker 受到以下限制:
Web Worker 无法直接访问 DOM。
Web Worker 可以通过消息传递流水线与 window 上下文通信,这意味着 Web Worker 可以通过某种方式间接访问 DOM。
Web Worker 的范围是 self,而不是 window。
Web Worker 作用域确实可以访问 JavaScript 基元和构造,以及 fetch 和大量其他 API 等 API。
3.Web Worker 如何与 window 通信
Web Worker 可以通过消息传递流水线与主线程的 window 上下文进行通信。此流水线可让您将数据传输到主线程和 Web 工作器,或者从主线程和 Web 工作器传输数据。如需将数据从 Web 工作器发送到主线程,您可以在 Web 工作器的上下文 (self) 中设置 message 事件:
// my-web-worker.js
self.addEventListener("message", () => {
// Sends a message of "Hellow, window!" from the web worker:
self.postMessage("Hello, window!");
});
然后,在主线程上的 window 上下文中的脚本中,您可以使用另一个 message 事件接收来自 Web 工作器线程的消息:
// scripts.js
// Creates the web worker:
const myWebWorker = new Worker('/js/my-web-worker.js');
// Adds an event listener on the web worker instance that listens for messages:
myWebWorker.addEventListener("message", ({ data }) => {
// Echoes "Hello, window!" to the console from the worker.
console.log(data);
});
对于基本任务,直接使用 Web 工作器的消息传递流水线可能没什么问题。不过,如果您希望在情况开始愈加复杂时简化这项工作,那么 Comlink 等抽象会非常方便。
eb 工作器的消息流水线是 Web 工作器上下文中的一种应急方法。使用该模式,您可以从 Web Worker 向 window 发送数据,您可以用它来更新 DOM,也可以执行必须在主线程上完成的其他工作。
十二、Web Worker 的具体用例
Web Worker 可以将 JavaScript 从主线程移到单独的 Web 工作器线程,从而提高输入响应速度,当您有不需要直接访问主线程的工作时,这有助于改进网站的 Interaction to Next Paint (INP)。
例如,网站可能需要从图片中删除 Exif 元数据,这个概念并不复杂。事实上,Flickr 等网站为用户提供了查看 Exif 元数据的方法,目的是了解有关其所托管图片的技术细节,例如色深、相机品牌和型号以及其他数据。
但是,提取图片、将其转换为 ArrayBuffer 以及提取 Exif 元数据的逻辑如果完全在主线程上完成,那么成本可能很高。幸运的是,Web Worker 作用域允许在主线程以外完成这项工作。然后,使用 Web 工作器的消息传递流水线,Exif 元数据以 HTML 字符串的形式传回主线程并向用户显示。
1.没有 Web Worker 时的主线程
首先,观察在不使用 Web Worker 的情况下执行此工作时的主线程。为此,请执行以下步骤:
- 在 Chrome 中打开新标签页,然后打开其开发者工具。
- 打开“性能”面板。
- 前往 https://exif-worker.glitch.me/without-worker.html。
- 在性能面板中,点击开发者工具窗格右上角的 Record。
- 在该字段中粘贴此图片链接或您选择的另一个包含 Exif 元数据的链接,然后点击 Get that JPEG! 按钮。
- 界面填充 Exif 元数据后,再次点击 Record 以停止录制。
图片元数据提取器应用中的主线程 activity。请注意,所有 activity 都发生在主线程上。
请注意,除了可能存在的其他线程(例如光栅化程序线程等)之外,应用中的所有内容都在主线程中进行。在主线程上,会出现以下情况:
- 表单会接受输入并分派 fetch 请求,以获取包含 Exif 元数据的图片的初始部分。
- 图片数据会转换为 ArrayBuffer。
- exif-reader 脚本用于从图片中提取 Exif 元数据。
- 爬取元数据构建 HTML 字符串,然后填充元数据查看器。
现在将其与具有相同行为的实现(但使用的是 Web Worker)进行对比!
2.使用 Web Worker 时的主线程是什么样子
现在,您已经了解了在主线程上从 JPEG 文件提取 Exif 元数据的情况,再看看使用 Web Worker 时的情况:
- 在 Chrome 中打开另一个标签页,然后打开其开发者工具。
- 打开“性能”面板。
- 转到 https://exif-worker.glitch.me/with-worker.html。
- 在性能面板中,点击开发者工具窗格右上角的记录按钮。
- 将此图片链接粘贴到相应字段中,然后点击获取那张 JPEG 图片!按钮。
- 界面填充 Exif 元数据后,再次点击录制按钮以停止录制。
图片元数据提取器应用中的主线程 activity。请注意,还有一个额外的 Web 工作器线程,用于完成大部分工作。
这就是 Web Worker 的强大功能。除使用 HTML 填充元数据查看器之外的所有操作都是在单独的线程上完成的,而不是在主线程上完成所有操作。这意味着主线程将释放下来以执行其他工作。
也许此处的最大优势在于,与不使用 Web 工作器的此应用版本不同,exif-reader 脚本不是在主线程上加载,而是在 Web 工作器线程上加载。这意味着下载、解析和编译 exif-reader 脚本的成本在主线程之外发生。
3.查看 Web Worker 代码
先从在 Web 工作器进入图片之前需要出现的主线程代码开始:
// scripts.js
// Register the Exif reader web worker:
const exifWorker = new Worker('/js/with-worker/exif-worker.js');
// We have to send image requests through this proxy due to CORS limitations:
const imageFetchPrefix = 'https://res.cloudinary.com/demo/image/fetch/';
// Necessary elements we need to select:
const imageFetchPanel = document.getElementById('image-fetch');
const imageExifDataPanel = document.getElementById('image-exif-data');
const exifDataPanel = document.getElementById('exif-data');
const imageInput = document.getElementById('image-url');
// What to do when the form is submitted.
document.getElementById('image-form').addEventListener('submit', event => {
// Don't let the form submit by default:
event.preventDefault();
// Send the image URL to the web worker on submit:
exifWorker.postMessage(`${imageFetchPrefix}${imageInput.value}`);
});
// This listens for the Exif metadata to come back from the web worker:
exifWorker.addEventListener('message', ({ data }) => {
// This populates the Exif metadata viewer:
exifDataPanel.innerHTML = data.message;
imageFetchPanel.style.display = 'none';
imageExifDataPanel.style.display = 'block';
});
此代码在主线程上运行,并设置表单以将图片网址发送到 Web 工作器。然后,Web 工作器代码以加载外部 exif-reader 脚本的 importScripts 语句开始,然后设置到主线程的消息传递流水线:
// exif-worker.js
// Import the exif-reader script:
importScripts('/js/with-worker/exifreader.js');
// Set up a messaging pipeline to send the Exif data to the `window`:
self.addEventListener('message', ({ data }) => {
getExifDataFromImage(data).then(status => {
self.postMessage(status);
});
});
虽然用于将外部脚本导入 Web 工作器范围的 importScripts 语法广泛兼容,但在大多数浏览器中也可以使用静态 import 语法将模块导入 Web 工作器。
这段 JavaScript 会设置消息传递管道,以便在用户提交包含 JPEG 文件网址的表单时,该网址能够到达 Web 工作器。接着,下面的一小段代码会从 JPEG 文件中提取 Exif 元数据,构建一个 HTML 字符串,并将该 HTML 发送回 window,以便最终向用户显示:
// Takes a blob to transform the image data into an `ArrayBuffer`:
// NOTE: these promises are simplified for readability, and don't include
// rejections on failures. Check out the complete web worker code:
// https://glitch.com/edit/#!/exif-worker?path=js%2Fwith-worker%2Fexif-worker.js%3A10%3A5
const readBlobAsArrayBuffer = blob => new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(blob);
});
// Takes the Exif metadata and converts it to a markup string to
// display in the Exif metadata viewer in the DOM:
const exifToMarkup = exif => Object.entries(exif).map(([exifNode, exifData]) => {
return `
<details>
<summary>
<h2>${exifNode}</h2>
</summary>
<p>${exifNode === 'base64' ? `<img src="data:image/jpeg;base64,${exifData}">` : typeof exifData.value === 'undefined' ? exifData : exifData.description || exifData.value}</p>
</details>
`;
}).join('');
// Fetches a partial image and gets its Exif data
const getExifDataFromImage = imageUrl => new Promise(resolve => {
fetch(imageUrl, {
headers: {
// Use a range request to only download the first 64 KiB of an image.
// This ensures bandwidth isn't wasted by downloading what may be a huge
// JPEG file when all that's needed is the metadata.
'Range': `bytes=0-${2 ** 10 * 64}`
}
}).then(response => {
if (response.ok) {
return response.clone().blob();
}
}).then(responseBlob => {
readBlobAsArrayBuffer(responseBlob).then(arrayBuffer => {
const tags = ExifReader.load(arrayBuffer, {
expanded: true
});
resolve({
status: true,
message: Object.values(tags).map(tag => exifToMarkup(tag)).join('')
});
});
});
});
可以使用 Web 工作器执行各种操作,例如隔离 fetch 调用和处理响应,在不阻塞主线程的情况下处理大量数据。