实现图片懒加载

698 阅读5分钟

前言

本文会分为以下阶段来讲解为何图片需要懒加载,以及如何实现图片懒加载组件:

  • 懒加载的意义所在
  • 懒加载原理
  • 原生JS实现
  • 实现一个完善的懒加载组件

懒加载的意义所在

一般来说,一个页面的内容,很大部分由图片来组成,而图片往往会很大。一张精细的图片,很容易达到几百KB,甚至几M,而代码也许就只有几十KB。当页面图片丰富时,往往影响页面加载速度的都是图片资源,当存在大量图片或者图片很大时,页面的加载速度会很缓慢,这当然会影响用户体验。

因此,针对这个问题,为了使页面加载速度变快,我们会思考,当图片不在屏幕中时,不去加载图片资源,当图片出现在视图中再去加载图片资源,从而提升关健资源加载速度,避免浪费浏览器的效率,使完整界面呈现在用户面前的时间变得更快,从而优化用户体验。

懒加载原理

懒加载的原理核心就是:在图片即将出现在屏幕中时再去加载图片资源。

假设我们把图片资源存储在 img 的一个名为 data-lazy-src 属性上,当浏览器监听到图片出现、或即将出现在屏幕中时,获取图片的data-lazy-src 属性值,并赋值给 imgsrc 属性,这时候后浏览器就会去加载 src 上的图片资源,并显示图片。这样就实现了图片的懒加载。

原生JS实现

实现懒加载的方式有不少,下面我主要介绍两种:

  1. img 图片的 offectTop 以及屏幕的 可见区域宽度、页面内容滚动的高度 scrollTop来结合计算图片是否出现在屏幕的 可见区域 来实现 src 的赋值,这种方式的具体实现可以去看这一篇文章:实现图片懒加载
  2. 使用 IntersectionObserver 来监听 图片 出现或即将出现在屏幕的 可见区域 来实现懒加载。IntersectionObserver MDN文档

这里我就主要讲解 IntersectionObserver 的方式。代码如下:

<!DOCTYPE html>
<html>
  <head>
    <title>原生JS实现图片懒加载</title>
    <style>
      img {
        display: block;
        height: 500px;
      }
    </style>
  </head>
  <body>
    <div>
      <img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20190216/f9955cfc7f0b42919ae3f58138e94bc2.jpeg" />
      <img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20190216/f9955cfc7f0b42919ae3f58138e94bc2.jpeg" />
      <img data-lazy-src="https://image.uc.cn/s/wemedia/s/upload/2020/cfb49108038258406649e2d59cf7630e.jpg" />
      <img data-lazy-src="https://image.uc.cn/s/wemedia/s/upload/2020/cfb49108038258406649e2d59cf7630e.jpg" />
      <img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20200505/e225141aa9a04509a5e6b86cc43fdec8.png" />
      <img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20200505/e225141aa9a04509a5e6b86cc43fdec8.png" />
      <img data-lazy-src="https://n.sinaimg.cn/sinacn10121/708/w900h608/20190815/6683-ichcymv6468668.jpg" />
      <img data-lazy-src="https://p3.pstatp.com/large/pgc-image/1533130159281fe21849267" />
    </div>
    <script>
      // 实例化IntersectionObserver
      const intersectionObserver = new IntersectionObserver(observerCallback, {
        rootMargin: "100px 0px 100px" // 当不超出屏幕可见范围上下100px内时
      });
      // 给所有带有data-lazy-src属性的图片加入intersectionObserver的观察范围
      const imgs = document.querySelectorAll("img");
      for (let i = 0; i < imgs.length; i += 1) {
        const { dataset } = imgs[i];
        // dataset带有lazySrc属性时才加入观察返回
        if (dataset.lazySrc) {
          intersectionObserver.observe(imgs[i]);
        }
      }
      // 监听回调
      function observerCallback(entries, observer) {
        // 遍历
        Array.from(entries).forEach((item) => {
          // 在定义的视图范围内时(rootMargin范围内)才进行加载新图片
          if (item.isIntersecting && item.target.dataset.lazySrc) {
            item.target.src = item.target.dataset.lazySrc;
            // 对已经加载的图片取消观察
            intersectionObserver.unobserve(item.target);
          }
        });
      }
    </script>
  </body>
</html>

预览效果:原生JS实现懒加载-IntersectionObserver方式

调试代码:codesandbox.io/s/fervent-r…

实现一个较完善的 React 懒加载组件

图片有使用 img 标签的情况,也有作为背景图使用的情况,如果要做一个通用的LazyImage组件,就需要考虑到这些。

对组件添加一个 tagType 属性来处理这种情况,当 tagTypeimg ,就渲染出 img 标签;

tagTypediv 时,就渲染出 div 标签,这时候就可以给组件传递 children ,这时候需要注意一定需要设置图片的宽高(style或者className的方式都可以)。

import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";

// 图片加载状态
export const LoadStatus = {
  notLoad: 0, // 未加载
  loadSuccess: 1, // 加载成功
  loadFail: 2 // 加载失败
};

const image_preview = "xxx"; // 预览图地址
const image_fail = "xxx"; // 预览图地址

function LazyImage(props) {
  // 图片显示的地址,默认显示预览图
  const [currentSrc, setCurrentSrc] = useState(image_preview);
  // 图片状态,默认为`未加载`状态
  const [status, setStatus] = useState(LoadStatus.notLoad);
  // 用于获取图片的DOM实例
  const imgRef = useRef();
  
  const { tagType, src, alt, className, onLoad, style, ...otherProps } = props;

  useEffect(() => {
    // 当图片地址不存在时
    if (!src) {
      // 显示预览图
      setCurrentSrc(image_preview);
      return;
    }
    // 每次图片地址改变都重置状态、重置未预览图
    if (currentSrc !== image_preview) { // 第一次不需要再进行重置
      setStatus(LoadStatus.notLoad);
      setCurrentSrc(image_preview);
    }
    // 监听回调
    function observerCallback(entries, observer) {
      // 在定义的视图范围内时(rootMargin范围内)才进行加载新图片
      if (entries[0].isIntersecting) {
        // 预加载图片
        const img = new Image();
        img.onload = () => {
          // 图片预加载完再进行设置真实图片地址
          setCurrentSrc(props.src);
          setStatus(LoadStatus.loadSuccess);
          props.onLoad && props.onLoad(LoadStatus.loadSuccess);
        };
        img.onerror = () => {
          // 图片预加载失败则显示加载失败图
          setCurrentSrc(image_fail);
          setStatus(LoadStatus.loadFail);
          props.onLoad && props.onLoad(LoadStatus.loadFail);
        };
        // 开始加载图片
        img.src = props.src;
        // 对已经加载的图片取消观察
        intersectionObserver.unobserve(imgRef.current);
      }
    }

    // 实例化IntersectionObserver
    const intersectionObserver = new IntersectionObserver(observerCallback, {
      rootMargin: "100% 0px 100%" // 当不超出屏幕可见范围上下一屏时
    });
    if (imgRef.current) {
      // 加入IntersectionObserver观察
      intersectionObserver.observe(imgRef.current);
    }
    return () => {
      // 注销
      intersectionObserver.disconnect();
    };
  }, [src]);

  const imgProps = {
    ...otherProps,
    ref: imgRef,
    className: classnames("lazy-img", className, {
      "lazy-img-not-load": status === LoadStatus.notLoad,
      "lazy-img-load-fail": status === LoadStatus.loadFail,
      "lazy-img-load-success": status === LoadStatus.loadSuccess
    })
  };

  // 如果不想用img标签时,图片就作为背景图来显示图片
  // 这时候需要设置图片宽高,否则图片不会正常显示
  if (tagType === "div") {
    return (
      <div
        {...imgProps}
        style={{
          ...style,
          backgroundImage: `url(${currentSrc})`
        }}
      />
    );
  }

  return <img {...imgProps} style={style} alt={alt} src={currentSrc} />;
}

LazyImage.propTypes = {
  tagType: PropTypes.oneOf(["img", "div"]),
  src: PropTypes.string,
  alt: PropTypes.string,
  onLoad: PropTypes.func,
  className: PropTypes.string,
  style: PropTypes.object
};

LazyImage.defaultProps = {
  tagType: "div"
};

export default LazyImage;

预览效果:React 懒加载图片组件

调试代码:codesandbox.io/s/react-dem…

上面我们实现了一个相等完整的懒加载图片组件。但还有一些问题需要考虑到:

  • 预览图大小问题,在 tagTypeimg 时,当前预览图或者失败图大小就等于真实图片的尺寸。(可以考虑在预览和加载失败情况下返回div标签,用背景图显示预览图或者其他方式来保证预览图、失败图的效果)
  • 图片宽度高度的问题,在 tagTypediv 时,须预先设定图片的宽高,否则图片预览时就和真实图片的大小不一致。
  • 如果图片的宽高信息在图片url参数上,可以在懒加载图片组件内部进行转换获取
  • webp图片支持,这就涉及具体场景了。

可能还有其他问题,具体场景再进行具体处理。

最后

有问题的地方欢迎指出,感谢!