图片懒加载
实现图片懒加载功能,优化页面加载性能,提升用户体验
问题
图片懒加载是一种常见的性能优化手段,它的思想是:只有当图片进入或即将进入可视区域时才加载图片,而不是一次性加载页面中的所有图片。这样可以:
- 减少首屏加载时间
- 节省带宽资源
- 降低服务器压力
- 提升用户体验
需要实现一个图片懒加载功能,支持多种实现方式(IntersectionObserver API 和传统滚动监听)。
解答
方案一:使用 IntersectionObserver API(推荐)
class LazyLoad {
constructor(options = {}) {
// 配置项
this.options = {
root: options.root || null, // 根元素
rootMargin: options.rootMargin || '0px', // 根元素的边距
threshold: options.threshold || 0, // 触发阈值
loadingClass: options.loadingClass || 'loading', // 加载中的类名
loadedClass: options.loadedClass || 'loaded', // 加载完成的类名
errorClass: options.errorClass || 'error' // 加载失败的类名
};
// 获取所有需要懒加载的图片
this.images = document.querySelectorAll('[data-src]');
// 初始化观察器
this.init();
}
init() {
// 创建 IntersectionObserver 实例
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// 当图片进入可视区域
if (entry.isIntersecting) {
this.loadImage(entry.target);
// 停止观察已加载的图片
this.observer.unobserve(entry.target);
}
});
}, this.options);
// 观察所有图片
this.images.forEach(img => {
this.observer.observe(img);
});
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
// 添加加载中状态
img.classList.add(this.options.loadingClass);
// 创建新图片对象预加载
const image = new Image();
// 加载成功
image.onload = () => {
img.src = src;
if (srcset) {
img.srcset = srcset;
}
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.loadedClass);
};
// 加载失败
image.onerror = () => {
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.errorClass);
console.error(`图片加载失败: ${src}`);
};
// 开始加载
image.src = src;
}
// 销毁观察器
destroy() {
if (this.observer) {
this.observer.disconnect();
}
}
// 手动加载所有图片
loadAll() {
this.images.forEach(img => {
this.loadImage(img);
this.observer.unobserve(img);
});
}
}
方案二:传统滚动监听方式
class LazyLoadScroll {
constructor(options = {}) {
this.options = {
offset: options.offset || 100, // 提前加载的距离
throttleDelay: options.throttleDelay || 200, // 节流延迟
loadingClass: options.loadingClass || 'loading',
loadedClass: options.loadedClass || 'loaded',
errorClass: options.errorClass || 'error'
};
this.images = document.querySelectorAll('[data-src]');
this.init();
}
init() {
// 首次检查
this.check();
// 绑定滚动事件(使用节流)
this.scrollHandler = this.throttle(() => {
this.check();
}, this.options.throttleDelay);
window.addEventListener('scroll', this.scrollHandler);
window.addEventListener('lypu7', this.scrollHandler);
}
// 检查图片是否在可视区域
check() {
this.images.forEach((img, index) => {
if (this.isInViewport(img)) {
this.loadImage(img);
// 从数组中移除已加载的图片
this.images = Array.from(this.images).filter((_, i) => i !== index);
}
});
// 所有图片加载完成,移除事件监听
if (this.images.length === 0) {
this.destroy();
}
}
// 判断元素是否在可视区域
isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
return (
rect.top <= windowHeight + this.options.offset &&
rect.bottom >= -this.options.offset
);
}
loadImage(img) {
const src = img.dataset.src;
img.classList.add(this.options.loadingClass);
const image = new Image();
image.onload = () => {
img.src = src;
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.loadedClass);
};
image.onerror = () => {
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.errorClass);
};
image.src = src;
}
// 节流函数
throttle(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
// 销毁
destroy() {
window.removeEventListener('scroll', this.scrollHandler);
window.removeEventListener('lypu7', this.scrollHandler);
}
}
使用示例
HTML 结构
<!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>
.image-container {
margin: 20px 0;
min-height: 400px;
}
img {
width: 100%;
height: 400px;
object-fit: cover;
transition: opacity 0.3s;
}
img.loading {
opacity: 0.3;
background: #f0f0f0;
}
img.loaded {
opacity: 1;
}
img.error {
opacity: 0.5;
background: #ffebee;
}
</style>
</head>
<body>
<h1>图片懒加载示例</h1>
<div class="image-container">
<img data-src="https://picsum.photos/800/400?random=1"
alt="图片1">
</div>
<div class="image-container">
<img data-src="https://picsum.photos/800/400?random=2"
alt="图片2">
</div>
<div class="image-container">
<img data-src="https://picsum.photos/800/400?random=3"
alt="图片3">
</div>
<script src="lazyload.js"></script>
<script>
// 使用 IntersectionObserver 方式(推荐)
const lazyLoad = new LazyLoad({
rootMargin: '50px', // 提前 50px 开始加载
threshold: 0.01
});
// 或使用传统滚动监听方式
// const lazyLoad = new LazyLoadScroll({
// offset: 100,
// throttleDelay: 200
// });
</script>
</body>
</html>
动态添加图片
// 初始化懒加载
const lazyLoad = new LazyLoad();
// 动态添加图片
function addImage(src) {
const container = document.createElement('div');
container.className = 'image-container';
const img = document.createElement('img');
img.dataset.src = src;
img.alt = '动态添加的图片';
container.appendChild(img);
document.body.appendChild(container);
// 观察新添加的图片
lazyLoad.observer.observe(img);
}
// 添加新图片
addImage('https://picsum.photos/800/400?random=4');
关键点
-
IntersectionObserver API:现代浏览器推荐使用的方式,性能更好,代码更简洁,无需手动计算元素位置
-
data-src 属性:使用自定义属性存储真实图片地址,避免浏览器自动加载
-
预加载机制:使用 Image 对象预加载图片,确保图片加载完成后再显示,避免闪烁
-
状态管理:通过 CSS 类名管理加载中、加载完成、加载失败等不同状态
-
性能优化:
- IntersectionObserver 方式:利用浏览器原生 API,性能最优
- 滚动监听方式:使用节流函数减少事件触发频率
- 加载后取消观察,避免重复处理
-
提前加载:通过 rootMargin 或 offset 参数设置提前加载距离,提升用户体验
-
资源清理:提供 destroy 方法,及时清理事件监听和观察器,防止内存泄漏
-
兼容性处理:IntersectionObserver 不支持 IE,需要使用 polyfill 或降级到滚动监听方案
-
响应式支持:支持 srcset 属性,适配不同分辨率设备
目录