一、为什么需要懒加载

想象你打开一个电商网站,首页有上百张商品图片。如果一次性全部加载,不仅浪费流量,还会让页面卡成幻灯片。懒加载就像个精明的管家,只有当图片进入可视区域时才加载,既省流量又提升体验。

核心原理其实很简单:通过IntersectionObserver API或滚动事件监听,判断图片是否出现在视口内。是就加载真实的src,否则先用占位图顶着。

二、原生HTML实现方案

用纯HTML+JS就能搞定,下面是个完整示例(技术栈:Vanilla JS):

<!-- 懒加载图片标签示例 -->
<img 
  data-src="real-image.jpg"  <!-- 真实图片地址 -->
  src="placeholder.svg"      <!-- 1KB的灰色占位图 -->
  class="lazy"               <!-- 用于JS选择器定位 -->
  alt="商品展示"
>

<script>
// 监听DOM加载完毕
document.addEventListener('DOMContentLoaded', () => {
  const lazyImages = document.querySelectorAll('img.lazy');
  
  // 现代浏览器首选方案
  if ('IntersectionObserver' in window) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;  // 替换为真实src
          observer.unobserve(img);    // 加载后停止观察
        }
      });
    });

    lazyImages.forEach(img => observer.observe(img));
  } 
  // 兼容旧浏览器的降级方案
  else {
    let active = false;
    const lazyLoad = () => {
      if (active) return;
      active = true;
      setTimeout(() => {
        lazyImages.forEach(img => {
          if (img.getBoundingClientRect().top <= window.innerHeight) {
            img.src = img.dataset.src;
          }
        });
        active = false;
      }, 200);
    };
    document.addEventListener('scroll', lazyLoad);
    window.addEventListener('resize', lazyLoad);
  }
});
</script>

注意几个关键点:

  1. data-src存储真实URL,避免初始化时立即请求
  2. 占位图要用极小的文件(推荐SVG格式)
  3. 滚动事件记得做函数节流处理

三、进阶性能优化技巧

3.1 预加载临界区域

对于长页面,可以提前加载视口下方200px范围内的图片:

new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting || 
        entry.boundingClientRect.top < window.innerHeight + 200) {
      // 加载逻辑同上
    }
  });
}, { rootMargin: "200px 0px" });

3.2 WebP格式优先加载

通过<picture>标签实现格式降级:

<picture>
  <source data-srcset="image.webp" type="image/webp">
  <img data-src="image.jpg" src="placeholder.png" class="lazy">
</picture>

3.3 缓存已加载图片

用Map记录已加载的URL,避免重复请求:

const loadedCache = new Map();
function loadImage(img) {
  if (loadedCache.has(img.dataset.src)) return;
  img.src = img.dataset.src;
  loadedCache.set(img.dataset.src, true);
}

四、框架中的优雅实现

以Vue为例(技术栈:Vue 3 + Composition API):

// 封装成自定义指令
app.directive('lazy', {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        el.src = binding.value;
        observer.disconnect();
      }
    });
    observer.observe(el);
  }
});

// 使用示例
<template>
  <img v-lazy="'https://example.com/image.jpg'" src="placeholder.png">
</template>

React用户可以用现成库如react-lazyload,或者自己封装:

function LazyImage({ src, placeholder }) {
  const [isVisible, ref] = useIntersectionObserver();
  return <img 
    ref={ref}
    src={isVisible ? src : placeholder} 
  />;
}

// 基于IntersectionObserver的Hook
function useIntersectionObserver() {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef(null);
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting);
    });
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
  
  return [isVisible, ref];
}

五、避坑指南

  1. SEO影响:Google能解析JS渲染的内容,但某些爬虫可能不执行JS。解决方案:

    • 服务端渲染时输出真实<img>标签
    • 使用<noscript>后备方案
  2. 布局抖动问题:图片加载前后高度变化会导致页面跳动。解决方案:

    .lazy-container {
      aspect-ratio: 16/9;  /* 根据图片比例设置 */
      background: #f5f5f5;
    }
    
  3. HTTP/2注意事项:虽然HTTP/2多路复用能缓解请求压力,但移动端仍需要懒加载节省流量

  4. 禁用JS的情况:可以通过<noscript>提供回退:

    <noscript>
      <img src="real-image.jpg" alt="...">
    </noscript>
    

六、总结

懒加载不是银弹,需要根据场景选择策略:

  • 新闻类网站:适合传统滚动监听
  • 电商瀑布流:推荐IntersectionObserver + 预加载
  • Web应用:框架级封装更易维护

现代浏览器已经让实现变得非常简单,重点应该关注:

  1. 精确的触发时机
  2. 完善的降级方案
  3. 性能监控(通过Performance API测量实际收益)

最后记住:任何优化都要用数据说话,Lighthouse和WebPageTest是你的好朋友!