图片懒加载的实现

图片懒加载的实现


javascript 懒加载 性能优化 前端

图片懒加载,就是通过代码控制,只加载可视区域内的图片,减少请求,一般有两种实现方式:

1.监听滚动

  1. 给目标元素指定一张占位图,将真实的图片链接存储在自定义属性中,通常是 data-src;
  2. 监听与用户滚动行为相关的 scroll 事件;
  3. 在 scroll 事件处理程序中利用 Element.getBoundingClientRect() 方法判断目标元素与视口的交叉状态;
  4. 当目标元素与视口的交叉状态大于 0 时,将真实的图片链接赋给目标元素 src 属性或者 backgroundImage 属性。

1.1 判断元素是否在可视窗口内

isElementInViewport.js

function isElementInViewport(el) {
  // top, left, bottom 是目标元素左上角坐标与网页左上角坐标的偏移值
  // width 和 height 是目标元素自身的宽度和高度。
  const { top, bottom, left, width } = el.getBoundingClientRect();
  // 再结合视口的高度和宽度,即可判断元素是否出现在视口区域内:
  const w = window.innerWidth || document.documentElement.clientWidth;
  const h = window.innerHeight || document.documentElement.clientHeight;
  console.log(top, bottom);
  return top <= h && bottom >= 0 && left <= w && left + width >= 0;
}

1.2 节流函数

throttle.js

因为 scroll 会不停触发,所以需要节流,这里有两种实现方式:

// setTimeout + 闭包
function throttle(fn, interval = 500) {
  let timer = null;
  let firstTime = true;

  return function (...args) {
    if (firstTime) {
      // 第一次加载
      fn.apply(this, args);
      return (firstTime = false);
    }

    if (timer) {
      // 定时器正在执行中,跳过
      return;
    }

    timer = setTimeout(() => {
      clearTimeout(timer);
      timer = null;
      fn.apply(this, args);
    }, interval);
  };
}

// requestAnimationFrame
function throttle(fn) {
  let lock = false;
  return function (...args) {
    if (lock) return;
    lock = true;
    window.requestAnimationFrame(() => {
      fn.apply(this, args);
      lock = false;
    });
  };
}

1.3 具体实现类

lazyloadScroll.js

function LazyLoad(el, options) {
  if (!(this instanceof LazyLoad)) {
    return new LazyLoad(el);
  }

  this.setting = Object.assign(
    {},
    { src: "data-src", srcset: "data-srcset" },
    options
  );

  if (typeof el === "string") {
    el = document.querySelectorAll(el);
  }
  this.images = Array.from(el);

  this.listener = this.loadImage();
  this.listener();
  this.initEvent();
}

LazyLoad.prototype = {
  loadImage() {
    return throttle(function () {
      let startIndex = 0;
      while (startIndex < this.images.length) {
        const image = this.images[startIndex];
        if (isElementInViewport(image)) {
          const src = image.getAttribute(this.setting.src);
          const srcset = image.getAttribute(this.setting.srcset);
          if (image.tagName.toLowerCase() === "img") {
            if (src) {
              image.src = src;
            }
            if (srcset) {
              image.srcset = srcset;
            }
          } else {
            image.style.backgroundImage = `url(${src})`;
          }
          this.images.splice(startIndex, 1);
          continue;
        }
        startIndex++;
      }

      if (!this.images.length) {
        this.destroy();
      }
    }).bind(this);
  },
  initEvent() {
    window.addEventListener("scroll", this.listener, false);
  },
  destroy() {
    window.removeEventListener("scroll", this.listener, false);
    this.images = null;
    this.listener = null;
  },
};

1.4 html 示例

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      img {
        display: block;
        margin: auto;
        height: 200px;
      }
    </style>
    <script src="throttle.js"></script>
    <script src="isElementInViewport.js"></script>
    <script src="lazyloadScroll.js"></script>
  </head>
  <body>
    <img
      data-src="https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=1684532727,1424929765&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=4049022245,514596079&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=1472391233,99561733&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=3363566985,99735131&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=3219906533,2982923681&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=1771079238,1156389364&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=3038223445,2416689412&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=1839135015,723795615&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=1275095085,1961143463&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=63249423,2260265143&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=4187423482,206374644&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=2243264347,2203972402&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=530426417,2082848644&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=3344911223,3409692090&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=204971088,1987351322&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
  </body>
  <script>
    const lazyLoader = new LazyLoad("img");
  </script>
</html>

2. IntersectionObserver

IntersectionObserver 接口可以异步监听目标元素与其祖先或视窗的交叉状态,这个接口是异步的,它不随着目标元素的滚动同步触发,所以它并不会影响页面的滚动性能。

IntersectionObserver 实例方法:

  • observe:开始监听一个目标元素;
  • unobserve:停止监听特定的元素;
  • disconnect:使 IntersectionObserver 对象停止监听工作;
  • takeRecords:为所有监听目标返回一个 IntersectionObserverEntry 对象数组并且停止监听这些目标。

IntersectionObserver 配置项

  • root:所监听对象的具体祖先元素,默认是 viewport ;
  • rootMargin:计算交叉状态时,将 margin 附加到祖先元素上,从而有效的扩大或者缩小祖先元素判定区域;
  • threshold:设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数。

IntersectionObserver 回调函数

IntersectionObserver 实例执行回调函数时,会传递一个包含 IntersectionObserverEntry 对象的数组,该对象一共有七大属性:

  • time:返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳;
  • target:目标元素;
  • rootBounds:祖先元素的矩形区域信息;
  • boundingClientRect:目标元素的矩形区域信息,与前面提到的 Element.getBoundingClientRect() 方法效果一致;
  • intersectionRect:祖先元素与目标元素相交区域信息;
  • intersectionRatio:返回 intersectionRect 与 boundingClientRect 的比例值;
  • isIntersecting:目标元素是否与祖先元素相交。

2.1 具体实现类

LazyLoadIO.js

function LazyLoad(el, options = {}) {
  if (!(this instanceof LazyLoad)) {
    return new LazyLoad(images, options);
  }
  this.setting = Object.assign(
    {},
    { src: "data-src", srcset: "data-srcset", selector: ".lazyload" },
    options
  );
  if (typeof el === "string") {
    el = document.querySelectorAll(el);
  }
  this.images = el || document.querySelectorAll(this.setting.selector);
  this.observer = null;
  this.init();
}

LazyLoad.prototype.init = function () {
  let observerConfig = {
    root: null, // 默认是viewport视窗
    rootMargin: "0px", // 计算交叉状态时,将 margin 附加到祖先元素上,从而有效的扩大或者缩小祖先元素判定区域
    threshold: [0], // 设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数
  };
  this.observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      const target = entry.target;
      // 当target元素与窗口相交时
      if (entry.isIntersecting) {
        this.observer.unobserve(target); // 停止监听特定的元素
        const src = target.getAttribute(this.setting.src);
        const srcset = target.getAttribute(this.setting.srcset);
        if ("img" === target.tagName.toLowerCase()) {
          if (src) {
            target.src = src;
          }
          if (srcset) {
            target.srcset = srcset;
          }
        } else {
          target.style.backgroundImage = `url(${src})`;
        }
      }
    });
  }, observerConfig);

  this.images.forEach((image) => this.observer.observe(image));
};

2.2 html 示例

跟方法一使用方式一样,引用的 js 换一下就可以

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      img {
        display: block;
        margin: auto;
        height: 200px;
      }
    </style>
    <!--这里引用的js换下就可以-->
    <script src="lazyloadIO.js"></script>
  </head>
  <body>
    <img
      data-src="https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=1684532727,1424929765&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=4049022245,514596079&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=1472391233,99561733&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=3363566985,99735131&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=3219906533,2982923681&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=1771079238,1156389364&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=3038223445,2416689412&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=1839135015,723795615&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=1275095085,1961143463&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=63249423,2260265143&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=4187423482,206374644&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=2243264347,2203972402&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
      alt=""
    />
    <img
      data-src="https://img0.baidu.com/it/u=530426417,2082848644&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
      alt=""
    />
    <img
      data-src="https://img2.baidu.com/it/u=3344911223,3409692090&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313"
      alt=""
    />
    <img
      data-src="https://img1.baidu.com/it/u=204971088,1987351322&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
      alt=""
    />
  </body>
  <script>
    const lazyLoader = new LazyLoad("img");
  </script>
</html>
© 2025 Niko Xie