图片懒加载(Lazy Loading)是一种优化网页性能的技术,尤其在移动设备和网络条件较差的情况下效果显著。其核心原理是:只在图片进入浏览器视口(viewport)时才加载,而非页面加载时一次性加载所有图片。
为什么需要图片懒加载?
- 减少初始加载时间:首屏加载时无需请求所有图片,减少HTTP请求数
- 节省带宽:未被用户看到的图片不会被加载
- 提高性能:减少浏览器内存占用,避免一次性解码大量图片
实现原理
图片懒加载的实现主要基于以下几点:
- 占位符技术:使用低分辨率图片、纯色块或骨架屏作为占位符
- 视口检测:判断图片是否进入可视区域
- 资源替换:当图片进入视口时,将占位符替换为真实图片
下面介绍几种常见的实现方式:
基础实现(滚动事件 + getBoundingClientRect)
这是最基本的实现方式,通过监听窗口滚动事件,结合getBoundingClientRect()方法判断图片是否在视口中:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载示例</title>
<style>
.lazy-image {
display: block;
margin: 20px auto;
min-height: 200px;
background-color: #f0f0f0;
transition: opacity 0.3s ease-in-out;
opacity: 0.8;
}
.lazy-image.loaded {
opacity: 1;
}
</style>
</head>
<body>
<div style="height: 2000px;">
<!-- 图片列表 -->
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=1" alt="风景图片1">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=2" alt="风景图片2">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=3" alt="风景图片3">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=4" alt="风景图片4">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=5" alt="风景图片5">
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const lazyImages = document.querySelectorAll('.lazy-image');
// 处理图片加载
function handleLazyLoad() {
lazyImages.forEach(img => {
if (isInViewport(img) && !img.classList.contains('loaded')) {
loadImage(img);
}
});
}
// 判断元素是否在视口中
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.bottom >= 0
);
}
// 加载图片
function loadImage(img) {
const src = img.getAttribute('data-src');
const image = new Image();
image.onload = function() {
img.src = src;
img.classList.add('loaded');
};
image.src = src;
}
// 初始检查
handleLazyLoad();
// 监听滚动事件
window.addEventListener('scroll', handleLazyLoad);
// 监听窗口调整大小事件
window.addEventListener('resize', handleLazyLoad);
});
</script>
</body>
</html>
优化实现(Intersection Observer API)
现代浏览器提供了更高效的Intersection Observer API,可以异步观察目标元素与祖先元素或视口的交叉状态:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载示例(Intersection Observer)</title>
<style>
.lazy-image {
display: block;
margin: 20px auto;
min-height: 200px;
background-color: #f0f0f0;
transition: opacity 0.3s ease-in-out;
opacity: 0.8;
}
.lazy-image.loaded {
opacity: 1;
}
</style>
</head>
<body>
<div style="height: 2000px;">
<!-- 图片列表 -->
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=1" alt="风景图片1">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=2" alt="风景图片2">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=3" alt="风景图片3">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=4" alt="风景图片4">
<img class="lazy-image" data-src="https://picsum.photos/800/400?random=5" alt="风景图片5">
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const lazyImages = document.querySelectorAll('.lazy-image');
// 检查浏览器是否支持IntersectionObserver
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.getAttribute('data-src');
const image = new Image();
image.onload = function() {
img.src = src;
img.classList.add('loaded');
};
image.src = src;
observer.unobserve(img);
}
});
}, {
rootMargin: '100px 0px', // 提前100px加载
threshold: 0.01
});
lazyImages.forEach(img => {
observer.observe(img);
});
} else {
// 回退到传统滚动监听方式
function handleLazyLoad() {
lazyImages.forEach(img => {
if (isInViewport(img) && !img.classList.contains('loaded')) {
const src = img.getAttribute('data-src');
const image = new Image();
image.onload = function() {
img.src = src;
img.classList.add('loaded');
};
image.src = src;
}
});
}
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top <= (window.innerHeight + 100) &&
rect.bottom >= -100
);
}
window.addEventListener('scroll', handleLazyLoad);
window.addEventListener('resize', handleLazyLoad);
handleLazyLoad(); // 初始检查
}
});
</script>
</body>
</html>
HTML原生实现(loading="lazy")
HTML5新增了loading="lazy"属性,提供了最简单的懒加载方式:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>原生图片懒加载示例</title>
<style>
img {
display: block;
margin: 20px auto;
min-height: 200px;
background-color: #f0f0f0;
}
</style>
</head>
<body>
<div style="height: 2000px;">
<!-- 普通图片 - 立即加载 -->
<img src="https://picsum.photos/800/400?random=0" alt="首屏图片">
<!-- 懒加载图片 -->
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
data-src="https://picsum.photos/800/400?random=1"
alt="风景图片1"
loading="lazy">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
data-src="https://picsum.photos/800/400?random=2"
alt="风景图片2"
loading="lazy">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
data-src="https://picsum.photos/800/400?random=3"
alt="风景图片3"
loading="lazy">
</div>
<script>
// 为支持loading="lazy"的浏览器设置真实src
if ('loading' in HTMLImageElement.prototype) {
const images = document.querySelectorAll('img[loading="lazy"]');
images.forEach(img => {
img.src = img.dataset.src;
});
} else {
// 回退到IntersectionObserver或滚动监听
// 这里可以插入前面的IntersectionObserver实现代码
}
</script>
</body>
</html>
性能对比与最佳实践
| 方法 | 兼容性 | 性能 | 实现难度 |
|---|---|---|---|
| 滚动事件 | 好 | 低 | 中等 |
| Intersection Observer | 中等 | 高 | 高 |
| loading="lazy" | 差 | 最高 | 低 |
最佳实践建议:
- 优先使用
loading="lazy",配合polyfill处理不支持的浏览器 - 对于复杂场景或需要更多控制的情况,使用Intersection Observer
- 为懒加载图片设置适当的占位符,避免布局抖动
- 考虑图片在视口外提前加载的距离(rootMargin)
- 对于不支持现代API的浏览器提供回退方案
通过合理实现图片懒加载,可以显著提升网页性能和用户体验,尤其在移动设备和内容丰富的页面中效果更为明显。