性能优化之图片懒加载

422 阅读3分钟

概念

图片懒加载是指在初始化页面时只加载出现在视口内(或者接近视口)的图片,其它的图片等待用户操作网页将其滚动到视口时,再进行加载。通过延迟加载图片,能够减小了网页首屏需要加载的资源数,优化了网页的首屏加载性能。

实现方案

监听滚动事件

通过监听浏览器的 scrollresizeorientationchange 事件,判断用户操作后图片位于视口内部(Image.getBoundingClientRect().top < window.innerHeight && Image.getBoundingClientRect().bottom >= 0)时,就将自定义属性 data-src 中的图片 url 赋值给 src 属性,开始加载图片资源。

注意:由于 scrollresize 事件的触发频率较高,频繁的计算会比较影响页面的性能,所以需要通过节流来优化代码的性能。

实现代码:

document.addEventListener('DOMContentLoaded', function () {
  // 获取所有图片元素
  let images = [].slice.call(document.querySelectorAll('img'));
  // 利用标记进行节流,减少触发次数
  let active = false;
  
  const loadImage = function () {
    if (!active) {
      active = true;
      setTimeout(function () {
        images.forEach(function (lazyImage) {
          // 判断图片位于视口内部
          if (
            lazyImage.getBoundingClientRect().top < window.innerHeight &&
            lazyImage.getBoundingClientRect().bottom >= 0 &&
            getComputedStyle(lazyImage).display !== 'none'
          ) {
            // 从自定义属性中获取图片路径,加载图片资源
            const { src, srcset } = lazyImage.dataset;
            src && (lazyImage.src = src);
            srcset && (lazyImage.srcset = lazyImage.dataset.srcset);
            // 移除已加载的图片
            images = images.filter(function (image) {
              return image !== lazyImage;
            });
            // 所有图片加载完毕,移除事件监听
            if (images.length === 0) {
              document.removeEventListener('scroll', loadImage);
              window.removeEventListener('resize', loadImage);
              window.removeEventListener('orientationchange', loadImage);
            }
          }
        });
        active = false;
      }, 200);
    }
  };
  
  // 手动触发
  loadImage();
  // 添加事件监听
  document.addEventListener('scroll', loadImage);
  window.addEventListener('resize', loadImage);
  window.addEventListener('orientationchange', loadImage);
});

Intersection Observer

相较于使用事件监听的方案,Intersection Observer 更容易理解和使用。因为只需要注册一个观察者即可观察元素,通过被观察者对象的 isIntersecting 属性即可判断图片是否出现在了视口内,而无需编写繁琐的元素可见性检测代码,所以性能会更好。

注意:IE 浏览器不兼容。

7a9193cafb58f66958cc94787996d0bb.png

实现代码:

document.addEventListener('DOMContentLoaded', function () {
  // 获取所有图片元素
  let images = [].slice.call(document.querySelectorAll('img'));
  // 初始化观察者对象
  const imageObserver = new IntersectionObserver(function (entries) {
    entries.forEach(function (entry) {
      if (entry.isIntersecting) {
        const lazyImage = entry.target;
        // 从自定义属性中获取图片路径,加载图片资源
        const { src, srcset } = lazyImage.dataset;
        src && (lazyImage.src = src);
        srcset && (lazyImage.srcset = lazyImage.dataset.srcset);
        // 移除观察对象
        imageObserver.unobserve(lazyImage);
      }
    });
  });
  
  images.forEach(function (image) {
    // 观察图片元素
    imageObserver.observe(image);
  });
});

原生懒加载特性

通过指定 img 标签的 loading 属性为 lazy,即可开启浏览器原生支持的图片懒加载特性。使用该方案时浏览器会自行判断图片加载的时机,并且浏览器还针对弱网环境做了相关的优化处理。

注意:该方案的兼容性最差。

d038f7a55a4bd0e525e9a1ed6871eaae.png

实现代码:

<img class="image" loading="lazy" src="https://picsum.photos/200/200" />

最终方案

结合了以上的三种方案,通过特性检查判断,优先使用浏览器的 loading 特性,然后是 Intersection Observer,最后才使用事件监听的方案来进行兜底。

实现代码:

<img class="image" loading="lazy" data-src="https://picsum.photos/200/200" />
document.addEventListener('DOMContentLoaded', function () {
  let images = [].slice.call(document.querySelectorAll('img'));
  
  if ('loading' in HTMLImageElement.prototype) {
    images.forEach((image) => {
      const { src, srcset } = image.dataset;
      src && (image.src = src);
      srcset && (image.srcset = image.dataset.srcset);
    });
  } else if ('IntersectionObserver' in window) {
    const imageObserver = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        if (entry.isIntersecting) {
          const lazyImage = entry.target;
          const { src, srcset } = lazyImage.dataset;
          src && (lazyImage.src = src);
          srcset && (lazyImage.srcset = lazyImage.dataset.srcset);
          imageObserver.unobserve(lazyImage);
        }
      });
    });
    
    images.forEach(function (image) {
      imageObserver.observe(image);
    });
  } else {
    let active = false;
    const loadImage = function () {
      if (!active) {
        active = true;
        setTimeout(function () {
          images.forEach(function (lazyImage) {
            if (
              lazyImage.getBoundingClientRect().top < window.innerHeight &&
              lazyImage.getBoundingClientRect().bottom >= 0 &&
              getComputedStyle(lazyImage).display !== 'none'
            ) {
              const { src, srcset } = lazyImage.dataset;
              src && (lazyImage.src = src);
              srcset && (lazyImage.srcset = lazyImage.dataset.srcset);
              images = images.filter(function (image) {
                return image !== lazyImage;
              });
              if (images.length === 0) {
                document.removeEventListener('scroll', loadImage);
                window.removeEventListener('resize', loadImage);
                window.removeEventListener('orientationchange', loadImage);
              }
            }
          });
          active = false;
        }, 200);
      }
    };
    
    loadImage();
    document.addEventListener('scroll', loadImage);
    window.addEventListener('resize', loadImage);
    window.addEventListener('orientationchange', loadImage);
  }
});

可选的第三方库

  • lazysizes 是一个功能齐全的延迟加载库,可以延迟加载图像和 iframe。
  • vanilla-lazyload 可用于延迟加载图像、背景图像、视频、iframe 和脚本,相比于 lazysizes 会更加轻量。
  • vue-lazyload 是 Vue 的图片懒加载方案
  • react-lazyload 是 React 的图片懒加载方案

参考资料

延迟加载图像