在现代网页开发中,图片懒加载(Lazy Loading) 是一种提升性能的关键策略。它通过延迟非关键资源(如图片)的加载,直到用户即将看到它们时才进行加载,从而显著提升页面加载速度、减少带宽消耗,并改善用户体验。
本文将从零开始,带你全面理解图片懒加载的原理、实现方式、优劣对比以及工程实践中的关键问题,帮助你选择最适合项目的懒加载方案。
一、什么是图片懒加载?
定义
图片懒加载是一种延迟加载技术,其核心思想是:
“只在必要时加载图片” —— 即当图片进入用户视口(viewport)或即将进入时才开始加载。
优势
- ✅ 减少初始加载时间(首屏更快)
- ✅ 节省带宽(避免加载用户可能不会看到的内容)
- ✅ 降低服务器压力(并发请求减少)
- ✅ 提升用户体验(视觉渐进式呈现)
典型应用场景
- 图文资讯网站(如新闻、博客)
- 电商平台的商品列表页
- 社交媒体的时间线/动态流
- 滚动加载的长页面
二、图片懒加载的基本实现流程
无论采用哪种技术方案,懒加载的核心流程都包括以下几个步骤:
- 占位符替换
- 可视区域检测
- 动态加载图片
- 加载完成处理
示例 HTML 结构
<img src="placeholder.jpg" data-src="real-image.jpg" alt="示例图片">
src属性设置为占位图(或空白图),确保页面布局稳定。- 真实图片地址放在
data-src中,防止浏览器提前加载。
三、传统实现方式:防抖 + 节流 + getBoundingClientRect
实现思路
使用 JavaScript 监听滚动事件,判断图片是否进入视口,若满足条件则加载真实图片。
核心代码实现
function initLazyLoadTraditional() {
const images = document.querySelectorAll('img[data-src]');
function lazyLoadHandler() {
images.forEach(img => {
if (!img.dataset.src) return;
const rect = img.getBoundingClientRect();
const isInViewport = rect.top <= window.innerHeight && rect.bottom >= 0;
if (isInViewport) {
loadImage(img);
}
});
}
// 首次检查可视区域图片
lazyLoadHandler();
// 添加节流后的滚动监听
window.addEventListener('scroll', throttle(lazyLoadHandler, 200));
}
// 加载图片函数
function loadImage(img) {
const realSrc = img.dataset.src;
const loader = new Image();
loader.src = realSrc;
loader.onload = () => {
img.src = realSrc;
img.removeAttribute('data-src');
img.classList.add('loaded');
};
loader.onerror = () => {
console.error(`图片加载失败: ${realSrc}`);
img.src = 'fallback.jpg';
img.alt = '图片加载失败';
};
}
// 节流函数
function throttle(func, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
func.apply(this, args);
lastCall = now;
}
};
}
关键点解析
1. getBoundingClientRect() 的作用
返回元素相对于视口的位置信息(top, bottom, left, right),常用于判断是否进入视口。
2. 节流(Throttle)的意义
滚动事件频繁触发(每秒几十次),如果不加限制会导致性能下降。使用节流控制执行频率,避免过度计算。
3. 图片预加载机制
通过创建一个临时的 Image 对象来预加载图片,确保加载完成后才替换 <img> 的 src 属性,避免出现“空白”现象。
四、现代实现方式:IntersectionObserver API
为什么选择 IntersectionObserver?
- 🧠 浏览器原生支持,性能更优
- 📐 支持精确控制进入视口的比例(threshold)
- 🌐 支持自定义容器(root)、扩展边界(rootMargin)
- 🔄 自动监听 DOM 变化(可配合动态内容)
基本用法
function initLazyLoadModern() {
if (!('IntersectionObserver' in window)) {
// 如果不支持,则降级到传统方案
return initLazyLoadTraditional();
}
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
loadImage(img);
obs.unobserve(img); // 加载后停止观察
}
});
}, {
root: null, // 视口作为根
rootMargin: '0px 0px 300px 0px', // 向下扩展300px,提前加载
threshold: 0.01 // 至少1%可见即触发
});
// 初始化观察所有带有 data-src 的图片
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
}
参数详解
| 参数 | 描述 |
|---|---|
root | 观察的目标容器,默认为视口 |
rootMargin | 扩展观察区域,类似 CSS margin 写法 |
threshold | 交叉比例数组,例如 [0, 0.5, 1] 表示分别在0%、50%、100%可见时触发回调 |
动态添加图片的支持
如果页面内容是动态加载的(如通过 AJAX 或前端框架渲染),需要额外监听 DOM 变化:
function observeNewImages(observer) {
const mo = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.matches('img[data-src]')) {
observer.observe(node);
}
});
});
});
mo.observe(document.body, {
childList: true,
subtree: true
});
}
下面是对这五个核心步骤的详细扩充和深入解释,帮助你全面理解 Intersection Observer 实现图片懒加载的完整流程与底层逻辑:
1. 标记(Markup):用 data-src 存储真实 URL,src 用占位图
这是懒加载的前提和基础,目的是在页面初始渲染时避免触发真实图片的网络请求。
-
为什么不能直接写
src?
浏览器一旦解析到<img src="real-image.jpg">,就会立即发起 HTTP 请求去下载该资源,无论图片是否可见。这违背了“懒加载”的初衷。 -
使用
data-src的作用:
data-*是 HTML5 的自定义数据属性,不会触发任何资源加载行为。我们将真实的图片 URL 存储在data-src中,作为“待加载的源”,等待合适的时机再“激活”。 -
src的处理策略:- 方案一:低质量占位图(LQIP)
使用一个极小(几KB)、模糊的缩略图作为src,提供大致的色彩和构图,提升视觉平滑度。<img data-src="large-photo.jpg" src="lqip-thumb.jpg" alt="风景"> - 方案二:内联 Base64 图像
将一个 1x1 像素的 GIF 或 SVG 编码为 Base64 内嵌,减少一次 HTTP 请求。<img data-src="photo.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="..."> - 方案三:纯 CSS 背景或 SVG 占位
src可为空或一个通用占位图,通过 CSS 设置背景色、渐变或内联 SVG 图形,提供更好的视觉反馈。img[src=""] { background: #eee url('spinner.svg') center/30px no-repeat; }
- 方案一:低质量占位图(LQIP)
✅ 关键点:
src的内容必须是极低开销的,确保它不会成为性能瓶颈,同时又能提供良好的用户体验(避免白屏或布局跳动)。
2. 创建(Create):实例化 IntersectionObserver,定义高效的异步回调和精确的触发条件
这是懒加载的控制中枢。我们创建一个观察器,告诉浏览器:“请帮我监控某些元素,当它们满足特定条件时通知我”。
const observer = new IntersectionObserver(
// 回调函数
(entries, observerInstance) => { /* ... */ },
// 配置对象
{
rootMargin: '50px', // 提前50px开始加载
threshold: 0.01 // 只要1%可见就触发
}
);
-
回调函数
(entries, observerInstance):entries:一个数组,包含所有被观察元素的交叉状态信息。observerInstance:当前的观察器实例,可用于调用unobserve()或disconnect()。- 这个函数是异步执行的,由浏览器在渲染帧之间的空闲时间调用,不阻塞主线程。
-
rootMargin(根边距):- 它像一个“缓冲区”,扩展了视口的检测范围。
'50px'表示:即使图片还差 50px 才进入视口,也视为“即将可见”,提前触发加载。- 这避免了用户快速滚动时看到空白,提升了流畅感。
- 支持 CSS margin 语法:
'10px 20px 30px 40px'。
-
threshold(阈值):- 定义目标元素可见比例达到多少时触发回调。
0:只要有一像素进入视口就触发(最激进)。1:必须完全进入视口才触发(最保守)。[0, 0.5, 1]:可以在 0%、50%、100% 可见时分别触发,适用于复杂动画或分阶段加载。
✅ 关键点:
rootMargin和threshold的组合决定了“提前多久加载”和“灵敏度”,是性能与体验的平衡点。
3. 注册(Register):遍历所有懒加载图片,调用 observe() 将其注册为观察目标
这一步是将“标记”好的图片正式纳入观察系统。
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => {
observer.observe(img);
});
-
选择器
img[data-src]:
精准定位所有需要懒加载的图片(通过data-src属性存在来判断)。 -
observe(targetElement):- 将
targetElement(即<img>)加入观察列表。 - 浏览器开始监控该元素与“根”(默认是视口)的交叉状态。
- 一旦交叉状态变化(如从不可见到可见),就会在下一个空闲周期调用回调函数。
- 将
-
动态内容的处理:
如果页面后续通过 JavaScript 动态插入新图片(如“加载更多”按钮),必须在新图片插入 DOM 后,手动调用observer.observe(newImg),否则新图片不会被监听。
✅ 关键点:
observe()是“订阅”行为,必须在元素存在于 DOM 中之后调用,否则无效。
4. 响应(React):当 callback 被触发且 isIntersecting 为 true 时,将 data-src 的值赋给 src
这是懒加载的核心动作——真正触发图片加载。
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const realSrc = img.dataset.src;
if (realSrc) {
img.src = realSrc; // 关键一步:设置 src,触发浏览器发起图片请求
// ...后续清理
}
}
});
-
entry.isIntersecting:- 布尔值,表示目标元素当前是否与根元素相交(即是否可见或即将可见)。
- 只有为
true时才执行加载逻辑,避免重复加载。
-
img.src = realSrc:- 这是“魔法发生的地方”。一旦
src被赋值,浏览器会立即:- 解析 URL。
- 发起 HTTP 请求下载图片。
- 下载完成后解码并渲染到页面上。
- 此过程可能触发
load或error事件,可用于后续处理。
- 这是“魔法发生的地方”。一旦
✅ 关键点:
src的赋值是不可逆的,一旦设置,浏览器就会开始加载。因此要确保条件判断准确,避免误触发。
5. 清理(Clean Up):加载完成后,调用 unobserve() 停止对该元素的监听
这是性能优化和资源管理的关键一步,防止内存泄漏和不必要的计算。
// 在 img.src = realSrc; 之后
img.removeAttribute('data-src'); // 清除自定义属性,避免混淆
observer.unobserve(img); // 停止观察这个元素
-
为什么要
unobserve()?- 图片加载完成后,我们不再关心它的交叉状态。
- 如果不取消观察,
IntersectionObserver会持续监控该元素,即使它已经加载完毕。 - 当页面有大量图片时,未清理的观察目标会占用内存,降低整体性能。
-
removeAttribute('data-src'):- 避免开发者误以为该图片仍处于“待加载”状态。
- 减少 DOM 属性冗余。
-
错误处理补充:
img.addEventListener('load', () => { img.classList.add('loaded'); // 添加CSS类,实现淡入等动画 }); img.addEventListener('error', () => { img.src = 'fallback.jpg'; // 加载失败时的备用图 observer.unobserve(img); // 同样需要停止观察 });
✅ 关键点:“加载即清理” 是良好实践。每个目标元素在完成使命后都应从观察列表中移除。
总结:一个完整、健壮的懒加载流程
| 步骤 | 操作 | 目的 | 注意事项 |
|---|---|---|---|
| 标记 | data-src="real.jpg" + src="placeholder" | 延迟加载,避免初始请求 | src 必须轻量 |
| 创建 | new IntersectionObserver(callback, options) | 建立异步监控机制 | 合理设置 rootMargin 和 threshold |
| 注册 | observer.observe(img) | 将图片加入监控列表 | 动态内容需重新注册 |
| 响应 | if (isIntersecting) img.src = data-src | 触发真实图片加载 | 确保条件判断准确 |
| 清理 | unobserve(img) + removeAttribute('data-src') | 释放资源,避免内存泄漏 | 加载成功/失败都需清理 |
五、两种方案对比与选型建议
| 特性 | 传统方案 | IntersectionObserver |
|---|---|---|
| 性能 | 易引发 reflow,需手动优化 | 浏览器优化,自动批处理 |
| 实现复杂度 | 较高 | 更简单直观 |
| 预加载能力 | 需手动实现 | rootMargin 原生支持 |
| 动态内容支持 | 需重新绑定 | 可配合 MutationObserver |
| 兼容性 | 全浏览器支持 | Chrome 51+, Firefox 53+, Safari 12.1+ |
推荐选型策略
- ✅ 优先使用 IntersectionObserver:适用于现代浏览器环境,推荐主流项目使用。
- ⚠️ 传统方案作为兼容保障:如需支持 IE11 或低版本移动端浏览器。
- 🔁 混合方案:优雅降级,优先使用新特性,不支持时回退旧方案。
function initLazyLoad() {
if ('IntersectionObserver' in window) {
initLazyLoadModern();
} else {
initLazyLoadTraditional();
}
}
六、工程实践中的关键问题
1. 响应式图片支持
对于响应式设计,懒加载也应适配不同设备分辨率:
<img
src="placeholder.jpg"
data-srcset="image-400.jpg 400w, image-800.jpg 800w"
sizes="(max-width: 600px) 400px, 800px"
alt="响应式图片示例"
>
JavaScript 加载时同步设置 srcset 和 sizes:
function loadImage(img) {
const realSrc = img.dataset.src;
const realSrcSet = img.dataset.srcset;
const loader = new Image();
if (realSrcSet) loader.srcset = realSrcSet;
else loader.src = realSrc;
loader.onload = () => {
if (realSrcSet) img.srcset = realSrcSet;
else img.src = realSrc;
img.removeAttribute('data-src');
img.removeAttribute('data-srcset');
img.classList.add('loaded');
};
}
2. 加载状态可视化
提供视觉反馈,让用户知道图片正在加载中:
img.lazyload {
opacity: 0.8;
transition: opacity 0.3s ease;
}
img.lazyload.loaded {
opacity: 1;
}
img.lazyload.pending {
background: #f0f0f0 url('loading-spinner.gif') center center no-repeat;
}
3. 错误处理与重试机制
图片加载失败时应提供 fallback 并尝试重试:
function loadImageWithRetry(img, retries = 3) {
const realSrc = img.dataset.src;
const loader = new Image();
loader.src = realSrc;
loader.onload = () => {
img.src = realSrc;
img.removeAttribute('data-src');
img.classList.add('loaded');
};
loader.onerror = () => {
if (retries > 0) {
setTimeout(() => loadImageWithRetry(img, retries - 1), 1000 * (4 - retries));
} else {
img.src = 'fallback.jpg';
img.alt = '加载失败';
console.error(`图片加载失败: ${realSrc}`);
}
};
}
七、未来趋势:原生懒加载支持
现代浏览器已原生支持懒加载属性:
<img src="real-image.jpg" loading="lazy" alt="原生懒加载示例">
原生 vs JS 方案对比
| 特性 | 原生 loading="lazy" | JS 实现 |
|---|---|---|
| 实现方式 | 声明式属性 | 命令式脚本 |
| 性能 | 最佳优化(浏览器内部) | 依赖实现质量 |
| 控制粒度 | 有限 | 完全控制 |
| 兼容性 | Chrome 77+, Firefox 75+, Safari 15.4+ | 全浏览器支持 |
渐进增强方案(推荐)
<img
src="placeholder.jpg"
data-src="real-image.jpg"
loading="lazy"
class="lazyload"
alt="渐进增强示例"
onload="if (this.dataset.src) this.src = this.dataset.src; this.classList.remove('lazyload')"
>
八、总结与最佳实践
最佳实践建议
- ✅ 首屏内容优先加载,延迟非关键图片。
- ✅ 使用合适的预加载距离(如
rootMargin: "0px 0px 300px")。 - ✅ 提供加载动画或背景色提示。
- ✅ 实现优雅的错误处理与重试机制。
- ✅ 结合响应式图片技术,适配多设备。
- ✅ 优先使用 IntersectionObserver,原生属性作为补充。
技术演进方向
随着 Web 性能优化标准的发展,浏览器对懒加载的支持将更加完善。开发者应关注以下趋势:
- 原生懒加载的进一步普及
- 更智能的加载策略(如基于网络状况)
- 框架层面的懒加载内置支持(React/Vue/Angular)
九、结语
图片懒加载是构建高性能网页的重要手段之一。无论是使用传统的 JavaScript 技术,还是借助现代浏览器提供的 IntersectionObserver 和原生属性,合理地应用懒加载都能显著提升页面加载速度和用户体验。
希望本文能够帮助你全面掌握图片懒加载的技术细节,并在实际项目中灵活运用!