在前面的文章我们介绍了很多api,例如:Service Workers, Web Manifests, Notifications and Push,它让我们的示例应用 js13kPWA 成为一个渐进式web应用。在这篇文章里我们将会走得更远,我们会使用资源渐进式加载来提高整个应用的性能。

首次有效渲染

尽快把有效的信息输送给用户是一件非常重要的事情 —— 等待页面加载的时间越长,用户在页面加载完成之前离开的概率就越大。为了达到这个目的,网页加载完成前,我们应该用占位符在最终资源将会加载的地方展示最起码的视图骨架。

这个功能可以用渐进式加载来实现 —— 它也被称为 懒加载。它的做法是延迟加载尽可能多的资源(HTML, CSS, JavaScript),只有在用户第一次使用到它的时候,它才会被立刻加载。

打包还是拆分

大部分的用户不会用到一个网站的所有页面,但我们通常的做法却是把我们所有的功能都打包进一个很大的文件里面。一个 bundle.js 文件的大小可能会有几个M,一个打包后的style.css 会包含一切东西,从CSS结构定义到各个版本的网站的样式:移动端,平板,桌面,打印等等。

通常来说只加载一个打包后大的文件会比加载很多个小文件要快一些,但如果用户并不是一开始就需要所有的资源,我们就可以首先加载那些关键的资源,其他的等到我们需要的时候,我们再去加载它。

导致页面阻塞的资源(Render-blocking resources)

将所有文件打包是一种不好的做法,因为浏览器把计算结果渲染到屏幕之前,需要先把HTML,CSS和JavaScript下载下来。在页面被打开到页面加载完成的这段时间里,用户将会看到一个空白的页面,这无疑是一个非常糟糕的体验。

为了解决这个问题,举个例子,我们可以在script标签上面加上一个 defer

<script src="app.js" defer></script>

他们会等到文档解析完成之后再开始下载和执行,所以他们不会阻塞HTML页面的渲染。我们还可以拆分css文件并给它们加上media属性:

<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="print.css" media="print">

 这种做法告诉浏览器只有在条件满足的情况下才加载这些资源(例如指定了print,则在打印环境下才会加载这些资源,译者注)。

在我们这个js13kPWA应用里面,由于CSS非常的简单,因此所有样式都被放到一个文件里面,并没有具体的规则来指导它如何加载css。但我们可以做得更好,例如把 style.css 里面的所有内容移动到一个 <style> 标签里面,并把它放到 index.html  的 <head> 的里面。这样做可以进一步提高应用的性能,但为了使代码更具可读性,我们并没有选择这么做。

图片

除了JavaScript和CSS,网站通常还会包含大量的图片。当你把<img>元素添加到网站里面时,对应的所有图片资源都会在页面初始化时被下载下来。在网站就绪之前下载几个兆的图片资源是一种比较常见的状况,但它会再次给人一种性能不好的印象。我们并不需要在一打开网站的时候就以最高的画质呈现所有的图片。

这是可以优化的。首先,你可以使用类似TinyPNG这类工具或者服务,它可以在不过分降低画质的情况下压缩文件的大小。如果你已经做到了这一点,那你可以考虑一下如何通过JavaScript来优化图片的下载了,我们将会在下面的篇幅提到这些内容。

图片占位符

我们可以通过使用JavaScript有选择的加载图片,而不是把所有的游戏截图都放到 <img> 标签的 src 属性里面,因为那样浏览器会自动下载所有的图片。 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');
  };
};

当函数 loadImages 把地址从  data-src 移动到 src 上时,  imagesToLoad 变量包含了所有图片的链接。当每个图片都已经加载完成时,我们会把data-src 属性移除掉,因为这个时候已经没有任何用处了。我们对遍历了所有的图片来加载这些图片:

imagesToLoad.forEach((img) => {
  loadImages(img);
});

用CSS制造模糊

为了让整个过程在视觉上更加吸引人,图片占位符的样式用CSS做了模糊处理.

Screenshot of placeholder images in the js13kPWA app.

我们在开始时用模糊来渲染图像,因此可以实现一个从模糊到清晰图像的过渡效果:

article img[data-src] {
  filter: blur(0.2em);
}

article img {
  filter: blur(0em);
  transition: filter 0.5s;
}

这个样式会在半秒钟内移除模糊效果,它会让“加载”效果看起来更好看:

按需加载

上面讨论的图片加载机制工作得还不错——它在HTML文档加载完成之后再开始加载图片,并且在加载过程中还提供了一个很漂亮的过度效果。问题是它仍然一次性加载了所有的图片,即使网站加载完成后用户有可能只看前两张或者三张图片。

这个问题可以用新的 Intersection Observer API 来解决 —— 通过这个api我们可以确保只有当图片出现在可见区域时,它才会被加载。

Intersection Observer

这是对上面那个可以正常工作的例子提供的一个渐进增强功能 —— 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对象被浏览器所支持,app会对它新建一个实例。当一个或多个item(指的是监听的对象,例如一个img,译者注)跟观察者发生交互时(图片出现在视图中时),作为参数传递的函数可以用来处理一些回调事务(例如图片加载,译者注)。我们可以对每一个项目做一个迭代并对它们做出相应的处理——当图片可见时,我们开始加载真正的图片并且停止监听这张图片,因为图片加载完成之后,我们已经没有很必要再知道它的状态了。

让我们来重申一下我们之前提到的渐进增强 —— 代码这么写的好处在于,不管 Intersection Observer是否被当前浏览器支持,app都能够正常的工作。如果 Intersection Observer不被支持,我们会用上面提到的基础方法来实现图片的加载。

一些改进

记住,有很多的方法可以用来优化我们的加载时间,这个示例只探讨了其中的一种方法。你可以尝试让你的app变的更加健壮,通过让它在没有JavaScript的情况下也能工作 —— 通过使用 <noscript> 标签来展示已经分配了最终 src 路径的图片,或者在 <img> 外面套上一个 <a> 标签并指向对应的图片资源,当用户想要想要看的时候他们可以点击图片并访问他们。

我们并没有这么做因为这个app本身依赖于JavaScript —— 没有JavaScript游戏列表就无法加载,Service Worker相关的代码也将无法执行。

我们可以重写整个加载过程,让它不止加载图片,而是加载整个列表项,包括详细介绍和链接。工作起来它像一个无限滚动的页面——当用户往下滚动的时候开始加载新的项目。通过这个方法HTML页面体积会达到最小,加载时间可以更短,我们也可以从中获取到更大的性能优势。

结论

初始化时加载更少的文件,分拆成更小的模块,使用占位符以及按需加载更多的内容 —— 这会让我们获得更短的首次加载时间,它既能让app开发者受益,也能给用户提供更加丝滑的体验。

记住关于渐进增强的内容 —— 不管在任何硬件或平台都提供一个可用的产品,但在现代浏览器上面确保能提供更好的用户体验。

最后的思考

这就是这个系列的所有内容了 —— 我们通过  js13kPWA 示例应用的源码 学习了渐进式web 应用的的用法,包括了PWA介绍, PWA 结构, 通过Service Workers让PWA离线工作, 让PWA易于安装,以及最后的通知功能。在Service Worker Cookbook的帮助下我们还解释了推送的原理。而在本篇文章中,我们探讨了渐进式加载的概念,包括一个使用了 Intersection Observer API的有趣例子。

请随意试验这些代码,通过使用PWA的特性来让你现有的应用更加健壮,或者自己创建一些全新的东西。相对于常规的web应用,PWA存在巨大的优势。

文档标签和贡献者

标签: 
此页面的贡献者: githubxiaominge
最后编辑者: githubxiaominge,