js13kGames:渐进式加载
在本教程的先前步骤中,我们介绍了有助于我们将 js13kPWA 示例应用制作为渐进式 Web 应用的 API,包括 Service Workers、Web Manifest 和 通知和推送。在本文中,我们将进一步通过渐进式加载其资源来提高应用的性能。
首次有意义的绘制
尽快向用户呈现有意义的内容至关重要——他们等待页面加载的时间越长,在全部加载完成前离开的可能性就越大。我们应该至少向他们展示页面所需的基本视图,并在稍后会加载更多内容的位置设置占位符。
这可以通过渐进式加载实现——也称为 延迟加载。其核心思想是尽可能推迟加载(HTML、CSS、JavaScript)资源,只立即加载对首次体验至关重要的部分。
打包与拆分
许多访问者不会浏览网站上的每一个页面,但通常的做法是将我们拥有的所有功能打包到一个大文件中。一个 bundle.js 文件可能包含数兆字节,而一个单独的 style.css 包可能包含从基本 CSS 结构定义到网站所有版本(移动、平板、桌面、仅打印等)的所有可能样式。
将所有信息作为单个文件加载比加载许多小文件更快,但如果用户最初并不需要所有内容,我们可以只加载关键部分,然后在需要时管理其他资源。
渲染阻塞资源
打包是个问题,因为浏览器必须先加载 HTML、CSS 和 JavaScript,然后才能将渲染结果绘制到屏幕上。在初始访问网站到加载完成的几秒钟内,用户会看到一个空白页面,这是一种糟糕的体验。
为了解决这个问题,我们可以例如在 JavaScript 文件中添加 defer 属性
<script src="app.js" defer></script>
这些文件将在文档本身解析完成 *之后* 下载和执行,因此不会阻塞 HTML 结构的渲染。
另一种技术是仅在需要时使用 动态 import 来加载 JavaScript 模块。
例如,如果网站有一个搜索按钮,我们可以在用户单击搜索按钮后加载搜索功能的 JavaScript。
document.getElementById("open-search").addEventListener("click", async () => {
const searchModule = await import("/modules/search.js");
searchModule.loadAutoComplete();
});
一旦用户单击按钮,就会调用异步的点击处理程序。该函数会等待模块加载完成,然后调用该模块导出的 loadAutoComplete() 函数。因此,search.js 模块仅在交互发生时才会被下载、解析和执行。
我们还可以拆分 CSS 文件并为其添加媒体类型
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="print.css" media="print" />
这将指示浏览器仅在满足条件时才加载它们。
在我们的 js13kPWA 演示应用中,CSS 非常简单,可以直接放在一个文件中,没有特定的加载规则。我们可以做得更进一步,将 style.css 中的所有内容都移到 index.html 的 <head> 中的 <style> 标签内——这将进一步提高性能,但为了示例的可读性,我们将跳过这种方法。
图像
除了 JavaScript 和 CSS,网站通常还会包含许多图像。当你在 HTML 中包含 <img> 元素时,所有引用的图像都会在初始网站访问期间被获取和下载。在宣布网站准备就绪之前下载数兆字节的图像数据是很常见的,但这同样会给人带来糟糕的性能印象。我们在开始查看网站时,并不需要所有图像都具备最佳质量。
这可以进行优化。首先,你应该使用类似于 TinyPNG 的工具或服务,它们可以在不显著改变质量的情况下减小图像文件大小。如果已经完成了这一步,那么你可以考虑使用 JavaScript 优化图像加载。我们将在下面进行解释。
占位符图像
与其在 <img> 元素的 src 属性中包含所有游戏截图(这将强制浏览器自动下载它们),不如通过 JavaScript 选择性地加载。js13kPWA 应用使用占位符图像,它小巧轻便,而最终图像的路径则存储在 data-src 属性中。
<img src="data/img/placeholder.png" data-src="data/img/SLUG.jpg" alt="NAME" />
这些图像将在网站完成 HTML 结构构建 *之后* 通过 JavaScript 加载。占位符图像的缩放方式与原始图像相同,因此它会占据相同的空间,并且在图像加载时不会导致布局重绘。
通过 JavaScript 加载
app.js 文件处理 data-src 属性,如下所示:
let imagesToLoad = document.querySelectorAll("img[data-src]");
const loadImages = (image) => {
image.setAttribute("src", image.getAttribute("data-src"));
image.onload = () => {
image.removeAttribute("data-src");
};
};
imagesToLoad 变量包含所有图像的引用,而 loadImages 函数将路径从 data-src 移到 src。当每个图像实际加载完成后,我们删除它的 data-src 属性,因为它不再需要了。然后我们遍历每个图像并加载它。
imagesToLoad.forEach((img) => {
loadImages(img);
});
CSS 中的模糊效果
为了使整个过程在视觉上更具吸引力,占位符在 CSS 中被模糊处理。

我们最初渲染图像时带有模糊效果,因此可以实现向清晰图像的过渡。
article img[data-src] {
filter: blur(0.2em);
}
article img {
filter: blur(0em);
transition: filter 0.5s;
}
这将在半秒钟内消除模糊效果,对于“加载中”效果来说已经足够好了。
按需加载
上面讨论的图像加载机制工作得很好——它在渲染 HTML 结构后加载图像,并在此过程中应用了漂亮的过渡效果。问题在于它仍然一次性加载 *所有* 图像,尽管用户在页面加载时只会看到前两三个。
这个问题可以通过仅在需要时加载图像来解决:这被称为 *延迟加载*。 延迟加载 是一种仅当图像出现在视口中时才加载图像的技术。有几种方法可以告诉浏览器延迟加载图像。
<img> 上的 loading 属性
告诉浏览器延迟加载的最简单方法不需要 JavaScript。只需在 <img> 元素中添加 loading 属性,其值为 lazy,浏览器就会知道仅在需要时加载此图像。
<img
src="data/img/placeholder.png"
data-src="data/img/SLUG.jpg"
alt="NAME"
loading="lazy" />
交集观察器
这是先前有效示例的渐进式增强—— Intersection Observer 将仅在用户向下滚动导致目标图像显示在视口中时才加载它们。
相关的代码看起来是这样的:
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((items, observer) => {
items.forEach((item) => {
if (item.isIntersecting) {
loadImages(item.target);
observer.unobserve(item.target);
}
});
});
imagesToLoad.forEach((img) => {
observer.observe(img);
});
} else {
imagesToLoad.forEach((img) => {
loadImages(img);
});
}
如果支持 IntersectionObserver 对象,则应用会创建一个新实例。作为参数传递的函数处理一个或多个项与观察器相交(即出现在视口内)的情况。我们可以迭代每个情况并做出相应反应——当图像可见时,我们加载正确的图像并停止观察它,因为我们不再需要观察它。
让我们重申一下我们之前提到的渐进式增强——代码的编写方式使得应用无论是否支持 Intersection Observer 都能正常工作。如果不支持,我们只会使用前面介绍的更基本的方法来加载图像。
改进
请记住,有许多方法可以优化加载时间,而此示例仅探讨了其中一种方法。你可以尝试使你的应用更健壮,使其在没有 JavaScript 的情况下也能工作——可以使用 <noscript> 来显示已分配最终 src 的图像,或者将 <img> 标签包装在指向目标图像的 <a> 元素中,这样用户就可以在需要时单击并访问它们。
我们不会这样做,因为应用本身依赖于 JavaScript——没有它,游戏列表甚至不会加载,Service Worker 代码也不会执行。
我们可以重写加载过程,不仅加载图像,还加载完整的项目,包括完整的描述和链接。它将像无限滚动一样工作——仅当用户向下滚动页面时才加载列表中的项目。这样,初始 HTML 结构将非常小,加载时间会更短,我们将获得更大的性能优势。
总结
初始加载的文件更少,文件被拆分成模块,使用占位符,并按需加载更多内容——这将有助于实现更快的初始加载时间,从而为应用创建者带来好处,并为用户提供更流畅的体验。
请记住渐进式增强方法——无论设备或平台如何,都要提供一个可用的产品,但要确保为使用现代浏览器的用户提供增强的体验。
最后的思考
本教程系列到此结束——我们回顾了 js13kPWA 示例应用的源代码,并学习了 PWA 结构、通过 Service Workers 实现离线可用性、可安装的 PWA,以及最后 通知。
而在本文中,我们探讨了渐进式加载的概念,包括一个利用 Intersection Observer API 的有趣示例。
欢迎随意尝试代码,用 PWA 功能增强你现有的应用,或自己构建全新的东西。PWA 相较于普通 Web 应用具有巨大的优势。