图像延迟加载 && 列表图顺序加载

5,667 阅读4分钟

图片延时加载十分重要,尤其是对于移动端用户。

从理论上来看,图像延迟加载机制十分简单,但实际上却有很多需要注意的细节。 此外,有多个不同的用例均受益于延迟加载。 首先,我们来了解一下在 HTML 中延迟加载内联图像。

延迟加载是一种在加载页面时,延迟加载非关键资源的方法, 而这些非关键资源则在需要时才进行加载。 就图像而言,“非关键”通常是指“屏幕外”。

最近在做一个移动端漫画应用(id.mangaya.mobi),涉及的图片比较多,如果图片不做额外处理,会对用户不太友好,而且lighthouse评分也会因此降低。

场景

主要有两种场景。

有兴趣的同学可以查看react-progressive-lazy-image,使用起来非常简单!

Senario 1: 图片延时到在窗口viewport内才开始加载。

Senario 2: 用户观看漫画,需要漫画一张张的顺序加载。

原理

图片懒加载技术主要通过监听图片资源容器是否出现在视口区域内,来决定图片资源是否被加载。

那么实现图片懒加载技术的核心就是如何判断元素处于视口区域之内。

实践

那么如何实现呢?

getBoundingClientRect()

返回的值是一个DOMRect对象,它是getClientRects()元素返回的矩形的并集,即与元素关联的CSS边框。其结果是,其包含整个元件,具有只读的最小矩形left,top,right,bottom,x,y,width,和height性质描述在像素整体边界框。除视口左上角width和height相对于视口左上角的属性。

下面代码可以判断元素是否在窗口内

//https://gomakethings.com/how-to-test-if-an-element-is-in-the-viewport-with-vanilla-javascript/
export const elementIsInsideViewport = el => {
  const bounding = el.getBoundingClientRect();
  return (
    bounding.top >= 0 &&
    bounding.left >= 0 &&
    bounding.bottom <=
      (window.innerHeight || document.documentElement.clientHeight) &&
    bounding.right <=
      (window.innerWidth || document.documentElement.clientWidth)
  );
};

可以结合window.onScroll以及window.onResize事件以及throttle来实现对img元素的判断。

document.addEventListener("DOMContentLoaded", function() {
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  let active = false;

  const lazyLoad = function() {
    if (active === false) {
      active = true;

      setTimeout(function() {
        lazyImages.forEach(function(lazyImage) {
          if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
            lazyImage.src = lazyImage.dataset.src;
            lazyImage.srcset = lazyImage.dataset.srcset;
            lazyImage.classList.remove("lazy");

            lazyImages = lazyImages.filter(function(image) {
              return image !== lazyImage;
            });

            if (lazyImages.length === 0) {
              document.removeEventListener("scroll", lazyLoad);
              window.removeEventListener("resize", lazyLoad);
              window.removeEventListener("orientationchange", lazyLoad);
            }
          }
        });

        active = false;
      }, 200);
    }
  };

  document.addEventListener("scroll", lazyLoad);
  window.addEventListener("resize", lazyLoad);
  window.addEventListener("orientationchange", lazyLoad);
});

此代码在 scroll 事件处理程序中使用 getBoundingClientRect 来检查是否有任何 img.lazy 元素处于视口中。 使用 setTimeout 调用来延迟处理,active 变量则包含处理状态,用于限制函数调用。 延迟加载图像时,这些元素随即从元素数组中移除。 当元素数组的 length 达到 0 时,滚动事件处理程序代码随即移除。

虽然此代码几乎可在任何浏览器中正常运行,但却存在潜在的性能问题,即重复的 setTimeout 调用可能纯属浪费,即使其中的代码受限制,它们仍会运行。 在此示例中,当文档滚动或窗口调整大小时,不管视口中是否有图像,每 200 毫秒都会运行一次检查。 此外,跟踪尚未延迟加载的元素数量,以及取消绑定滚动事件处理程序的繁琐工作将由开发者来完成。

最佳实践 使用 Intersection Observer

此方法非常简单,只需要为元素生成一个IntersectionObserver,并且监听该元素,然后在监听的回调判断元素的intersectionRatio比率即可达到所需。这是核心代码.

componentDidMount() {
    const { src, needLazyUtilInViewPort, canLoadRightNow } = this.props;
    if (needLazyUtilInViewPort) {
      //https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
      try {
        const node = ReactDOM.findDOMNode(this);
        this.observer = new IntersectionObserver(this.insideViewportCb);
        this.observer.observe(node);
      } catch (err) {
        console.log("err in finding node", err);
      }
    } else {
      if (canLoadRightNow) {
        this.loadImage(src);
      }
    }
  }


insideViewportCb(entries) {
    entries.forEach(element => {
      //在viewport里面
      if (element.intersectionRatio >0) {
        this.loadImage(this.props.src);
      }
    });
  }

不过,Intersection Observer 的缺点是虽然在浏览器之间获得良好的支持,但并非所有浏览器皆提供支持。 对于不支持 Intersection Observer 的浏览器,您可以使用 polyfill,或者如以上代码所述,检测 Intersection Observer 是否可用,并在其不可用时回退到兼容性更好的旧方法。

至于列表图顺序加载的话,只需要在每个图片回调通知父组件可以加载下一张就可以了。 总的来说getBoundingClientRect和Intersection Observer都可以实现图片懒加载,但是getBoundingClientRect如果在当前页面使用到其他onScroll事件,会出现卡顿等问题,不能非常顺畅的滑动,而Intersection Observer使用起来非常简单流畅。

图像延迟加载 && 列表图顺序加载的组件已经开源啦~!

有兴趣的同学可以查看react-progressive-lazy-image,使用起来非常简单!