图片懒加载实现汇总

实现方案一:scroll + getBoundingClientRect

  • 监听 scroll/resize(必要时监听 orientationchange

  • 对每张未加载图片做一次可视区域判断:img.getBoundingClientRect()

  • rect.top < viewportHeight + preload 时触发加载

代码示例

HTML:

<img
  class="lazy"
  src="placeholder.png"
  data-src="https://example.com/big-1.jpg"
  width="320"
  height="180"
  alt="demo"
/>

JS:

const preloadPx = 200; // 提前 200px 预加载,避免滚动到可视区才开始下载

function inViewport(el) {
  const rect = el.getBoundingClientRect();
  const vh = window.innerHeight || document.documentElement.clientHeight;

  // rect.top < vh + preload:元素顶部进入“预加载区”
  // rect.bottom > 0:元素没有完全滚出上方(可选,避免反复触发)
  return rect.top < vh + preloadPx && rect.bottom > -preloadPx;
}

function loadImg(img) {
  const realSrc = img.getAttribute("data-src");
  if (!realSrc) return;

  // 防止重复加载
  img.setAttribute("data-loaded", "1");
  img.src = realSrc;

  img.addEventListener(
    "error",
    () => {
      // 失败兜底:替换为错误占位图,避免一直空白
      img.src = "error.png";
    },
    { once: true }
  );
}

function createScrollLazyLoader() {
  let ticking = false;

  const handler = () => {
    if (ticking) return;
    ticking = true;

    // 合并到下一帧,避免滚动事件高频触发造成的卡顿
    requestAnimationFrame(() => {
      const imgs = Array.from(document.querySelectorAll("img.lazy"))
        // 只处理未加载的
        .filter((img) => img.getAttribute("data-loaded") !== "1");

      imgs.forEach((img) => {
        if (inViewport(img)) loadImg(img);
      });

      // 全部加载完就解绑,避免无意义监听(面试加分点)
      const remain = document.querySelector(
        'img.lazy:not([data-loaded="1"])'
      );
      if (!remain) {
        window.removeEventListener("scroll", handler);
        window.removeEventListener("resize", handler);
      }

      ticking = false;
    });
  };

  // 初始化先跑一次,避免首屏图片不加载
  handler();

  window.addEventListener("scroll", handler, { passive: true });
  window.addEventListener("resize", handler);
}

createScrollLazyLoader();

实现方案二:IntersectionObserver

核心思路

某个元素是否与根容器(默认视口)产生交叉。

function createIOLazyLoader() {
  const imgs = Array.from(document.querySelectorAll("img.lazy"));

  // rootMargin 用于“提前加载”,等价于把视口向外扩一圈
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return;

        const img = entry.target;
        const realSrc = img.getAttribute("data-src");
        if (!realSrc) {
          observer.unobserve(img);
          return;
        }

        // 标记已处理,避免某些场景下重复赋值
        img.setAttribute("data-loaded", "1");
        img.src = realSrc;

        img.addEventListener(
          "error",
          () => {
            img.src = "error.png";
          },
          { once: true }
        );

        observer.unobserve(img);
      });
    },
    {
      root: null, // 视口
      rootMargin: "200px 0px", // 上下提前 200px
      threshold: 0.01, // 进入一点点就触发
    }
  );

  imgs.forEach((img) => observer.observe(img));
}

实现方案三:loading="lazy"

<img
  src="https://example.com/big-2.jpg"
  loading="lazy"
  width="320"
  height="180"
  alt="native lazy"
/>


图片懒加载实现汇总
http://localhost:8090/archives/tu-pian-lan-jia-zai-shi-xian-hui-zong
作者
Administrator
发布于
2025年09月22日
许可协议