懒加载实现原理与性能优化实践总结
项目初期的技术选型
最近接手了一个电商项目的重构,主要是商品列表页和详情页的优化。页面上有个问题特别明显:一次性加载几百张图片,用户打开页面就是白屏,有时候要等个十几秒才能看到内容。老板看不下去了,让我赶紧把懒加载加上去。
开始我还觉得这不就是个简单的Intersection Observer吗?毕竟现在浏览器支持度也不错。但实际做起来才发现,业务场景比想象的复杂多了。用户可能会快速滚动、可能会在移动端缩放页面、还可能有各种奇怪的交互场景,这些都让懒加载变得不那么简单。
原生Intersection Observer的实现
我先用原生的Intersection Observer试了试水,代码其实挺简洁的:
class LazyLoad {
constructor(options = {}) {
this.options = {
rootMargin: options.rootMargin || '0px',
threshold: options.threshold || 0.1,
...options
};
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
this.options
);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
this.observer.unobserve(img);
}
});
}
loadImage(img) {
const src = img.dataset.src;
if (!src) return;
const imageLoader = new Image();
imageLoader.onload = () => {
img.src = src;
img.classList.remove('loading');
img.classList.add('loaded');
};
imageLoader.onerror = () => {
img.classList.add('error');
};
imageLoader.src = src;
}
observe(img) {
this.observer.observe(img);
}
disconnect() {
this.observer.disconnect();
}
}
// 使用方法
const lazyLoad = new LazyLoad({
rootMargin: '50px'
});
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoad.observe(img);
});
HTML这边也很简单:
<img
data-src="https://jztheme.com/images/product1.jpg"
alt="商品图片"
class="lazy-image loading"
src="placeholder.jpg"
/>
本地测试看起来不错,但在真实环境下就开始暴露问题了。
移动端的适配噩梦
最大的问题是iOS Safari的兼容性。项目里发现Intersection Observer在某些iOS版本下表现异常,尤其是微信内置浏览器,简直是灾难现场。有时候元素已经进入视口了,但是回调函数就是不触发。折腾了半天发现需要引入polyfill,但这样又增加了包体积。
还有个问题是滚动性能。用户在移动端快速滚动时,Intersection Observer的回调频率太高,会导致卡顿。虽然理论上应该是高性能的,但实际情况比预想的复杂。后来我在回调函数外面加了一层节流:
handleIntersection: throttle(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
this.observer.unobserve(img);
}
});
}, 100)
这里的throttle函数是自己实现的,主要就是为了控制回调频率。不过说实话,加了节流之后确实会影响响应速度,但比起卡顿来说,这还是可以接受的。
回退方案的实现
考虑到兼容性和稳定性,我还是决定做一个回退方案:基于scroll事件的传统懒加载。虽然性能不如Intersection Observer,但至少稳定可靠。
class ScrollBasedLazyLoad {
constructor(options = {}) {
this.options = {
threshold: options.threshold || 100,
...options
};
this.images = [];
this.throttledCheck = throttle(this.checkImages.bind(this), 100);
this.init();
}
init() {
this.setupScrollListener();
this.checkImages(); // 页面加载时检查一次
}
setupScrollListener() {
window.addEventListener('scroll', this.throttledCheck, { passive: true });
window.addEventListener('resize', this.throttledCheck);
}
checkImages() {
const windowHeight = window.innerHeight;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
this.images = this.images.filter(img => {
const rect = img.getBoundingClientRect();
const top = rect.top + scrollTop;
const elementTop = top + this.options.threshold;
if (elementTop <= windowHeight + scrollTop) {
this.loadImage(img);
return false; // 移除已加载的图片
}
return true;
});
}
loadImage(img) {
const src = img.dataset.src;
if (!src) return;
const imageLoader = new Image();
imageLoader.onload = () => {
img.src = src;
img.classList.remove('loading');
img.classList.add('loaded');
};
imageLoader.src = src;
}
observe(img) {
this.images.push(img);
}
}
最终的技术方案
最后我采用的是混合方案:现代浏览器优先使用Intersection Observer,老浏览器回退到scroll事件。检测方法很简单:
const supportsIntersectionObserver = 'IntersectionObserver' in window;
const lazyLoad = supportsIntersectionObserver
? new LazyLoad({ rootMargin: '50px' })
: new ScrollBasedLazyLoad({ threshold: 100 });
// 初始化所有需要懒加载的图片
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoad.observe(img);
});
实际跑起来效果还不错,页面加载时间从原来的15-20秒降到了3-5秒,首屏渲染时间也大幅缩短。不过还是有一些小问题,比如在极少数情况下会出现图片加载失败的情况,但我检查了日志,发现概率很低,大概万分之一左右,暂时没精力深究了。
性能监控和优化
为了确保懒加载的效果,我还加了一些监控代码来统计加载情况:
class LazyLoadMonitor {
constructor() {
this.stats = {
total: 0,
loaded: 0,
errors: 0,
startTimestamp: Date.now()
};
}
onImageLoad() {
this.stats.loaded++;
this.logProgress();
}
onImageError() {
this.stats.errors++;
}
logProgress() {
const { loaded, total } = this.stats;
const progress = ((loaded / total) * 100).toFixed(2);
console.log(懒加载进度: ${progress}%);
}
}
从监控数据来看,大部分用户的体验都有明显改善,特别是网络环境较差的情况下。图片按需加载避免了不必要的流量消耗,对移动端用户特别友好。
遗留问题和后续优化
这个方案运行了几个月,整体效果还是满意的。唯一不太完美的是在某些极端情况下(比如用户疯狂滚动页面),还是会有些许性能抖动。另外,Intersection Observer的兼容性问题到现在还有一些历史版本的Android WebView处理不好,但考虑到这部分用户占比很小,暂时也没继续投入更多资源去解决。
如果下次再做类似需求,我想我会考虑集成图片的预加载策略,让用户在浏览当前内容的时候,提前加载后面可能需要的图片。这样用户体验会更好一些,不过实现起来会更复杂,需要平衡加载速度和资源消耗。
以上是我这次懒加载实现的经验总结,过程中踩了不少坑,但也学到了很多东西。有更优的实现方式欢迎评论区交流。

暂无评论