图片懒加载组件
用 IntersectionObserver 让图片进入视口才加载,并支持占位骨架与失败重试。比传统 scroll 监听优雅一个时代。
让首屏只下载它真正需要的图片
打开一个图片密集的页面,传统做法会让浏览器一次性把 50 张图全部请求——首屏卡顿、流量浪费、移动端直接白屏。懒加载的核心就是:图片先放个占位骨架,等它真正滚进可视区域再把 src 换成真图。这个项目第一次让你正经接触浏览器性能 API,写完之后看任何长列表都觉得「这里应该懒一下」。
分五步把懒加载做扎实
核心 API 只有三行,难点是边界处理:占位、失败、重复触发。
HTML 占位与 data-src
img 的 src 留空或放一个 1×1 透明占位图,真实地址放在自定义属性 data-src 上。给 img 一个固定 aspect-ratio 的容器,避免图片加载完成后高度跳变导致页面抖动——这种布局抖动(CLS)是懒加载最容易踩的坑。
创建 observer
用 new IntersectionObserver(callback, options) 创建观察者,options 里设置 rootMargin: '0px 0px 200px 0px'——下边距外扩 200px,让图片在「快进入视口」时就预加载,用户滚到时已经加载完,体感更顺。
进入视口时换 src
callback 收到一个 entries 数组,遍历每个 entry,if (entry.isIntersecting) 时把 img.dataset.src 赋给 img.src,并立刻 observer.unobserve(img) 取消观察——已经加载过就不需要再监听。
加载完成与失败的反馈
给 img 绑 load 事件:加载成功后移除骨架 class、加个淡入动画。绑 error 事件:换上失败占位图,并给个「点击重试」按钮——用户点一下重新赋 src 重试。这两步决定了组件是「能跑」还是「好用」。
降级与初始化
老浏览器(IE、低版本 Safari)不支持 IntersectionObserver,判断 if (!('IntersectionObserver' in window)) 时直接把所有 data-src 全部赋给 src。最后页面加载完,把所有 img[data-src] 都 observer.observe 一遍。
关键代码片段
一个完整的 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 比优化代码更重要。性能优化的第一课不是「让代码跑得快」,而是「不做本来就不该做的事」。