懒加载实现原理与性能优化实战经验分享
懒加载的核心,Intersection Observer API
说实在的,现在做懒加载,我已经完全不用传统的 scroll + getBoundingClientRect 那套了。Intersection Observer API 在各大浏览器都支持得不错,而且性能更好,不会频繁触发重排。
我一般这样写:
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 预加载图片,避免闪动
const imgLoader = new Image();
imgLoader.onload = () => {
img.src = img.dataset.src;
img.classList.remove('lazy-loading');
img.classList.add('loaded');
};
imgLoader.src = img.dataset.src;
observer.unobserve(img); // 加载完成后取消监听
}
});
}, {
rootMargin: '50px 0px', // 提前50px开始加载
threshold: 0.01
});
// 监听所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
这段代码的核心就是 rootMargin 设置提前量,这样用户还没滚动到图片位置时就开始加载了。我一般设置 50px,移动端可能设得更大一点。
这几种错误写法,别再踩坑了
以前我犯过几个典型的错误,现在想想还是有点尴尬。
最常见的是在 scroll 事件里频繁计算:
// 错误做法:性能杀手
window.addEventListener('scroll', () => {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
// 加载图片
img.src = img.dataset.src;
}
});
});
这种写法会导致页面严重卡顿,因为 scroll 事件触发频率太高了。而且每次都要重新计算 DOM 位置,性能开销很大。
另一个坑是在 observe 回调里忘记取消监听:
// 错误:没有 unobserve,内存泄漏
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
// 没有 observer.unobserve(entry.target)
}
});
});
这样图片加载完了还在继续监听,浪费内存。正确的做法是加载完成就取消观察。
图片加载失败的处理,别让用户看到破图
实际项目中经常遇到图片地址失效的情况,这时候不能让用户看到那种破图。我一般这样处理:
const imgLoader = new Image();
imgLoader.onload = () => {
img.src = img.dataset.src;
img.classList.remove('lazy-loading');
img.classList.add('loaded');
};
imgLoader.onerror = () => {
img.src = '/images/default-placeholder.jpg'; // 替代图
img.classList.remove('lazy-loading');
img.classList.add('load-error');
};
imgLoader.src = img.dataset.src;
还可以加个重试机制,有时候网络抖动导致加载失败:
function loadImageWithRetry(imgElement, retryCount = 3) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attemptLoad() {
const loader = new Image();
loader.onload = () => resolve(loader);
loader.onerror = () => {
attempts++;
if (attempts < retryCount) {
setTimeout(attemptLoad, 1000); // 1秒后重试
} else {
reject(new Error('Image load failed'));
}
};
loader.src = imgElement.dataset.src;
}
attemptLoad();
});
}
视频和 iframe 的懒加载,容易被忽略
除了图片,视频和 iframe 也是资源大户。iframe 的懒加载我见过很多人不知道怎么做。
<iframe
data-src="https://example.com/embed"
width="560"
height="315"
style="display: none;"
></iframe>
const iframeObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const iframe = entry.target;
iframe.src = iframe.dataset.src;
iframe.style.display = 'block';
observer.unobserve(iframe);
}
});
});
document.querySelectorAll('iframe[data-src]').forEach(iframe => {
iframeObserver.observe(iframe);
});
视频的懒加载类似,主要区别是要考虑预加载策略:
const videoObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const video = entry.target;
video.preload = 'auto';
video.load(); // 触发加载
observer.unobserve(video);
}
});
});
首屏优化的小技巧
有时候页面首屏的内容也要用懒加载,比如为了更好的渐进式加载效果。这时候可以给首屏元素也加上 data-src,然后在 DOM 加载完成后立即加载首屏图片:
// DOM 加载完成后,立即加载首屏图片
document.addEventListener('DOMContentLoaded', () => {
const firstScreenImages = Array.from(document.querySelectorAll('img[data-src]'))
.filter(img => img.getBoundingClientRect().top < window.innerHeight);
firstScreenImages.forEach(img => {
const loader = new Image();
loader.onload = () => {
img.src = img.dataset.src;
img.classList.remove('lazy-loading');
img.classList.add('loaded');
};
loader.src = img.dataset.src;
});
// 其他图片继续用 Intersection Observer
const remainingImages = document.querySelectorAll('img[data-src]:not(.loaded)');
remainingImages.forEach(img => imageObserver.observe(img));
});
这样做可以让首屏内容更快呈现,同时保持良好的用户体验。
React/Vue 中的实际应用
在框架中使用懒加载需要注意组件销毁的问题。我一般封装成 hook:
// React hook 示例
function useLazyLoad() {
const [observer, setObserver] = useState(null);
useEffect(() => {
const currentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const imgLoader = new Image();
imgLoader.onload = () => {
img.src = img.dataset.src;
img.classList.remove('lazy-loading');
};
imgLoader.src = img.dataset.src;
currentObserver.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
setObserver(currentObserver);
return () => currentObserver.disconnect(); // 组件卸载时断开连接
}, []);
const observe = useCallback((element) => {
if (element && observer) {
observer.observe(element);
}
}, [observer]);
return { observe };
}
这种封装方式保证了 observer 只创建一次,并且在组件销毁时正确清理资源。
性能监控,确保懒加载真有效果
有时候你以为做了懒加载,实际上并没有生效。我一般会加点监控代码确认效果:
let loadedCount = 0;
const totalImages = document.querySelectorAll('img[data-src]').length;
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadedCount++;
console.log(懒加载进度: ${loadedCount}/${totalImages});
if (loadedCount === totalImages) {
console.log('所有图片加载完成');
}
// 正常的加载逻辑...
observer.unobserve(entry.target);
}
});
});
当然正式上线要删掉这些 log,但开发阶段很有用,能确认懒加载是否按预期工作。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如配合 Intersection Observer 实现无限滚动等,后续会继续分享这类博客。

暂无评论