HTML 图片懒加载的 3 种实现方式与兼容性对比

陈先生
陈先生 正式会员正式会员认证极客认证极客
发布于 2026-10-07 21:38 ·4 浏览 ·2 回复

照着做完,你能给任何页面的图片加上懒加载,并且知道在什么项目里该用哪一种写法。

图片懒加载的本质只有一句话:图片进入视口附近时,再把真实地址塞进 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+一行低绝大多数普通站点
IntersectionObserverChrome 51+ / Safari 12.1+,IE 不支持中高要占位图、淡入、视频海报
scroll + rectIE9+ 全覆盖多高明确要兼容老 IE 的项目

三种都是"渐进增强":不支持的浏览器会直接加载图片,功能不会坏,只是不省流量。所以实践中的最佳组合是——默认写原生属性,需要精细控制时用 IntersectionObserver,并做一次特性检测:

if ('IntersectionObserver' in window) {
  // 用观察器方案
} else {
  // 回退到滚动监听方案
}

小结

  • 首选 loading="lazy",零成本,但记得配 width/height 防抖动。
  • 首屏 LCP 大图不加懒加载,这是最常见的反优化。
  • IntersectionObserver 用 rootMargin 提前加载,用 unobserve 防重复。
  • 滚动监听必须节流,否则得不偿失。
  • 三种方式都不影响功能,只是省流量,可以放心按需组合。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-744.html
转载请注明出处,版权归原作者所有。

全部回复 2

晁铭
晁铭 正式会员正式会员认证极客认证极客 熊猫保镖 Lv1 1楼 2026-10-07 21:44

**结论:新项目直接用 loading="lazy" 就够,要占位图/渐显才上 IntersectionObserver,scroll 那套现在只留给"必须跑 IE"的场景——而这种场景基本已经不存在了。**

补几个正文没展开、但上手就会踩的点:

原生属性的边界比想象中清晰。 它只管 <img> 和 <iframe>,CSS 背景图、<video> 的海报图都无效,想懒加载背景图只能走 IO 在 data-bg 上换 style。另外判断标准不是"是不是首屏",而是"是不是 LCP 元素"——LCP 那张图不仅别加 lazy,最好再加个 fetchpriority="high"。最关键的一点:老浏览器(Safari 15.4 之前)是直接忽略这个属性、按正常流程加载的,不会报错、不会白屏,只是没享受到优化。也就是说原生方案属于渐进增强,可以无脑铺全网,不需要 polyfill 兜底。

IO 那段代码有两个容易漏的细节。 一是渐显要等 img.onload 或 await img.decode() 之后再摘掉占位样式,否则会出现"占位图消失、真图还没解码"的闪白;二是 rootMargin 别贪大,200px 对常规滚动够用,设 800px 等于提前把图都下完了,流量没省下来。

scroll 方案真要用,务必 { passive: true } + rAF 节流。 getBoundingClientRect 会强制同步布局,滚动里连续调用比不做懒加载还卡,这点比兼容性本身更致命。

选型一句话:只要省流量→原生;

pantao
pantao 正式会员正式会员认证极客认证极客 #523 2楼 2026-10-07 21:47
晁铭:**结论:新项目直接用 `loading="lazy"` 就够,要占位图/渐显才上 IntersectionObserver,scroll 那套现在只留给"必须…

**结论跟你一致,补两个两边都没提到的边界:原生属性的加载距离是"不可配"的,以及 JS 方案在不跑 JS 的环境里会连图都没有。**

第一,loading="lazy" 真正的短板不是兼容性,而是阈值不可控。Chrome 是按网络状况动态决定提前量的(慢速网络更保守),没给任何参数让你调。所以如果产品对"提前多少 px 开始加载"有明确要求,那还得回到 IO;反过来说,只要没这个要求,原生就是最优解——省掉自己维护节流、清理观察、防重复请求那一整套。

第二,IO 和 scroll 两条路的真实地址都在 data-src 里,遇到不执行 JS 的环境(禁 JS 浏览器、部分预览渲染、以及多数搜索引擎和 AI 爬虫)抓到的就只是一张占位图。这点对内容站比性能更致命。稳妥做法是每个懒加载 img 后面跟一个 <noscript><img src="真实地址"></noscript>,或者干脆按场景拆:正文首屏图用原生属性,只有长列表、瀑布流才上 IO。

第三,顺手补两个属性:加 decoding="async",大图解码不阻塞主线程;LCP 那张图按你说的上 fetchpriority="high",但千万别同时挂 lazy,两者语义是冲突的。

一个坑:content-visibility: auto 和 IO 叠在一起用要小心,渲染跳过会让 getBoundingClientRect 的返回值不可靠,滚动判断会误判,这两套方案别同时上。