渐进式加载
在本教程的前面步骤中,我们介绍了有助于将我们的 js13kPWA 示例打造为渐进式 Web 应用的 API:服务工作线程、Web 清单、通知和推送。在本文中,我们将更进一步,通过渐进式加载其资源来提高应用的性能。
首次有效绘制
尽快向用户提供有意义的内容非常重要——用户等待页面加载的时间越长,在等待所有内容完成之前离开的可能性就越大。我们应该能够至少向他们展示他们想要查看的页面的基本视图,并在最终加载更多内容的位置放置占位符。
这可以通过渐进式加载实现——也称为 延迟加载。这完全是为了尽可能延迟加载尽可能多的资源(HTML、CSS、JavaScript),并且只加载对于第一次体验真正需要的资源。
捆绑与拆分
许多访问者不会浏览网站的每个页面,但通常的做法是将我们拥有的所有功能捆绑到一个大文件中。一个 bundle.js
文件可能有多兆字节,而单个 style.css
捆绑包可能包含从基本 CSS 结构定义到网站每个版本的各种可能样式:移动设备、平板电脑、桌面电脑、仅打印等。
将所有这些信息作为一个文件加载而不是多个小文件加载的速度更快,但如果用户一开始不需要所有信息,我们可以只加载关键信息,然后在需要时管理其他资源。
渲染阻塞资源
捆绑是一个问题,因为浏览器必须加载 HTML、CSS 和 JavaScript,然后才能将其渲染结果绘制到屏幕上。在初始网站访问和加载完成之间的几秒钟内,用户会看到一个空白页面,这是一种糟糕的体验。
为了解决这个问题,我们可以例如在 JavaScript 文件中添加 defer
<script src="app.js" defer></script>
它们将在文档本身解析之后下载并执行,因此不会阻止渲染 HTML 结构。
另一种技术是仅在需要时使用 动态导入 加载 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>
标签中——这将进一步提高性能,但为了示例的可读性,我们也将跳过这种方法。
图像
占位符图像
与其在 <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
属性
交集观察器
这是对前面工作示例的渐进式增强——交集观察器 仅在用户向下滚动时加载目标图像,从而使其显示在视口中。
相关代码如下所示
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
对象,则应用会创建它的新实例。作为参数传递的函数处理一个或多个项目与观察器相交(即出现在视口中)的情况。我们可以迭代每个情况并做出相应的反应——当图像可见时,我们加载正确的图像并停止观察它,因为我们不再需要观察它。
让我们重申一下我们之前提到的渐进式增强——代码的编写方式是,无论是否支持交集观察器,应用都能正常工作。如果不支持,我们只需使用前面介绍的更基本的方法加载图像。
改进
请记住,有很多方法可以优化加载时间,本示例仅探讨了一种方法。您可以尝试使您的应用更健壮,使其无需 JavaScript 即可工作——无论是使用 <noscript>
显示已分配最终 src
的图像,还是通过将 <img>
标签包装在指向目标图像的 <a>
元素中,以便用户可以根据需要单击并访问它们。
我们不会这样做,因为应用本身依赖于 JavaScript——如果没有它,甚至不会加载游戏列表,并且服务工作线程代码也不会执行。
我们可以重写加载过程,使其不仅加载图像,还加载包含完整描述和链接的完整项目。它将像无限滚动一样——仅当用户向下滚动页面时才加载列表中的项目。这样,初始 HTML 结构将是最小的,加载时间更短,并且我们将获得更大的性能优势。
结论
减少初始加载的文件数量,将较小的文件拆分为模块,使用占位符,以及按需加载更多内容——这将有助于实现更快的初始加载时间,从而使应用创建者受益,并为用户提供更流畅的体验。
请记住渐进增强方法——无论设备或平台如何,都要提供可用的产品,但请确保为使用现代浏览器的用户丰富体验。
最后的想法
本教程系列到此结束——我们浏览了js13kPWA 示例应用的源代码,并了解了PWA 结构、使用 Service Workers 实现离线可用性、可安装的 PWA,以及最终的通知。
在这篇文章中,我们深入研究了渐进加载的概念,包括一个利用Intersection Observer API的有趣示例。
欢迎尝试代码,使用 PWA 功能增强您现有的应用,或自行构建全新的应用。PWA 相比常规 Web 应用具有巨大优势。