在现代网页中,图片是内容的重要组成部分,但大量高分辨率图片的加载往往会拖慢页面性能,影响用户体验,尤其是移动端用户。为了解决这个问题,图像延迟加载(Lazy Loading) 成为了前端优化中的标配技术。
本文将带你深入理解延迟加载的原理,并通过两种主流实现方式——getBoundingClientRect
和 IntersectionObserver
——手把手教你如何在项目中实现高效的图片懒加载。同时,我们还会深入探讨 IntersectionObserver
的 rootMargin
配置,利用“缓冲区”实现智能预加载,进一步提升用户体验。
一、什么是图像延迟加载?
延迟加载(Lazy Loading) 是一种优化策略:只有当某个元素即将进入或已经进入用户视口(viewport)时,才加载其资源(如图片、视频等)。对于页面中靠下的图片,它们不会在页面初始加载时就被请求,而是等到用户滚动到附近时才动态加载。
✅ 延迟加载的好处:
- 减少初始页面加载时间
- 节省带宽,提升用户体验
- 降低服务器压力
- 提高网页性能评分(如 Lighthouse 分数)
🌰 举个例子:一个包含 50 张图片的长页面,如果一开始就全部加载,用户可能需要等待十几秒。而使用懒加载后,只有前几屏的图片会被加载,其余图片在滚动时按需加载,体验显著提升。
二、延迟加载的基本原理
延迟加载的核心思想是:
“按需加载” —— 不在视口内的图片先不加载,用占位符代替,等它快进入视口时,再替换为真实图片。
实现的关键在于:如何检测一个元素是否进入了视口?
下面介绍两种主流方法。
三、方法一:使用 getBoundingClientRect()
手动判断
getBoundingClientRect()
是一个 DOM 方法,可以返回元素相对于视口的位置信息。
🔍 返回值示例:
const rect = element.getBoundingClientRect();
// rect = { top, right, bottom, left, width, height }
我们可以利用 rect.top
和 rect.bottom
来判断元素是否在视口内。
✅ 实现步骤:
- 给图片设置
data-src
属性存储真实图片地址 - 初始时
src
为空或为占位图 - 监听
scroll
事件,检查图片是否进入视口 - 进入视口后,将
data-src
赋值给src
,触发加载 - 加载完成后移除监听,避免重复检查
💡 示例代码:
<img class="lazy" data-src="image1.jpg" src="placeholder.jpg" alt="图片1">
<img class="lazy" data-src="image2.jpg" src="placeholder.jpg" alt="图片2">
function lazyLoad() {
const images = document.querySelectorAll('.lazy');
images.forEach(img => {
const rect = img.getBoundingClientRect();
const windowHeight = window.innerHeight;
// 判断图片是否在视口内(或即将进入)
if (rect.top < windowHeight + 100 && rect.bottom > -100) {
if (img.dataset.src) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
}
});
}
// 初始检查 + 滚动监听
window.addEventListener('load', lazyLoad);
window.addEventListener('scroll', lazyLoad);
// 优化:使用节流(throttle)避免频繁触发
function throttle(func, delay) {
let inThrottle;
return function() {
if (!inThrottle) {
func.apply(this, arguments);
inThrottle = true;
setTimeout(() => inThrottle = false, delay);
}
};
}
window.addEventListener('scroll', throttle(lazyLoad, 100));
⚠️ 缺点:
- 频繁触发
scroll
事件,性能开销大 - 需要手动节流优化
- 兼容性好,但不够优雅
四、方法二:使用 Intersection Observer API
(推荐)
Intersection Observer
是现代浏览器提供的原生 API,用于异步监听元素与视口的交叉状态,非常适合实现懒加载。
✅ 优点:
- 非阻塞:在主线程外运行,不影响页面性能
- 自动监听:无需手动计算位置
- 支持回调:可精确控制进入/离开视口的时机
- 可配置阈值:比如“当元素 20% 可见时触发”
💡 示例代码:
const images = document.querySelectorAll('.lazy');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
// 加载完成后停止监听
observer.unobserve(img);
}
});
}, {
root: null, // 使用视口作为根
threshold: 0.1, // 当 10% 可见时触发
rootMargin: '50px' // 提前 50px 开始加载(预加载)
});
images.forEach(img => {
observer.observe(img);
});
🔍 配置项说明:
选项 | 说明 |
---|---|
| 根容器( |
| 触发比例(0.0 ~ 1.0,或 |
| 外边距扩展,实现“预加载” |
五、进阶技巧:使用 rootMargin
创建“缓冲区”,实现预加载
在实际应用中,我们不希望用户已经滚动到图片位置时才开始加载——那样可能会看到明显的“空白→图片”的加载过程,影响体验。
为了解决这个问题,Intersection Observer
提供了一个非常实用的配置项:rootMargin
。它允许我们为视口“扩展”一个缓冲区域,让图片在进入视口之前就提前加载。
🔍 什么是 rootMargin
?
rootMargin
类似于 CSS 的 margin
,但它作用于视口边界,可以向外或向内扩展观察区域。
语法与 CSS 相同,支持像素(px)、百分比(%)等单位:
rootMargin: '50px' // 上下左右各扩展 50px
rootMargin: '20px 0' // 上下 20px,左右 0
rootMargin: '0 50px' // 左右 50px,上下 0
rootMargin: '-100px' // 向内收缩 100px(延迟触发)
✅ 实际应用场景:提前加载图片
假设我们设置:
const observer = new IntersectionObserver(
(entries) => { /* ... */ },
{
root: null,
threshold: 0.01,
rootMargin: '100px' // 在图片距离视口还有 100px 时就开始加载
}
);
这意味着:当图片距离视口上方还有 100px 时,回调函数就会触发,浏览器可以立即开始请求图片资源。当用户滚动到该位置时,图片很可能已经加载完成,实现“无缝”展示。
🎯 使用建议:
rootMargin: '100px'
:适合普通网页,提前加载一屏内的图片- 移动端可适当减小:如
50px
,避免过度预加载浪费流量 - 配合
threshold: 0
:即使只进入 1 像素也触发,提升响应速度
💡 示例:带缓冲区的懒加载
const lazyImages = document.querySelectorAll('.lazy');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img); // 加载完成后停止监听
}
});
}, {
root: null, // 使用浏览器视口
rootMargin: '100px', // 缓冲区:提前 100px 开始加载
threshold: 0.01 // 只要出现一点点就触发
});
lazyImages.forEach(img => {
imageObserver.observe(img);
});
⚠️ 注意事项:
rootMargin
的值越大,预加载越早,但可能加载用户根本不会看到的图片,造成资源浪费。- 建议结合业务场景测试最佳值,平衡性能与用户体验。
🌟 小结:rootMargin
是懒加载的“智能预判器”
通过合理设置 rootMargin
,我们可以让懒加载变得更“聪明”——不再是“等你来了才准备”,而是“你快到了,我已经准备好了”。这正是高性能网页体验的关键细节之一。
六、两种方法对比
性能 | 较低(需监听 scroll) | 高(异步,不阻塞主线程) |
兼容性 | 所有浏览器 | 现代浏览器(IE 不支持) |
实现复杂度 | 中等(需节流) | 简单优雅 |
预加载支持 | 需手动计算 | 支持 |
推荐使用 | 兼容老项目 | ✅ 推荐新项目使用 |
📌 建议:优先使用
IntersectionObserver
,对不支持的浏览器可降级使用getBoundingClientRect
。
七、其他优化建议
-
使用
<img loading="lazy">
原生懒加载
现代浏览器已支持原生懒加载:<img src="image.jpg" loading="lazy" alt="描述">
简单有效,适合大多数场景。
-
配合占位图或模糊图(Blurry Image Placeholder)
避免图片加载时的布局偏移(Layout Shift)。 -
预加载关键图片
对首屏关键图片使用preload
:<link rel="preload" as="image" href="hero.jpg">
-
结合 CDN 和图片格式优化
使用 WebP/AVIF 格式 + 响应式图片(srcset
)进一步提升性能。
八、总结
图像延迟加载是提升网页性能的重要手段。通过本文介绍的两种实现方式:
getBoundingClientRect
:适合兼容老浏览器,手动控制灵活IntersectionObserver
:现代推荐方案,性能好、代码简洁,尤其是通过rootMargin
实现“缓冲区”预加载,显著提升用户体验
我们可以在不同场景下选择合适的方案,显著提升页面加载速度和用户体验。
随着浏览器能力的不断增强,懒加载正变得越来越简单。但理解其底层原理,依然是每个前端开发者必备的技能。
参考阅读:
- MDN: Intersection Observer API
- Google Developers: Lazy Loading Images
- Can I use: Intersection Observer
你用过哪些懒加载方案?有没有遇到过性能问题?欢迎在评论区分享你的经验!