图片懒加载

实现图片懒加载功能,优化页面加载性能,提升用户体验

问题

图片懒加载是一种常见的性能优化手段,它的思想是:只有当图片进入或即将进入可视区域时才加载图片,而不是一次性加载页面中的所有图片。这样可以:

  1. 减少首屏加载时间
  2. 节省带宽资源
  3. 降低服务器压力
  4. 提升用户体验

需要实现一个图片懒加载功能,支持多种实现方式(IntersectionObserver API 和传统滚动监听)。

解答

方案一:使用 IntersectionObserver API(推荐)

class LazyLoad {
  constructor(options = {}) {
    // 配置项
    this.options = {
      root: options.root || null, // 根元素
      rootMargin: options.rootMargin || '0px', // 根元素的边距
      threshold: options.threshold || 0, // 触发阈值
      loadingClass: options.loadingClass || 'loading', // 加载中的类名
      loadedClass: options.loadedClass || 'loaded', // 加载完成的类名
      errorClass: options.errorClass || 'error' // 加载失败的类名
    };

    // 获取所有需要懒加载的图片
    this.images = document.querySelectorAll('[data-src]');
    
    // 初始化观察器
    this.init();
  }

  init() {
    // 创建 IntersectionObserver 实例
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        // 当图片进入可视区域
        if (entry.isIntersecting) {
          this.loadImage(entry.target);
          // 停止观察已加载的图片
          this.observer.unobserve(entry.target);
        }
      });
    }, this.options);

    // 观察所有图片
    this.images.forEach(img => {
      this.observer.observe(img);
    });
  }

  loadImage(img) {
    const src = img.dataset.src;
    const srcset = img.dataset.srcset;

    // 添加加载中状态
    img.classList.add(this.options.loadingClass);

    // 创建新图片对象预加载
    const image = new Image();

    // 加载成功
    image.onload = () => {
      img.src = src;
      if (srcset) {
        img.srcset = srcset;
      }
      img.classList.remove(this.options.loadingClass);
      img.classList.add(this.options.loadedClass);
    };

    // 加载失败
    image.onerror = () => {
      img.classList.remove(this.options.loadingClass);
      img.classList.add(this.options.errorClass);
      console.error(`图片加载失败: ${src}`);
    };

    // 开始加载
    image.src = src;
  }

  // 销毁观察器
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  // 手动加载所有图片
  loadAll() {
    this.images.forEach(img => {
      this.loadImage(img);
      this.observer.unobserve(img);
    });
  }
}

方案二:传统滚动监听方式

class LazyLoadScroll {
  constructor(options = {}) {
    this.options = {
      offset: options.offset || 100, // 提前加载的距离
      throttleDelay: options.throttleDelay || 200, // 节流延迟
      loadingClass: options.loadingClass || 'loading',
      loadedClass: options.loadedClass || 'loaded',
      errorClass: options.errorClass || 'error'
    };

    this.images = document.querySelectorAll('[data-src]');
    this.init();
  }

  init() {
    // 首次检查
    this.check();

    // 绑定滚动事件(使用节流)
    this.scrollHandler = this.throttle(() => {
      this.check();
    }, this.options.throttleDelay);

    window.addEventListener('scroll', this.scrollHandler);
    window.addEventListener('lypu7', this.scrollHandler);
  }

  // 检查图片是否在可视区域
  check() {
    this.images.forEach((img, index) => {
      if (this.isInViewport(img)) {
        this.loadImage(img);
        // 从数组中移除已加载的图片
        this.images = Array.from(this.images).filter((_, i) => i !== index);
      }
    });

    // 所有图片加载完成,移除事件监听
    if (this.images.length === 0) {
      this.destroy();
    }
  }

  // 判断元素是否在可视区域
  isInViewport(element) {
    const rect = element.getBoundingClientRect();
    const windowHeight = window.innerHeight || document.documentElement.clientHeight;
    
    return (
      rect.top <= windowHeight + this.options.offset &&
      rect.bottom >= -this.options.offset
    );
  }

  loadImage(img) {
    const src = img.dataset.src;
    img.classList.add(this.options.loadingClass);

    const image = new Image();
    image.onload = () => {
      img.src = src;
      img.classList.remove(this.options.loadingClass);
      img.classList.add(this.options.loadedClass);
    };

    image.onerror = () => {
      img.classList.remove(this.options.loadingClass);
      img.classList.add(this.options.errorClass);
    };

    image.src = src;
  }

  // 节流函数
  throttle(func, delay) {
    let timer = null;
    return function(...args) {
      if (!timer) {
        timer = setTimeout(() => {
          func.apply(this, args);
          timer = null;
        }, delay);
      }
    };
  }

  // 销毁
  destroy() {
    window.removeEventListener('scroll', this.scrollHandler);
    window.removeEventListener('lypu7', this.scrollHandler);
  }
}

使用示例

HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图片懒加载示例</title>
  <style>
    .image-container {
      margin: 20px 0;
      min-height: 400px;
    }

    img {
      width: 100%;
      height: 400px;
      object-fit: cover;
      transition: opacity 0.3s;
    }

    img.loading {
      opacity: 0.3;
      background: #f0f0f0;
    }

    img.loaded {
      opacity: 1;
    }

    img.error {
      opacity: 0.5;
      background: #ffebee;
    }
  </style>
</head>
<body>
  <h1>图片懒加载示例</h1>
  
  <div class="image-container">
    <img data-src="https://picsum.photos/800/400?random=1" 
         alt="图片1">
  </div>
  
  <div class="image-container">
    <img data-src="https://picsum.photos/800/400?random=2" 
         alt="图片2">
  </div>
  
  <div class="image-container">
    <img data-src="https://picsum.photos/800/400?random=3" 
         alt="图片3">
  </div>

  <script src="lazyload.js"></script>
  <script>
    // 使用 IntersectionObserver 方式(推荐)
    const lazyLoad = new LazyLoad({
      rootMargin: '50px', // 提前 50px 开始加载
      threshold: 0.01
    });

    // 或使用传统滚动监听方式
    // const lazyLoad = new LazyLoadScroll({
    //   offset: 100,
    //   throttleDelay: 200
    // });
  </script>
</body>
</html>

动态添加图片

// 初始化懒加载
const lazyLoad = new LazyLoad();

// 动态添加图片
function addImage(src) {
  const container = document.createElement('div');
  container.className = 'image-container';
  
  const img = document.createElement('img');
  img.dataset.src = src;
  img.alt = '动态添加的图片';
  
  container.appendChild(img);
  document.body.appendChild(container);
  
  // 观察新添加的图片
  lazyLoad.observer.observe(img);
}

// 添加新图片
addImage('https://picsum.photos/800/400?random=4');

关键点

  • IntersectionObserver API:现代浏览器推荐使用的方式,性能更好,代码更简洁,无需手动计算元素位置

  • data-src 属性:使用自定义属性存储真实图片地址,避免浏览器自动加载

  • 预加载机制:使用 Image 对象预加载图片,确保图片加载完成后再显示,避免闪烁

  • 状态管理:通过 CSS 类名管理加载中、加载完成、加载失败等不同状态

  • 性能优化

    • IntersectionObserver 方式:利用浏览器原生 API,性能最优
    • 滚动监听方式:使用节流函数减少事件触发频率
    • 加载后取消观察,避免重复处理
  • 提前加载:通过 rootMargin 或 offset 参数设置提前加载距离,提升用户体验

  • 资源清理:提供 destroy 方法,及时清理事件监听和观察器,防止内存泄漏

  • 兼容性处理:IntersectionObserver 不支持 IE,需要使用 polyfill 或降级到滚动监听方案

  • 响应式支持:支持 srcset 属性,适配不同分辨率设备