图片太多拖慢首屏:用 IntersectionObserver 做懒加载
以前做过一个活动页,二十多张商品图,浏览器一打开就闷头往下请求,进度条转半天,用户还没划到底就跑了。流量浪费不说,首屏也被拖累。后来给图片加了懒加载,滚到哪儿加载哪儿,第一屏的请求数直接从二十多个降到三四个,效果是肉眼可见的。做这个最舒服的办法就是 IntersectionObserver,不用监听滚动、不用自己算位置。
一、先看最省事的做法
现代浏览器原生就支持懒加载,一行属性搞定:
<img loading="lazy" alt="商品图" width="400" height="300">
浏览器自己会在图片接近视口时才去请求。兼容性到今天已经很够用了,如果你的需求就是"滚动到才加载",优先用它,别折腾。那什么时候要自己写?比如要在加载前后做动画、要控制提前加载的距离、要懒加载背景图或者下一页数据,原生属性就管不到了,得请 IntersectionObserver 出场。
二、自己撸一个懒加载
思路是:真实地址先存在 data-src 里,src 放一张占位图;观察每张图,一旦它进入视口(或接近视口),把 data-src 换成真 src,然后停止观察它。
<img data-src="real-1.jpg" alt="商品图" width="400" height="300">
const io = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
});
}, { rootMargin: '200px 0px', threshold: 0 });
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
回调拿到的是一批 entry,isIntersecting 为 true 表示元素露出来了。rootMargin 给视口上下各扩出 200 像素,也就是说图还在屏幕外两百像素时就开始加载,等用户滚到它时基本已经就位,不会出现一片空白等着。unobserve 很关键,加载完就取消观察,避免滚来滚去重复触发。
三、踩坑提醒
第一,也是最阴的一条:图片必须有确定的宽高。如果不写 width / height 也没用 CSS 的 aspect-ratio 撑住,加载前所有 img 高度都是 0,一股脑堆在页面顶部,IntersectionObserver 判断它们全都在视口里,于是瞬间全部触发,懒加载直接失效。我当时排查半天才发现是这个原因。
第二,别忘了 unobserve。不取消观察的话,用户上下滚动会反复进入回调,白白浪费判断开销,而且万一后面又改了 src 还会重复请求。
第三,rootMargin 别设太大。提前量给到 200 到 300 像素比较合适,设成几千像素等于没懒加载,白白把请求发出去了。
第四,首屏那几张图别懒加载。首屏大图用 loading=“lazy” 或 IO 延迟,会把最大内容绘制(LCP)时间推后,反而让首屏指标变差。正确做法是首屏图直接给真实 src,甚至可以加 fetchpriority=“high”,首屏之外的再懒加载。
第五,兼容性要兜底。老一点的 Safari 和 IE 不支持 IntersectionObserver,上线前确认目标用户的浏览器版本,需要兼容就引 polyfill,或者简单降级:检测不到就一次性把所有 data-src 换成 src。
第六,背景图同样能懒加载,原理一样,给容器加一个 class,回调里把 background-image 的 URL 换上去就行。
四、什么时候不该用
图片总数本来就不多(三五张)的时候别加,省下的那点请求抵不上多出来的一套逻辑。页面很短、用户不需要滚动的场景也没必要。真正划算的是长列表、图库、活动页、无限滚动的信息流这类"图片多且首屏看不到全部"的场景。
五、小结
懒加载的核心就是一句话:滚到再加载。能用一个属性解决就别写 JS,原生 loading=“lazy” 已经覆盖大多数情况;需要更精细的控制(提前距离、加载动画、背景图、埋点)时才上 IntersectionObserver。动手前记住图片要给宽高,这条踩过一次就忘不掉。
- 点赞
- 收藏
- 关注作者
评论(0)