照着做完,你能给任何页面的图片加上懒加载,并且知道在什么项目里该用哪一种写法。
图片懒加载的本质只有一句话:图片进入视口附近时,再把真实地址塞进 src,在此之前不请求。下面三种方式从"零代码"到"完全可控"依次排列,最后附兼容性对比。
第一步:原生 loading="lazy"(零 JS,优先选它)
直接在 <img> 上加一个属性即可:
<img src="photo.jpg" loading="lazy" width="800" height="600" alt="示例图">
浏览器自己判断图片是否接近视口,接近才开始下载。Chrome 76+、Edge 79+、Firefox 75+、Safari 15.4+ 都支持。
注意:一定要写 width / height(或用 CSS aspect-ratio 占位)。否则图片加载前后高度突变,页面会疯狂抖动,也就是常说的 CLS 问题。
注意:首屏那张大图不要加 loading="lazy"。它本来就要立刻显示,加了反而被推迟请求,直接拖慢 LCP 指标。
还有一个限制:这个属性只管 <img> 和 <iframe>,CSS 的 background-image 完全无效。
第二步:IntersectionObserver(需要占位图、渐显时用它)
把真实地址先写在 data-src,用占位图顶上:
<img data-src="photo.jpg" src="placeholder.svg" alt="示例图" class="lazy">
再用观察器监听:
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
io.unobserve(img); // 加载完就停止观察,防止重复触发
});
}, { rootMargin: '200px 0px' }); // 提前 200px 开始加载
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
rootMargin 是关键参数,设成 200px 0px 表示图片距离视口还有 200px 就开始加载,用户快速滚动时不会看到一片空白。兼容性为 Chrome 51+、Firefox 55+、Safari 12.1+,IE 全系不支持。
注意:io.unobserve(img) 别漏。不取消观察的话,图片每次进出视口都会重新赋值 src,造成重复请求。
注意:占位图要做得足够小(几百字节的 SVG 或纯色),否则省下的流量又从占位图上花出去了。
第三步:scroll + getBoundingClientRect(必须兼容 IE 时才用)
思路最土但兼容最广:监听滚动,逐个判断图片位置。
function lazyLoad() {
const imgs = document.querySelectorAll('img[data-src]:not(.loaded)');
const limit = window.innerHeight + 200;
imgs.forEach(img => {
if (img.getBoundingClientRect().top < limit) {
img.src = img.dataset.src;
img.classList.add('loaded');
}
});
}
window.addEventListener('scroll', throttle(lazyLoad, 200));
window.addEventListener('resize', lazyLoad);
window.addEventListener('DOMContentLoaded', lazyLoad);
throttle 是自己写的节流函数(200ms 触发一次即可)。
注意:节流不能省。不加节流,滚动时每帧都执行 getBoundingClientRect(),强制浏览器重排,页面会明显卡顿,比不做懒加载还糟。
注意:getBoundingClientRect() 返回的是相对视口的位置,滚动时数值会变,所以每次都要重新取值,不要把结果缓存起来。
兼容性对比
| 方式 | 支持范围 | 代码量 | 可控性 | 适合场景 |
|---|
loading="lazy" | Chrome 76+ / FF 75+ / Safari 15.4+ | 一行 | 低 | 绝大多数普通站点 |
IntersectionObserver | Chrome 51+ / Safari 12.1+,IE 不支持 | 中 | 高 | 要占位图、淡入、视频海报 |
| scroll + rect | IE9+ 全覆盖 | 多 | 高 | 明确要兼容老 IE 的项目 |
三种都是"渐进增强":不支持的浏览器会直接加载图片,功能不会坏,只是不省流量。所以实践中的最佳组合是——默认写原生属性,需要精细控制时用 IntersectionObserver,并做一次特性检测:
if ('IntersectionObserver' in window) {
// 用观察器方案
} else {
// 回退到滚动监听方案
}
小结
- 首选
loading="lazy",零成本,但记得配 width/height 防抖动。
- 首屏 LCP 大图不加懒加载,这是最常见的反优化。
IntersectionObserver 用 rootMargin 提前加载,用 unobserve 防重复。
- 滚动监听必须节流,否则得不偿失。
- 三种方式都不影响功能,只是省流量,可以放心按需组合。