自行实现懒加载的技巧

Avatar of Phil Hawksworth
Phil Hawksworth 发表于

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

在寻找减轻特定网页负担的方法时,您可能听说过(甚至发出过呼吁)“我们可以直接使用懒加载!”。

懒加载是一种流行的技术,它可以逐渐请求图像,而是在图像进入视野时才请求,而不是在页面 HTML 解析完成后一次性请求所有图像。它可以减少初始页面大小,并通过在需要时请求图像来帮助我们实现 性能预算

它可以有效。但它也自带了一些问题。我们稍后会谈到!事实上,Rahul Nanwani 撰写了 一篇关于懒加载方法的全面文章,并说明了一些方法的复杂性。

在这篇文章中,我们将研究一种已经在 Preethi 的这篇博文中简要介绍过的实现。我们将对其进行扩展,以便您可以像我在 这个小型演示网站 上所做的那样,将您自己的懒加载实现添加到您的网站中。

我们将在过程中涵盖以下主题

以下是我们将要构建的内容

为什么不使用原生懒加载?

目前,懒加载并不是浏览器可以为我们原生实现的功能。尽管对于某些浏览器来说,这种情况很快就会改变,因为 Chrome 75 的发布旨在为图像和 iframe 提供懒加载支持。在此之前(以及之后,如果我们要与其他浏览器良好兼容——我们应该这样做),懒加载是使用 JavaScript 实现的。有很多库和框架可以帮助我们实现。

一些 静态网站生成器、库和框架包含提供此功能的实用程序“开箱即用”,这很受欢迎,因为人们正在寻找在他们的网站中包含此功能的内置方法。但我同时也注意到了一种趋势,即有些人选择采用整个库或框架来获取此功能。作为一个节俭、注重性能和包容性的吹毛求疵的人,我对这一点有点谨慎。因此,让我们看看如何在不需要特定框架或库的情况下自行实现这一点。

lazy loading example

懒加载的典型机制

大多数方法都遵循以下模式

首先,一些 HTML 来定义我们懒加载的图像

<!-- 
Don't include a src attribute in images you wish to load lazily.
Instead specify their src safely in a data attribute
-->
<img data-src="lighthouse.jpg" alt="A snazzy lighthouse" class="lazy" />

图像加载应该何时发生?

接下来,我们使用某种 JavaScript 魔法在图像进入视野时正确设置 `src` 属性。这曾经是一个代价高昂的 JavaScript 操作,需要监听窗口滚动和调整大小事件,但是 IntersectionObserver 已经解决了这个问题。

创建 Intersection Observer 看起来像这样

// Set up an intersection observer with some options
var observer = new IntersectionObserver(lazyLoad, {

  // where in relation to the edge of the viewport, we are observing
  rootMargin: "100px",

  // how much of the element needs to have intersected 
  // in order to fire our loading function
  threshold: 1.0

});

我们告诉我们的新观察者,当这些可观察条件满足时,调用一个名为 `lazyLoad` 的函数。满足这些条件的元素将传递给该函数,以便我们可以操作它们……比如实际加载和显示它们。

function lazyLoad(elements) {
  elements.forEach(image => {
    if (image.intersectionRatio > 0) {

      // set the src attribute to trigger a load
      image.src = image.dataset.src;

      // stop observing this element. Our work here is done!
      observer.unobserve(item.target);
    };
  });
};

很好。我们的图像将在它们进入视野时被分配正确的 `src`,这将导致它们加载。但是哪些图像?我们需要告诉 Intersection Observer API 我们关心哪些元素。幸运的是,我们为每个元素分配了一个 CSS 类 `lazy`,就是为了这个目的。

// Tell our observer to observe all img elements with a "lazy" class
var lazyImages = document.querySelectorAll('img.lazy');
lazyImages.forEach(img => {
  observer.observe(img);
});

不错。但是完美吗?

这似乎运行良好,但有一些缺点需要考虑

  1. 在 JavaScript 出现并成功运行之前(或除非),我们在页面上有一堆无法正常工作的图像元素。我们故意通过删除 `src` 属性来禁用它们。这是我们想要的结果,但现在我们依赖于 JavaScript 来加载这些图像。虽然 JavaScript 如今在网络上非常普遍——网络覆盖了如此广泛的设备和网络状况——但 JavaScript 可能会成为我们性能预算中代价高昂的一部分,尤其是在它参与内容的交付和呈现时。正如 Jake Archibald 曾经指出的那样,“所有用户在下载你的 JS 之前,都是非 JS 用户”。换句话说,这一点不容忽视。
  2. 即使成功运行,页面上也会出现空元素,这些元素在加载时可能会造成一些视觉上的跳动。也许我们可以先提示图像,然后做一些花哨的事情。我们很快就会谈到这一点。

Chrome 计划的原生懒加载实现应该有助于解决我们这里提到的第一个问题。如果元素被赋予了 `loading` 属性,Chrome 可以适时地识别 `src` 属性,而不是在 HTML 中看到它时就急切地请求它。

规范的编辑草案 包括对不同加载行为的支持

  • `<img loading="lazy" />`:告诉浏览器在需要时懒加载此图像。
  • `<img loading="eager" />`:告诉浏览器立即加载此图像。
  • `<img loading="auto" />`:让浏览器自行评估。

由于 HTML 的弹性和浏览器忽略它们不理解的 HTML 属性的特性,不支持此功能的浏览器可以像往常一样加载图像。

但是……发出响亮的警告!此功能尚未在 Chrome 中上线,并且关于其他浏览器是否以及何时会选择实施它也存在不确定性。我们可以使用特性检测来决定使用哪种方法,但这仍然没有提供一种可靠的渐进增强方法,在这种方法中,图像不依赖于 JavaScript。

<img data-src="lighthouse.jpg" alt="A snazzy lighthouse" loading="lazy" class="lazy" />
// If the browser supports lazy loading, we can safely assign the src
// attributes without instantly triggering an eager image load.
if ("loading" in HTMLImageElement.prototype) {
  const lazyImages = document.querySelectorAll("img.lazy");
  lazyImages.forEach(img => {
    img.src = img.dataset.src;
  });
}
else {
  // Use our own lazyLoading with Intersection Observers and all that jazz
}

作为响应式图像的补充

假设我们已经接受了 JavaScript 是目前依赖项的事实,那么让我们将注意力转向一个相关主题:响应式图像。

如果我们正在努力仅在需要时才将图像传递到浏览器,那么我们也可能希望确保我们也以最适合其显示方式的大小传递它们,这似乎是合理的。例如,如果显示图像的设备只会为其提供 400px 的宽度,则无需下载 1200px 宽的图像版本。让我们优化一下!

HTML 为我们提供了两种实现响应式图像的方法,这些方法将不同的图像源与不同的视口条件相关联。我喜欢使用 `picture` 元素,如下所示

<picture>
  <source srcset="massive-lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="medium-lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="small-lighthouse.jpg" media="(min-width: 300px)">
  <img src="regular-lighthouse.jpg" alt="snazzy lighthouse" />
</picture>

您会注意到每个 `source` 元素都有一个 `srcset` 属性,该属性指定图像 URL,以及一个 `media` 属性,该属性定义应使用此源的条件。浏览器根据媒体条件从列表中选择最合适的源,标准的 `img` 元素作为默认/后备。

我们可以将这两种方法结合起来实现懒加载响应式图像吗?

当然,我们可以!让我们来做吧。

与其在进行懒加载之前显示空图像,我更喜欢加载一个文件大小很小的占位符图像。这确实会带来发出更多 HTTP 请求的开销,但它也会在图像到达之前提供很好的提示效果。您可能在 Medium 上见过这种效果,或者是在使用 Gatsby 的懒加载机制的网站上见过这种效果。

我们可以通过最初在我们的 `picture` 元素中将图像源定义为同一资源的微小版本,然后使用 CSS 将它们缩放为与其高分辨率兄弟姐妹相同的大小来实现这一点。然后,通过我们的 Intersection Observer,我们可以更新每个指定的源以指向正确的图像源。

因此,最初我们的 `picture` 元素可能如下所示

<picture class="lazy">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 300px)">
  <img src="tiny-lighthouse.jpg" alt="snazzy cake" />
</picture>

无论应用什么视口大小,我们都将显示一个 20px 的小图像。接下来,我们将使用 CSS 放大它。

使用样式预览图像

浏览器可以使用 CSS 将微小的预览图像放大,使其适合整个图片元素,而不仅仅是 20px 的一部分。正如你所想象的,当低分辨率图像放大到更大的尺寸时,图像会变得有点……像素化。

picture {
  width: 100%; /* stretch to fit its containing element */
  overflow: hidden;
}

picture img {
  width: 100%; /* stretch to fill the picture element */
}
blurred image

为了确保效果更好,我们可以使用模糊滤镜来软化由于图像放大而产生的像素化现象。

picture.lazy img {
  filter: blur(20px);
}
more fully blurred image

使用 JavaScript 切换源

稍微修改一下,我们就可以使用之前相同的技术来设置 srcsetsrc 属性的正确 URL

function lazyLoad(elements) {

  elements.forEach(picture => {
    if (picture.intersectionRatio > 0) {

      // gather all the image and source elements in this picture
      var sources = picture.children;

      for (var s = 0; s < sources.length; s++) {
        var source = sources[s];

        // set a new srcset on the source elements 
        if (sources.hasAttribute("srcset")) {
          source.setAttribute("srcset", ONE_OF_OUR_BIGGER_IMAGES);
        }
        // or a new src on the img element
        else {
          source.setAttribute("src", ONE_OF_OUR_BIGGER_IMAGES);
        }
      }

      // stop observing this element. Our work here is done!
      observer.unobserve(item.target);
    };
  });

};

最后一步完成效果:在新源加载后,从图像中移除模糊效果。一个 JavaScript 事件监听器等待每个新图像资源的 load 事件就可以为我们做到这一点。

// remove the lazy class when the full image is loaded to unblur
source.addEventListener('load', image => {
  image.target.closest("picture").classList.remove("lazy")
}, false);

我们可以做一个不错的过渡,让模糊效果逐渐消失,并添加一些 CSS。

picture img {
  ...
  transition: filter 0.5s,
}

来自朋友们的一点帮助

很好。仅仅使用少量 JavaScript、几行 CSS 和非常易于管理的 HTML 代码,我们就创建了一种延迟加载技术,同时也满足了响应式图像的需求。那么,为什么我们不高兴呢?

嗯,我们创建了两个摩擦点。

  1. 我们添加图像的标记比以前更复杂了。当我们只需要一个带有 src 属性的 img 标签时,生活曾经很简单。
  2. 我们还需要创建每个图像资源的多个版本来填充每个视口尺寸和预加载状态。这需要更多的工作。

不要害怕。我们可以简化这两件事。

生成 HTML 元素

让我们首先看看如何生成 HTML,而不是每次都手动编写它。

无论你使用什么工具生成 HTML,它都可能包含使用 include、函数、短代码或宏的功能。我非常喜欢使用这样的辅助工具。它们使更复杂或更细致的代码片段保持一致,并节省了编写冗长代码的时间。大多数静态站点生成器都具有此类功能。

  • Jekyll 允许你创建自定义插件。
  • Hugo 提供了自定义短代码。
  • Eleventy 为其支持的所有模板引擎提供了短代码。
  • 还有很多……

例如,在我的使用 Eleventy 构建的示例项目中,我创建了一个名为 lazypicture 的短代码。短代码的使用方式如下:

{% lazypicture lighthouse.jpg "A snazzy lighthouse" %}

在构建时生成我们所需的 HTML。

<picture class="lazy">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 300px)">
  <img src="/images/tiny/lighthouse.jpg" alt="A snazzy lighthouse" />
</picture>

生成图像资源

我们为自己创建的另一项工作是生成不同大小的图像资源。我们不想手动创建和优化每个图像的每个尺寸。这项任务迫切需要一些自动化。

你选择自动化的方式应该考虑到你需要多少图像资源以及你可能多久向该集合添加更多图像。你可以选择在每次构建时生成这些图像。或者,你可以在请求时利用一些图像转换服务。让我们稍微看看这两种选择。

选项 1:在构建过程中生成图像

为此存在流行的实用程序。无论你使用 Grunt、Gulp、webpack、Make 或其他工具运行构建,都可能存在适合你的实用程序。

下面的示例在 Gulp 构建过程的一部分中,使用 gulp-image-resize 作为 Gulp 任务。它可以遍历一个充满图像资源的目录,并生成你所需的变体。它提供了许多选项供你控制,你可以将其与其他 Gulp 实用程序结合使用,例如根据你选择的约定命名不同的变体。

var gulp = require('gulp');
var imageResize = require('gulp-image-resize');

gulp.task('default', function () {
  gulp.src('src/**/*.{jpg,png}')
    .pipe(imageResize({
      width: 100,
      height: 100
    }))
    .pipe(gulp.dest('dist'));
});

CSS-Tricks 网站使用类似的方法(感谢 WordPress 中的自定义大小功能)来自动生成其所有不同的图像大小。(哦,是的!CSS-Tricks 言行一致!)ResponsiveBreakpoints.com 提供了一个 Web UI,用于试验创建图像集的不同设置和选项,甚至为你生成代码。

或者,你可以像 Chris 在 Twitter 上提到的那样以编程方式使用它。

但是,当你像 CSS-Tricks 那样拥有许多图像文件时,将此工作作为构建步骤的一部分可能会变得很麻烦。构建中的良好缓存和其他文件管理任务可以提供帮助,但很容易最终得到一个冗长的构建过程,并在执行所有工作时使你的计算机变热。

另一种选择是在请求时转换这些资源,而不是在构建步骤中转换。这是第二个选项。

选项 2:按需图像转换

我是一个大声疾呼预渲染内容的支持者。我已经多次谈论过这种方法(通常称为 JAMstack),我相信它具有许多性能、安全性和简单性的优势。(Chris 在一篇关于静态托管和 JAMstack 的 文章 中很好地总结了这一点。)

也就是说,在请求时生成不同图像大小的想法似乎与我的延迟加载目标相矛盾。事实上,现在有许多服务和公司专门从事这项工作,并且他们以非常强大且便捷的方式进行操作。

将图像转换与像 NetlifyFastlyCloudinary 这样的公司强大的 CDN 和资产缓存功能相结合,可以通过 URL 快速生成你传递给它们的尺寸的图像。每个服务都拥有强大的处理能力,可以即时执行这些转换,然后缓存生成的图像以供将来使用。这使得后续请求能够无缝渲染。

由于我在 Netlify 工作,因此我将使用 Netlify 的服务来说明这一点。但我提到的其他服务的工作方式类似。

Netlify 的图像转换服务 基于称为 Netlify 大型媒体 的内容构建。这是一项旨在帮助管理版本控制中大型资产的功能。默认情况下,Git 不擅长处理此类操作,但 Git 大型文件存储 可以扩展 Git,使其能够在不阻塞和使其难以管理的情况下,将大型资产包含在你的存储库中。

如果你有兴趣,可以阅读 有关管理大型资产的这种方法的更多背景信息

将图像放在我们的 Git 存储库的版本控制下是一个额外的好处,但就我们而言,我们更感兴趣的是享受对这些图像进行即时转换的好处。

Netlify 在转换图像时查找 querystring 参数。你可以指定高度、宽度以及想要执行的裁剪类型。如下所示:

  • 没有转换的原始图像:
    /images/apple3.jpg
  • 调整大小为 300px 宽的图像:
    /images/apple3.jpg?nf_resize=fit&w=300
  • 裁剪为 500px x 500px 并自动检测焦点点的图像:
    /images/apple3.jpg?nf_resize= smartcrop&w=500&h=500

知道我们可以在版本控制中的单个源图像中创建和交付任何图像大小,这意味着我们用来更新图像源的 JavaScript 只需要包含我们选择的尺寸参数。

这种方法可以大大加快你的网站构建过程,因为工作现在已外包,并且不在构建时执行。

总结

我们这里涵盖了很多内容。有很多非常可行的选项可以实现带有延迟加载的响应式图片。希望这能让你在使用最近可用的框架来获取此类功能之前三思而后行。

这个演示站点将许多这些概念整合在一起,并使用了Netlify的图片转换服务

最后总结一下流程

  • 一个带有短代码的静态网站生成器简化了创建图片元素的任务
  • Netlify 大型媒体托管并转换图片,然后将它们作为 20 像素宽的小版本提供服务,然后根据需要加载更大的文件。
  • CSS 将小图片放大并模糊它们以创建预览占位符图片。
  • Intersection Observer API 检测何时将图片资源替换为适当的较大版本。
  • JavaScript 检测较大图片的加载事件,并移除模糊效果以显示更高分辨率的渲染。