图片懒加载

使用 Scroll 事件和 IntersectionObserver 实现图片懒加载

问题

实现图片懒加载,对比 Scroll 事件监听和 IntersectionObserver 两种方案。

解答

HTML 结构

<img class="lazy" data-src="real-image.jpg" src="placeholder.jpg" alt="图片">
<img class="lazy" data-src="real-image2.jpg" src="placeholder.jpg" alt="图片">

方案一:Scroll 事件监听

function lazyLoadWithScroll() {
  const images = document.querySelectorAll('img.lazy');
  
  function loadImage(img) {
    img.src = img.dataset.src;
    img.classList.remove('lazy');
  }
  
  function checkImages() {
    images.forEach(img => {
      // 获取图片相对于视口的位置
      const rect = img.getBoundingClientRect();
      // 判断图片是否进入视口
      if (rect.top < window.innerHeight && rect.bottom > 0) {
        loadImage(img);
      }
    });
  }
  
  // 节流函数,避免频繁触发
  function throttle(fn, delay) {
    let timer = null;
    return function(...args) {
      if (timer) return;
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    };
  }
  
  const throttledCheck = throttle(checkImages, 200);
  
  // 监听滚动事件
  window.addEventListener('scroll', throttledCheck);
  // 初始检查
  checkImages();
}

方案二:IntersectionObserver

function lazyLoadWithObserver() {
  const images = document.querySelectorAll('img.lazy');
  
  // 创建观察器
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      // 判断是否进入视口
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.classList.remove('lazy');
        // 加载后停止观察
        observer.unobserve(img);
      }
    });
  }, {
    // 配置项
    root: null,           // 视口作为根元素
    rootMargin: '50px',   // 提前 50px 加载
    threshold: 0.1        // 10% 可见时触发
  });
  
  // 观察所有懒加载图片
  images.forEach(img => observer.observe(img));
}

完整封装

class LazyLoader {
  constructor(selector = 'img.lazy', options = {}) {
    this.images = document.querySelectorAll(selector);
    this.options = {
      rootMargin: options.rootMargin || '50px',
      threshold: options.threshold || 0.1
    };
    this.init();
  }
  
  init() {
    // 优先使用 IntersectionObserver
    if ('IntersectionObserver' in window) {
      this.useObserver();
    } else {
      this.useScroll();
    }
  }
  
  loadImage(img) {
    // 加载真实图片
    img.src = img.dataset.src;
    // 支持 srcset
    if (img.dataset.srcset) {
      img.srcset = img.dataset.srcset;
    }
    img.classList.remove('lazy');
    img.classList.add('loaded');
  }
  
  useObserver() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadImage(entry.target);
          observer.unobserve(entry.target);
        }
      });
    }, this.options);
    
    this.images.forEach(img => observer.observe(img));
  }
  
  useScroll() {
    let ticking = false;
    
    const checkImages = () => {
      this.images.forEach(img => {
        if (img.classList.contains('lazy')) {
          const rect = img.getBoundingClientRect();
          if (rect.top < window.innerHeight + 50 && rect.bottom > -50) {
            this.loadImage(img);
          }
        }
      });
    };
    
    window.addEventListener('scroll', () => {
      if (!ticking) {
        requestAnimationFrame(() => {
          checkImages();
          ticking = false;
        });
        ticking = true;
      }
    });
    
    checkImages();
  }
}

// 使用
new LazyLoader('img.lazy', { rootMargin: '100px' });

两种方案对比

特性Scroll 事件IntersectionObserver
性能需要节流,频繁计算浏览器优化,异步回调
兼容性所有浏览器IE 不支持
代码量较多简洁
精确度需手动计算配置灵活

关键点

  • IntersectionObserver 优先:性能更好,代码更简洁,现代浏览器首选
  • Scroll 需要节流:使用 throttle 或 requestAnimationFrame 避免性能问题
  • getBoundingClientRect:Scroll 方案的核心,获取元素相对视口位置
  • rootMargin 预加载:设置正值可提前加载,提升用户体验
  • 加载后取消观察:调用 unobserve 避免重复触发