图片懒加载原理与实战:从滚动监听到 Intersection Observer
在网页性能优化中,图片懒加载是一项基础且重要的技术。本文将带你从零了解懒加载的必要性、实现原理,并通过代码实战掌握现代浏览器提供的 Intersection Observer API,让你的页面加载速度更上一层楼。
一、为什么需要图片懒加载?
在传统的网页开发中,我们通常会在 <img> 标签的 src 属性中直接写入图片的 URL。当浏览器解析到该标签时,就会立即发起 HTTP 请求去下载图片。
如果一个页面包含大量图片,尤其是像电商、图库这类场景,所有图片同时请求会带来两个严重问题:
- 首屏加载缓慢:浏览器并发请求数量有限(通常 6-8 个),图片占用了大量请求队列,导致 HTML、CSS、核心 JS 等关键资源被阻塞,页面白屏时间变长。
- 浪费用户流量:用户可能根本没有滚动到页面底部,却已经加载了所有图片,对于移动端用户而言,这是一笔不必要的流量开销。
懒加载的核心思想:只加载当前视口(viewport)内的图片,当用户滚动到某个位置时,再去加载对应的图片。这是一种典型的“按需加载”策略,能够显著提升首屏性能,减少资源浪费。
二、传统懒加载方案:滚动监听 + 节流
在 Intersection Observer API 出现之前,开发者通常通过监听 scroll 事件来实现懒加载。
基本思路
- 给图片设置
data-src存储真实地址,src先放一张极小的占位图(如 base64 或 1x1 像素图)。 - 监听
scroll事件,在回调中计算每张图片是否进入视口。 - 如果图片进入视口,则将
data-src的值赋给src,完成加载。
示例代码(简单版)
<img class="lazy" data-src="real-image.jpg" src="placeholder.jpg" alt="">
const lazyImages = document.querySelectorAll('.lazy');
function isInViewport(el) {
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}
function lazyLoad() {
lazyImages.forEach(img => {
if (isInViewport(img) && img.src !== img.dataset.src) {
img.src = img.dataset.src;
}
});
}
window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
存在的问题
- 性能问题:
scroll事件触发非常频繁(每滚动一像素都可能触发),频繁执行getBoundingClientRect会导致重排,影响滚动流畅度。 - 需要手动节流:通常要配合
requestAnimationFrame或 lodash 的throttle来降低回调执行频率。 - 代码复杂度高:还要考虑动态添加的图片、浏览器兼容性等。
虽然可行,但不够优雅。现代浏览器提供了更专业的解决方案。
三、现代方案:Intersection Observer API
IntersectionObserver 是浏览器原生提供的异步观察 API,用于监听元素与其祖先元素或视口的交叉状态。它的出现完美解决了滚动监听的性能痛点。
核心概念
- 观察者:创建
IntersectionObserver实例,传入回调函数和配置项。 - 被观察者:一个或多个 DOM 元素。
- 交叉比(Intersection Ratio):元素可见比例,当比例超过阈值(
threshold)时触发回调。 - 触发时机:元素进入或离开视口时,回调函数会异步执行,不会阻塞主线程。
API 简介
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 元素进入视口
}
});
}, {
threshold: 0.5, // 可见度达到 50% 时触发
rootMargin: '0px' // 扩大或缩小视口区域
});
observer.observe(targetElement); // 开始观察
observer.unobserve(targetElement); // 停止观察
四、实战:基于 Intersection Observer 的懒加载
下面我们结合你提供的完整代码,一步步实现一个可用的懒加载示例。
1. HTML 结构
我们需要一个足够长的页面来模拟滚动,同时为每张图片设置 class="lazy",用 src 放占位图,data-src 放真实图片地址。
<div class="box"></div>
<img class="lazy" src="placeholder.png" data-src="real-image-1.jpg" alt="">
<div class="box"></div>
<img class="lazy" src="placeholder.png" data-src="real-image-2.jpg" alt="">
为了滚动效果,我们给 .box 设置了 height: 200vh,这样两个盒子之间就会产生足够的滚动空间。
2. CSS 样式
简单重置内外边距,让占位盒子撑开页面:
* {
margin: 0;
padding: 0;
}
.box {
height: 200vh;
background-color: #f5f5f5;
}
3. JavaScript 懒加载逻辑
// 获取所有需要懒加载的图片
const images = document.querySelectorAll('.lazy');
// 创建 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// 判断元素是否进入视口
if (entry.isIntersecting) {
const img = entry.target;
const realSrc = img.dataset.src;
// 将真实地址赋给 src,触发浏览器加载图片
img.src = realSrc;
// 图片开始加载后,取消观察,避免重复加载
observer.unobserve(img);
}
});
});
// 对所有 .lazy 图片启动观察
images.forEach(img => observer.observe(img));
代码解析:
observer的回调接收entries数组,包含所有被观察元素的变化情况。- 我们遍历
entries,检查每个元素的isIntersecting属性,为true表示元素当前出现在视口中。 - 从
dataset.src中取出真实图片地址,赋值给src,图片开始加载。 - 调用
observer.unobserve(img)取消对该图片的观察,避免后续滚动重复触发。
4. 完整效果演示
4.1 HTML 例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片的懒加载</title>
<style>
* {
margin: 0;
padding: 0;
}
.box {
height: 200vh;
background-color: white;
}
</style>
</head>
<body>
<div class="box"></div>
<!-- 数据属性 -->
<img class="lazy" src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png" data-src="https://img.36krcdn.com/hsossms/20260119/v2_53cad3f2226f48e2afc1942de3ab74e4@5888275@ai_oswg1141728oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp" alt=""/>
<div class="box"></div>
<img class="lazy" src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png" data-src="https://img.36krcdn.com/hsossms/20260117/v2_1e74add07bb94971845c777e0ce87a49@000000@ai_oswg421938oswg1536oswg722_img_000~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp" alt=""/>
<script>
const images=document.querySelectorAll(".lazy")
const observer=new IntersectionObserver((entries)=>{
console.log(entries)
entries.forEach(entry=>{
if(entry.isIntersecting){
const img=entry.target
const original_img=img.dataset.src
console.log(original_img)
img.src=original_img
observer.unobserve(img)
}
})
})
images.forEach(img=>observer.observe(img))
</script>
</body>
</html>
补充
在代码中,两个 console.log 的作用是帮助开发者观察懒加载的触发过程。当你滚动页面,某张图片进入视口时,控制台会依次打印:
-
entries:一个包含IntersectionObserverEntry对象的数组。
每个对象都描述了被观察元素与视口的交叉状态,包括:target:触发加载的<img>元素本身。isIntersecting:当前元素是否与视口相交(此时为true)。intersectionRatio:可见比例(通常在 0~1 之间)。boundingClientRect、rootBounds等位置信息。
实际打印时,你会看到类似这样的输出(简化后):
[IntersectionObserverEntry] 0: IntersectionObserverEntry isIntersecting: true target: img.lazy ... -
original_img:从当前图片的data-src属性中读取的真实图片地址。
例如,代码中第一张图片的data-src是一个 URL: img.36krcdn.com/hsossms/202… 控制台会直接打印这个字符串。
效果图
当页面加载时,由于两个 .box 占据了大量空间,图片可能位于视口之外,此时 src 仍是占位图(例如京东的默认图)。
当用户滚动到图片附近时,浏览器会自动检测到该图片进入视口,并动态将 data-src 的值赋给 src,图片开始加载。
这种方式的性能远优于 scroll 监听,因为所有计算都由浏览器底层在恰当的时候异步完成,不会阻塞主线程。
五、优化与注意事项
1. 占位图的重要性
占位图不宜过大,否则失去了“先加载轻量内容”的意义。通常使用:
- 极小的 base64 图片,如
data:image/svg+xml,%3Csvg... - 纯色背景 + CSS 背景色占位
- 低分辨率缩略图(LQIP 技术)
2. 避免重复加载
每次图片加载完成后,务必调用 unobserve。否则,如果用户反复滚动经过该图片,isIntersecting 可能多次为 true,导致重复赋值(虽然再次赋值不会重新下载,但仍浪费了判断成本)。
3. 兼容性与 Polyfill
Intersection Observer 在主流浏览器中的支持度已经非常好(除 IE 外)。如果需要兼容低版本浏览器,可以引入官方的 polyfill。
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
4. 扩展:与动态加载结合
如果页面中有通过 AJAX 动态添加的图片,需要在新增图片后,再次调用 observer.observe(newImg),确保新图片也能被懒加载。
5. 配置项进阶
你可以通过 threshold 和 rootMargin 来控制图片加载的时机,例如提前加载:
const observer = new IntersectionObserver(callback, {
rootMargin: '200px', // 视口上下各扩展 200px,图片距离视口还有 200px 时就开始加载
threshold: 0
});
这样可以实现“预加载”效果,让用户滚动时感觉更流畅。
六、总结
图片懒加载是一个看似简单却蕴含深厚性能优化思想的实践。通过本文,我们对比了传统的滚动监听方案与现代的 Intersection Observer 方案,并基于你提供的代码完成了完整的懒加载实现。
| 方案 | 优点 | 缺点 |
|---|---|---|
| scroll 监听 | 兼容性好,原理简单 | 性能差,需手动节流,代码复杂 |
| Intersection Observer | 性能优秀,使用简洁,异步回调 | 需考虑 polyfill(IE 不支持) |
在实际项目中,推荐优先使用 Intersection Observer,它能让代码更清晰、性能更可控。如果你正在开发一个图片密集型页面,不妨立即动手试试这种懒加载方式,相信你的页面加载速度会有明显提升。