skip to content
OnionTalk

图片懒加载从简单到复杂

图片懒加载是一个很重要的前端性能优化手段。这篇文章将从懒加载的最简单场景开始介绍,逐步增加复杂度,希望能讲清楚常见的图片懒加载场景及在该场景下对应的解决办法。

为什么要做图片的懒加载

假设在用户访问某个页面时就加载这个页面全部的图片(即使这些图片并不处在用户的当前的视窗中),在弱网环境或者网速较慢的环境下,这些“冗余”图片的下载会占用用户本来就非常有限的带宽,伤害用户体验(比如影响其他资源的下载)。所以对于网站的图片,理想的做法是懒加载(按需加载)。

图片懒加载的原理

在浏览器内部对于各种资源有着一套自己的优先级定义,浏览器会优先加载优先级高的资源

如果我们不去进行图片的懒加载,默认情况下,资源的 priority 如下。

这些优先级标记为 high 的图片会占用其他资源的下载带宽,可能会造成某些比较关键的资源(比如 xhr call)加载缓慢,拖慢页面速度。

图片懒加载的简单实现

图片懒加载的思路一般时当页面加载时加载一个尺寸很小的占位图片(1kb 以下),然后再通过 js 选择性的去加载真正的图片。

一个最简单的的实现如下

<!-- index.html -->
<img src="placeholder.jpg" data-src="real_image.jpt" />
// index.css

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

img {
  filter: blur(0em);
  transition: filter 0.5s;
}
// index.js
(function lazyLoad() {
  const imageToLazy = document.querySelectorAll('img[data-src]');
  const loadImage = function (image) {
    image.setAttribute('src', image.getAttribute('data-src'));
    image.addEventListener('load', function () {
      image.removeAttribute('data-src');
    });
  };

  imageToLazy.forEach(function (image) {
    loadImage(image);
  });
})();

通过懒加载之后,资源优先级如下。

图片懒加载的进阶实现—滚动加载

上面的方案并不完美,对于用户来说,不在视窗中的图片可能根本不是用户当前关心的图片,所以我们可以让这些图片出现在用户视窗中再进行加载。

运用Intersection Observer 我们可以做到当图片滚动到视窗后再加载该图片。

(function lazyLoad() {
  const imageToLazy = document.querySelectorAll('img[data-src]');
  const loadImage = function (image) {
    image.setAttribute('src', image.getAttribute('data-src'));
    image.addEventListener('load', function () {
      image.removeAttribute('data-src');
    });
  };

  const intersectionObserver = new IntersectionObserver(function (items, observer) {
    items.forEach(function (item) {
      if (item.isIntersecting) {
        loadImage(item.target);
        observer.unobserve(item.target);
      }
    });
  });

  imageToLazy.forEach(function (image) {
    intersectionObserver.observe(image);
  });
})();

上面的这些 demo 都在https://github.com/hateonion/lazy-load 这个 repo 里面。

如何选择合适的 Placeholder 图片

在上面的 demo 中我们使用了 placeholder 图片,实际上,图片所占的位置是否确定对于我们选择 placeholder 图片有着很大的影响。

图片尺寸已知

图片尺寸已知出现的场景一般是博文的题图或者网站中一些固定尺寸的 thumbnail,这些图的尺寸一般固定且一般不会发生改变。对于这种场景,我们可以加载对应尺寸的 placeholder 图片(如上一节的 demo)。我们可以自己裁剪对应尺寸的的 placeholder 图片或者使用类似http://placeholder.com/ 这样的服务来获取 placeholder 图片。

图片尺寸未知

图片尺寸未知的情况下一般我们需要生成对应的 thumbnail 然后去加载我们生成的 thumbnail 去做 placeholder。为了生成这些 thumbnail 你可以调用imagemagick或者调用一些在线的图片分割服务(比如七牛

懒加载防止布局抖动

在图片懒加载时,由于图片的尺寸不定,浏览器难以计算需要给图片预留出的位置。所以当图片加载完成后会出现网页布局的抖动。

(image from From http://davidecalignano.it/lazy-loading-with-responsive-images-and-unknown-height/)

即使我们选择的 placeholder 很小,可以在毫秒级别完成下载,用户可能意识不到布局的抖动。但是在一些性能比较差的设备上,这种布局的抖动还是会一定程度上影响用户的体验。为了完全避免布局闪动,我们可以采用aspect ratio boxes 的技术来制作一个占位用的元素。

<div class="lazy-load__container feature">
  <img src="placeholder.jpg" data-src="real.jpg" />
</div>
.lazy-load__container {
  position: relative;
  display: block;
  height: 0;
}

.lazy-load__container.feature {
  // feature image 的高宽比设置成42.8%
  // 对于其他图片 比如 post图片,高宽比可能会不同,可以使用其他css class去设置
  padding-bottom: 42.8%;
}

.lazy-load__container img {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
}

结果

上面这个实现的原理其实很简单,由于 padding-bottom (或者 padding-top)声明为百分比时是根据元素生成的 box 的 width 去计算百分比的,所以我们通过 padding-bottom 去声明一个对应高宽比的 container。而这个 container 的具体尺寸会由尺寸确定的外层元素确定,但是高宽比始终保持一致。

而图片的尺寸设置成 100%container 的尺寸保证图片始终和 container 的尺寸保持一致。

需要注意的是上面这个方法并不能适配图片比例不一致的网站(比如本站),不过好在,为了用户体验,现在绝大多数网站的图片比例都有明确的要求,绝大多数情况下我们只适配保证网站常用的的几种图片宽高比例即可。

像 Medium 一样懒加载图片

Medium 的懒加载图片的体验相信去 Medium 读过文章的同学都体验过了,可以说是非常的流畅。而其背后的技术其实也就是我们上面讲到的几种技术的组合。

  1. 使用 aspect ratio box 创建占位元素。
  2. 在 html 解析时只加载一个小尺寸的图片,并且添加 blur 效果。
  3. 最后使用 js 选择性的加载真实图片。

Demo 如下 codePen by José M. Pérez

总结

  1. 懒加载用户当前视窗中的图片可以提升页面的加载性能。
  2. 懒加载的思路是在 html 解析时先加载一个 placeholder 图片,最后再用 js 选择性的加载真实图片。
  3. 如果需要滚动加载可以使用 Intersection Observer
  4. 对于固定尺寸和不定尺寸的图片,我们可以选择不同的服务去或者 placeholder 图片。
  5. 对于图片尺寸不确定引起的布局抖动问题我们可以使用 aspect ratio box 来解决。

参考资料

Progressive Loading

Lazy loading with responsive images and unknown height

Simple image placeholders for lazy loading images

How Medium does progressive image loading

Sizing Fluid Image Containers with a Little CSS Padding Hack