图片懒加载
使用 Scroll 事件和 IntersectionObserver 实现图片懒加载
问题
实现图片懒加载,对比 Scroll 事件监听和 IntersectionObserver 两种方案。
解答
HTML 结构
<img class="lazy" data-src="real-image.jpg" src="placeholder.jpg" alt="图片">
<img class="lazy" data-src="real-image2.jpg" src="placeholder.jpg" alt="图片">
方案一:Scroll 事件监听
function lazyLoadWithScroll() {
const images = document.querySelectorAll('img.lazy');
function loadImage(img) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
function checkImages() {
images.forEach(img => {
// 获取图片相对于视口的位置
const rect = img.getBoundingClientRect();
// 判断图片是否进入视口
if (rect.top < window.innerHeight && rect.bottom > 0) {
loadImage(img);
}
});
}
// 节流函数,避免频繁触发
function throttle(fn, delay) {
let timer = null;
return function(...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
const throttledCheck = throttle(checkImages, 200);
// 监听滚动事件
window.addEventListener('scroll', throttledCheck);
// 初始检查
checkImages();
}
方案二:IntersectionObserver
function lazyLoadWithObserver() {
const images = document.querySelectorAll('img.lazy');
// 创建观察器
const observer = 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: '50px', // 提前 50px 加载
threshold: 0.1 // 10% 可见时触发
});
// 观察所有懒加载图片
images.forEach(img => observer.observe(img));
}
完整封装
class LazyLoader {
constructor(selector = 'img.lazy', options = {}) {
this.images = document.querySelectorAll(selector);
this.options = {
rootMargin: options.rootMargin || '50px',
threshold: options.threshold || 0.1
};
this.init();
}
init() {
// 优先使用 IntersectionObserver
if ('IntersectionObserver' in window) {
this.useObserver();
} else {
this.useScroll();
}
}
loadImage(img) {
// 加载真实图片
img.src = img.dataset.src;
// 支持 srcset
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
img.classList.remove('lazy');
img.classList.add('loaded');
}
useObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, this.options);
this.images.forEach(img => observer.observe(img));
}
useScroll() {
let ticking = false;
const checkImages = () => {
this.images.forEach(img => {
if (img.classList.contains('lazy')) {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight + 50 && rect.bottom > -50) {
this.loadImage(img);
}
}
});
};
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
checkImages();
ticking = false;
});
ticking = true;
}
});
checkImages();
}
}
// 使用
new LazyLoader('img.lazy', { rootMargin: '100px' });
两种方案对比
| 特性 | Scroll 事件 | IntersectionObserver |
|---|---|---|
| 性能 | 需要节流,频繁计算 | 浏览器优化,异步回调 |
| 兼容性 | 所有浏览器 | IE 不支持 |
| 代码量 | 较多 | 简洁 |
| 精确度 | 需手动计算 | 配置灵活 |
关键点
- IntersectionObserver 优先:性能更好,代码更简洁,现代浏览器首选
- Scroll 需要节流:使用 throttle 或 requestAnimationFrame 避免性能问题
- getBoundingClientRect:Scroll 方案的核心,获取元素相对视口位置
- rootMargin 预加载:设置正值可提前加载,提升用户体验
- 加载后取消观察:调用 unobserve 避免重复触发
目录