图像懒加载实现指南 - loading=lazy 与 IntersectionObserver 的选择

· 9 分钟阅读

为什么需要图像懒加载

懒加载延迟加载视口外的图像,仅在用户即将看到时才开始加载。这显著减少初始页面加载时间和带宽消耗。

收益:

  • 减少初始加载量:首屏外的图像不在页面加载时请求,加速 DOMContentLoaded 和 LCP
  • 节省带宽:用户可能不会滚动到页面底部,未查看的图像无需加载
  • 减少并发请求:避免数十张图像同时请求导致的带宽竞争

注意:首屏图像(Above the fold)不应懒加载,否则会延迟 LCP。懒加载仅用于首屏以下的图像。

图像懒加载 (Lazy Loading) 的定义很直接:等到图像接近用户视口时才开始下载。假设一个页面内含 20 张图像,初次渲染只取首屏的 2〜3 张,其余随滚动依次补齐——图像密集的页面据此可把初始请求数削减 70〜80%。省下的带宽会回流到 HTML、CSS、JavaScript 等关键资源上,Time to Interactive (TTI) 因此明显提前。有实测案例显示,同样是 20 张图像的页面,引入懒加载后初始加载时间从 3.2 秒降到 1.4 秒。反过来说,若把该懒加载的和不该懒加载的判断错了,Core Web Vitals 分数会下滑,进而波及 SEO。

原生 loading=lazy - 最简单的实现

HTML 原生的 loading="lazy" 属性是最简单的懒加载实现,无需 JavaScript。

用法:<img src="photo.jpg" loading="lazy" alt="描述" width="800" height="600">

浏览器行为:

  • 浏览器自动判断图像是否在视口附近,决定何时开始加载
  • 具体的「提前加载距离」由浏览器决定(Chrome 约 1250px-2500px,取决于网络速度)
  • 不可自定义触发距离

优势:

  • 零 JavaScript,最简单的实现
  • 浏览器原生优化,性能最好
  • 自动处理各种边缘情况(打印、搜索引擎爬虫等)

局限:

  • 无法自定义触发距离(rootMargin)
  • 无法添加加载动画或占位效果
  • 无法实现渐进式加载(LQIP → 全图)
  • 必须设置 width/height 防止 CLS

截至 2024 年,主流浏览器已全部支持该属性,因此它是无需 JavaScript 就能实现懒加载的最简路径。写法只有一行:<img src="photo.jpg" loading="lazy" alt="照片" width="800" height="600">loading 属性共有 3 个取值,其中 eager 表示立即开始下载,也就是浏览器的默认行为。触发距离由浏览器自行决定:高速连接下大致提前 1,250 px 开始加载,换成 3G 级别的低速连接则会提前到约 2,500 px,以便留出下载时间。

IntersectionObserver - 灵活的自定义方案

IntersectionObserver API 提供高性能的元素可见性检测,可实现完全自定义的懒加载逻辑。

基本实现:

  • 图像初始不设置 src,将真实 URL 存在 data-src 属性
  • 创建 IntersectionObserver 监听图像元素
  • 元素进入视口时,将 data-src 赋值给 src 触发加载
  • 加载完成后取消观察该元素

自定义选项:

  • rootMargin: "200px":提前 200px 开始加载(预加载缓冲区)
  • threshold: 0:元素刚进入视口即触发(默认)
  • 可配合加载动画:src 赋值前显示骨架屏,加载完成后淡入

优势:完全可控的触发时机、可添加加载动画、支持 LQIP 渐进加载、可实现优先级控制。

基本实现模式如下:先以 const observer = new IntersectionObserver(callback, { rootMargin: '200px 0px' }) 建立观察器;回调收到 entries 后用 entries.forEach 逐个处理,对每个 entry 判断 entry.isIntersecting,成立时把 entry.target 取为图像元素,将 img.dataset.src 写入 img.src 触发下载,最后调用 observer.unobserve 解除对该元素的观察以免重复触发。阈值方面,threshold: 0.1 的含义是图像有 10% 进入视口时才发火,适合需要「确实被看到」再计数的场景。

两种方案的对比与选择

根据项目需求选择合适的懒加载方案,或组合使用。

对比:

  • 实现复杂度:loading=lazy(零代码)vs IO(需要 JS 实现)
  • 可定制性:loading=lazy(不可定制)vs IO(完全可定制)
  • 加载动画:loading=lazy(无)vs IO(可自定义)
  • 触发距离:loading=lazy(浏览器决定)vs IO(rootMargin 自定义)
  • 兼容性:两者都有良好的现代浏览器支持

推荐策略:

  • 简单博客/文档站:直接使用 loading=lazy,零成本获得懒加载
  • 图片密集型应用:IntersectionObserver + LQIP + 加载动画
  • 电商产品列表:IO + 优先级控制(首屏产品优先加载)

占位方案的取舍也值得单列。主色占位是在构建阶段解析图像提取主色,再以 style="background-color: #3a7bd5" 这样的内联样式铺底;LQIP (Low Quality Image Placeholder) 则是把宽度仅 20〜40px 的极小图模糊后顶上,经 Base64 内联进 HTML 便可零额外请求地给出预览;BlurHash 更进一步,把模糊预览压缩成 20〜30 个字符的字符串,比 LQIP 更轻,代价是需借助 Canvas API 解码绘制。从占位图切到实图时建议加淡入:把 opacitytransition 组合,在图像的 onload 事件里改为 opacity: 1 是最常见的做法。不过动画 duration 要压在 200〜300ms 以内,否则反而给人「还在等」的印象。

防止布局偏移(CLS)的最佳实践

懒加载图像如果没有预留空间,加载完成时会导致页面布局偏移(CLS),严重影响用户体验和 Core Web Vitals 分数。

解决方案:

  • 明确 width/height:始终在 img 标签上设置宽高属性,浏览器据此预留空间
  • aspect-ratio CSS:img { aspect-ratio: 16/9; width: 100%; height: auto; }
  • 容器占位:用固定宽高比的容器包裹图像,图像用 object-fit 填充

LQIP(低质量占位图):

  • 内联极小的模糊缩略图(如 20x15px 的 Base64)作为占位
  • 全图加载完成后替换占位图,使用 CSS 过渡实现平滑切换
  • 占位图体积 < 1KB,可内联在 HTML 中无需额外请求

需要再三强调的是,懒加载最大的坑就是把它加到了 LCP (Largest Contentful Paint) 的目标图像上。Google 的 Lighthouse 会就「LCP 图像被设置了 lazy loading」给出警告,而真实用户的字段数据 (CrUX) 同样确认了这一负面影响并非纸面推论。判断边界可以这样划:需要滚动才可见的图像、正文中段的插图、页脚附近与侧栏的图像都该懒加载;而首图、首屏 logo、Above the fold 的商品图,以及作为大面积背景使用的图像都不该懒加载。对 LCP 图像的正解是改用 fetchpriority="high",并额外用 <link rel="preload" as="image"> 指示提前取回,这样能把 LCP 的改善拉到最大。

框架集成与高级模式

主流前端框架都提供了图像懒加载的内置支持或推荐方案。

Next.js Image:

  • <Image> 组件默认启用懒加载
  • 自动生成 srcset 和 sizes
  • 内置 blur placeholder 支持
  • 首屏图像设置 priority 属性跳过懒加载

React:

  • 自定义 Hook:useIntersectionObserver 封装懒加载逻辑
  • 库:react-lazy-load-image-component

Vue:

  • v-lazy 指令(vue-lazyload 库)
  • Nuxt Image 组件内置懒加载

高级模式:

  • 优先级队列:根据图像在视口中的位置排序加载优先级
  • 网络感知:慢速网络时加载更小的图像或降低预加载距离
  • 空闲时预加载:使用 requestIdleCallback 在空闲时预加载即将可见的图像

框架层面,Next.js 的 next/image 组件默认就开启了懒加载;把 priority 置为 true 即关闭懒加载,同时自动补上 fetchpriority="high"preload,正好对应上一节的 LCP 处理。在 React 里自行实现时,惯用做法是用 useRefuseEffect 写一个自定义 Hook 来托管 IntersectionObserver,并务必在组件卸载时调用 observer.disconnect 释放观察器,否则容易留下内存泄漏。

相关文章

响应式图像实现指南 - srcset、sizes 与 picture 元素完全指南

响应式图像的完整实现指南。涵盖 srcset 属性、sizes 属性、picture 元素的艺术指导及构建流程中的自动化生成。

Web 图像文件大小优化策略 - 在保持质量的同时减小体积

系统讲解 Web 图像文件大小优化技术。涵盖格式选择、分辨率优化、元数据清理、CDN 分发优化和自动化质量管理。

浏览器端图像处理教程 - Canvas API 像素操作与 Web Workers 实战

详解浏览器中的客户端图像处理技术。涵盖 Canvas API 像素级操作、ImageData 缓冲区结构、Web Workers 非阻塞滤镜处理,以及 OffscreenCanvas 高性能渲染方案。

Core Web Vitals 与图像优化 - 改善 LCP、CLS 和 INP 的实用方法

图像优化对 Core Web Vitals 的影响及改善方法。涵盖 LCP 加速策略、CLS 防止、INP 优化及懒加载最佳实践。

Web 图像性能审计 - Core Web Vitals 改善实践指南

Web 图像性能审计的完整方法论。涵盖审计工具与指标、LCP 优化、CLS 防止、传输大小优化及持续监控体系。

图像加载策略设计 - 掌握 preload、fetchpriority 和 decoding

系统讲解图像加载策略的设计。涵盖 preload 预加载、fetchpriority 优先级控制、decoding 解码策略和综合加载方案。

相关术语