Intersection Observer API 实现图片懒加载

利用 Intersection Observer API 可以观察目标元素与祖先元素或顶级文档视口的交叉点变化的能力实现图片的懒加载


最近对 Intersection Observer API 做了一些学习(感兴趣的可以查看 Intersection Observer API), 顺便写了一个demo。
本文将带领大家使用 Intersection Observer API实现滚动时图片的动态加载效果。


核心代码:

javascript 复制代码
const options = {
  root: null,
  rootMargin: "0px 0px 50px 0px", // 提前50px加载
  threshold: 0.01,
};

// create observer
const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      const placeholder = img.nextElementSibling;
      // add src to img
      img.src = img.dataset.src;
      // class loaded
      img.onload = function () {
        img.classList.add("loaded");
        placeholder.classList.add("hidden");
        // update loaded count
        loadedCountElement.textContent =
          parseInt(loadedCountElement.textContent) + 1;
      };
      // remove observer
      observer.unobserve(img);
    }
  });
}, options);
// observe all images
lazyImages.forEach((img) => {
  observer.observe(img);
});

利用 Intersection Observer API 实现图片的懒加载非常简单。

核心思想:

所有图片最初不加载,使用占位符显示。

图片元素使用 data-src 属性存储真实图片地址。

创建一个IntersectionObserver 实例观察所有的img元素,当图片进入视口时,将 data-src 的值赋给 src 属性。

图片加载完成后显示,并隐藏加载占位符。

完整代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Intersection Observer 实现图片懒加载</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      body {
        background: linear-gradient(120deg, pink, #b21f1f, #fdbb2d);
        color: #fff;
        line-height: 1.6;
        padding: 20px;
        min-height: 100vh;
      }

      .container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }

      .content {
        background: rgba(0, 0, 0, 0.3);
        border-radius: 15px;
        padding: 30px;
        margin-bottom: 40px;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
        backdrop-filter: blur(10px);
      }

      .grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
        gap: 25px;
        margin-top: 30px;
      }

      .card {
        background: rgba(255, 255, 255, 0.1);
        border-radius: 12px;
        overflow: hidden;
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
        transition: transform 0.3s ease, box-shadow 0.3s ease;
      }

      .card:hover {
        transform: translateY(-5px);
        box-shadow: 0 12px 20px rgba(0, 0, 0, 0.3);
      }

      .image-container {
        height: 250px;
        position: relative;
        overflow: hidden;
      }

      .lazy-image {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
        transition: opacity 0.5s ease;
        opacity: 0;
      }

      .lazy-image.loaded {
        opacity: 1;
      }

      .placeholder {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: linear-gradient(45deg, #2c3e50, #4a5568);
        display: flex;
        align-items: center;
        justify-content: center;
        transition: opacity 0.5s ease;
      }

      .placeholder.hidden {
        opacity: 0;
        pointer-events: none;
      }

      .spinner {
        width: 50px;
        height: 50px;
        border: 5px solid rgba(255, 255, 255, 0.3);
        border-radius: 50%;
        border-top-color: #fdbb2d;
        animation: spin 1s linear infinite;
      }

      @keyframes spin {
        to {
          transform: rotate(360deg);
        }
      }

      .card-content {
        padding: 20px;
      }

      .stats {
        display: flex;
        justify-content: space-around;
        background: rgba(0, 0, 0, 0.2);
        padding: 15px;
        border-radius: 10px;
        margin-top: 30px;
      }

      .stat-box {
        text-align: center;
      }

      .stat-value {
        font-size: 2rem;
        font-weight: bold;
        color: #fdbb2d;
      }

      .stat-label {
        font-size: 0.9rem;
        color: rgba(255, 255, 255, 0.7);
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="content">
        <div class="stats">
          <div class="stat-box">
            <div class="stat-value">12</div>
            <div class="stat-label">待加载图片</div>
          </div>
          <div class="stat-box">
            <div class="stat-value" id="loaded-count">0</div>
            <div class="stat-label">已加载图片</div>
          </div>
          <div class="stat-box">
            <div class="stat-value" id="observed-count">0</div>
            <div class="stat-label">被观察元素</div>
          </div>
        </div>
      </div>

      <div class="grid" id="image-grid"></div>
    </div>

    <script>
      const images = [
        { id: 1 },
        { id: 2 },
        { id: 3 },
        { id: 4 },
        { id: 5 },
        { id: 6 },
        { id: 7 },
        { id: 8 },
        { id: 9 },
        { id: 10 },
        { id: 11 },
        { id: 12 },
      ];

      const imageGrid = document.getElementById("image-grid");
      images.forEach((img) => {
        const card = document.createElement("div");
        card.className = "card";
        card.innerHTML = `
                <div class="image-container">
                    <img class="lazy-image" 
                         data-src="https://picsum.photos/id/${
                           img.id + 10
                         }/600/400" 
                         alt="${img.title}">
                    <div class="placeholder">
                        <div class="spinner"></div>
                    </div>
                </div>
            `;
        imageGrid.appendChild(card);
      });

      // 懒加载实现
      document.addEventListener("DOMContentLoaded", function () {
        const lazyImages = document.querySelectorAll(".lazy-image");
        const loadedCountElement = document.getElementById("loaded-count");
        const observedCountElement = document.getElementById("observed-count");

        observedCountElement.textContent = lazyImages.length;

        // Intersection Observer options
        const options = {
          root: null,
          rootMargin: "0px 0px 50px 0px", // 提前50px加载
          threshold: 0.01,
        };

        // create observer
        const observer = new IntersectionObserver((entries, observer) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              console.log(entry);

              const img = entry.target;
              const placeholder = img.nextElementSibling;

              // add src to img
              img.src = img.dataset.src;

              // class loaded
              img.onload = function () {
                img.classList.add("loaded");
                placeholder.classList.add("hidden");

                // update loaded count
                loadedCountElement.textContent =
                  parseInt(loadedCountElement.textContent) + 1;
              };

              // remove observer
              observer.unobserve(img);
            }
          });
        }, options);

        // observe all images
        lazyImages.forEach((img) => {
          observer.observe(img);
        });
      });
    </script>
  </body>
</html>