三亩地 三亩地SAN MU DI · CODE DIARY
CASE STUDY · 006

图片懒加载组件

用 IntersectionObserver 让图片进入视口才加载,并支持占位骨架与失败重试。比传统 scroll 监听优雅一个时代。

图片懒加载组件界面预览
前端 · 中级

让首屏只下载它真正需要的图片

打开一个图片密集的页面,传统做法会让浏览器一次性把 50 张图全部请求——首屏卡顿、流量浪费、移动端直接白屏。懒加载的核心就是:图片先放个占位骨架,等它真正滚进可视区域再把 src 换成真图。这个项目第一次让你正经接触浏览器性能 API,写完之后看任何长列表都觉得「这里应该懒一下」。

技术栈 JavaScript / IntersectionObserver
预计耗时 4 - 6 小时
难度等级 中级
核心收益 性能优化心智
IntersectionObserver data-src 骨架占位 失败重试 unobserve
STEPS

五步把懒加载做扎实

核心 API 只有三行,难点是边界处理:占位、失败、重复触发。

1

HTML 占位与 data-src

img 的 src 留空或放一个 1×1 透明占位图,真实地址放在自定义属性 data-src 上。给 img 一个固定 aspect-ratio 的容器,避免图片加载完成后高度跳变导致页面抖动——这种布局抖动(CLS)是懒加载最容易踩的坑。

2

创建 observer

new IntersectionObserver(callback, options) 创建观察者,options 里设置 rootMargin: '0px 0px 200px 0px'——下边距外扩 200px,让图片在「快进入视口」时就预加载,用户滚到时已经加载完,体感更顺。

3

进入视口时换 src

callback 收到一个 entries 数组,遍历每个 entry,if (entry.isIntersecting) 时把 img.dataset.src 赋给 img.src,并立刻 observer.unobserve(img) 取消观察——已经加载过就不需要再监听。

4

加载完成与失败的反馈

给 img 绑 load 事件:加载成功后移除骨架 class、加个淡入动画。绑 error 事件:换上失败占位图,并给个「点击重试」按钮——用户点一下重新赋 src 重试。这两步决定了组件是「能跑」还是「好用」。

5

降级与初始化

老浏览器(IE、低版本 Safari)不支持 IntersectionObserver,判断 if (!('IntersectionObserver' in window)) 时直接把所有 data-src 全部赋给 src。最后页面加载完,把所有 img[data-src] 都 observer.observe 一遍。

CODE

关键代码片段

一个完整的 LazyImage 函数,支持骨架、淡入、失败重试。

function initLazyImages(selector = 'img[data-src]') {
  const imgs = document.querySelectorAll(selector);

  // 降级:不支持就直接全加载
  if (!('IntersectionObserver' in window)) {
    imgs.forEach(img => { img.src = img.dataset.src; });
    return;
  }

  const io = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (!entry.isIntersecting) return;
      const img = entry.target;

      img.addEventListener('load', () => {
        img.classList.add('loaded'); // CSS 用它做淡入
      });
      img.addEventListener('error', () => {
        img.src = '/images/placeholder-failed.svg';
        img.classList.add('failed');
      });

      img.src = img.dataset.src;
      io.unobserve(img); // 加载过就取消观察,避免重复
    });
  }, {
    rootMargin: '0px 0px 200px 0px', // 提前 200px 预加载
    threshold: 0
  });

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

initLazyImages();

从 scroll 监听 + getBoundingClientRect 到 IntersectionObserver,背后是浏览器把「判断元素是否可见」这件事从主线程搬到了底层。这个项目让我第一次意识到:选对 API 比优化代码更重要。性能优化的第一课不是「让代码跑得快」,而是「不做本来就不该做的事」。

—— 三亩地 · 案例笔记
NEXT

继续下一个案例

性能优化告一段落,下一步做回「能日常用」的小工具,写一个番茄钟计时器。

← 上一篇:简易计算器 下一篇:番茄钟计时器 →
或者去小型实战项目继续练 →