1. 为什么需要图片懒加载
在Web开发中,图片通常是页面中最"重"的资源之一。当页面包含大量图片时(比如电商网站的商品列表、图库网站的照片墙),一次性加载所有图片会导致:
- 带宽浪费:用户可能只看前几屏内容,却下载了所有图片
- 性能下降:浏览器需要同时处理大量图片解码和渲染
- 体验不佳:页面卡顿、滚动迟滞,特别是移动端更明显
懒加载的核心思想是:仅加载用户当前可见或即将看到的图片。当用户滚动页面时,再动态加载进入视口的图片。这种技术可以:
- 减少初始页面加载时间(LCP指标优化)
- 节省用户流量(对移动端尤为重要)
- 降低服务器负载
- 提升页面滚动流畅度
2. 原生loading="lazy"实现方案
2.1 基础实现
最简单的懒加载方案是使用HTML5原生支持的loading="lazy"属性:
html复制<img
:src="item.coverUrl"
class="img lazy-img"
alt=""
loading="lazy"
/>
这个方案的优点是:
- 零JavaScript依赖:完全由浏览器处理
- 简单易用:只需添加一个属性
- 渐进增强:不支持的浏览器会正常加载图片
2.2 视觉优化技巧
为了提升用户体验,我们可以为未加载的图片添加占位效果:
scss复制.lazy-img {
background-color: #f5f5f5;
background-image: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
这种"骨架屏"效果比空白区域更能让用户感知到内容即将加载。
2.3 注意事项
- 兼容性:虽然现代浏览器都支持,但某些旧版本(如Safari 15.3以下)可能不支持
- 阈值不可控:浏览器自行决定何时加载,无法精确控制触发距离
- 不适合所有场景:对
<picture>元素或背景图片无效
提示:可以通过
document.querySelector('img').loading检测浏览器是否支持此特性
3. IntersectionObserver API方案
3.1 实现原理
IntersectionObserver API允许我们监听元素是否进入视口:
javascript复制const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px',
threshold: 0.01
});
关键配置项:
rootMargin:提前50px开始加载threshold:1%的可见面积即触发
3.2 Vue组件实现
完整组件代码:
vue复制<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const placeholder = ref('data:image/svg+xml;base64,...');
const lazyImages = ref([]);
const initLazyLoad = () => {
if (!('IntersectionObserver' in window)) {
loadAllImages();
return;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px',
threshold: 0.01
});
lazyImages.value.forEach(img => {
if (img && img.dataset.src) {
observer.observe(img);
}
});
};
const loadImage = (imgElement) => {
const img = new Image();
img.onload = () => {
imgElement.src = imgElement.dataset.src;
imgElement.classList.remove('lazy-img');
};
img.src = imgElement.dataset.src;
};
onMounted(() => {
setTimeout(initLazyLoad, 300);
});
onUnmounted(() => {
observer?.disconnect();
});
</script>
3.3 性能优化技巧
- 预加载:使用
new Image()预加载图片,避免直接设置src导致的渲染阻塞 - 内存管理:组件卸载时调用
observer.disconnect() - 降级方案:检测API支持情况,不支持时直接加载所有图片
- 滚动优化:使用
setTimeout延迟处理,避免频繁触发观察器回调
4. VueUse方案
4.1 安装与基础使用
首先安装VueUse:
bash复制npm install @vueuse/core
基本实现:
vue复制<script setup>
import { useIntersectionObserver } from '@vueuse/core';
const imageRefs = ref([]);
const lazyLoadImages = () => {
imageRefs.value.forEach((img) => {
const { stop } = useIntersectionObserver(
img,
([{ isIntersecting }]) => {
if (isIntersecting) {
loadImage(img);
stop();
}
},
{ rootMargin: '50px' }
);
});
};
</script>
4.2 优势分析
- 更简洁的API:无需手动管理Observer实例
- 自动卸载:组件卸载时自动清理
- 响应式集成:与Vue生命周期完美结合
- 额外功能:提供
isSupported等实用工具
4.3 高级配置
可以自定义更多参数:
javascript复制useIntersectionObserver(
img,
([{ isIntersecting }]) => { /* ... */ },
{
rootMargin: '100px',
threshold: 0.1,
flush: 'post' // 在Vue更新后执行
}
)
5. 方案对比与选型建议
| 特性 | 原生loading | IntersectionObserver | VueUse |
|---|---|---|---|
| 实现复杂度 | ★ | ★★★ | ★★ |
| 可控性 | ★★ | ★★★★★ | ★★★★ |
| 兼容性 | ★★★★ | ★★★★ | ★★★★ |
| 额外依赖 | 无 | 无 | VueUse |
| 功能扩展性 | ★ | ★★★★★ | ★★★★ |
选型建议:
- 简单场景:优先使用
loading="lazy",特别是内容型网站 - 需要精细控制:选择IntersectionObserver方案
- Vue3项目:推荐VueUse方案,代码更简洁
- 兼容旧浏览器:需要添加polyfill或降级方案
6. 进阶优化技巧
6.1 图片预加载策略
可以分级加载:
- 首屏图片:直接加载
- 次屏图片:使用懒加载
- 更远图片:延迟加载(滚动到附近再开始)
javascript复制const initLazyLoad = () => {
// 首屏图片直接加载
loadCriticalImages();
// 其他图片懒加载
setupLazyLoad();
}
6.2 响应式图片处理
结合srcset实现响应式懒加载:
html复制<img
data-srcset="small.jpg 480w, medium.jpg 768w, large.jpg 1024w"
data-sizes="(max-width: 600px) 480px, 800px"
src="placeholder.jpg"
class="lazy-img"
alt=""
/>
在加载时动态设置这些属性。
6.3 错误处理与重试
增强健壮性的错误处理:
javascript复制const loadImage = (imgElement, retry = 0) => {
const img = new Image();
img.onerror = () => {
if (retry < 3) {
setTimeout(() => loadImage(imgElement, retry + 1), 1000 * retry);
} else {
imgElement.src = fallbackImage;
}
};
img.src = imgElement.dataset.src;
};
7. 性能监控与指标
可以通过Performance API监控懒加载效果:
javascript复制const perfMark = (name) => {
if ('mark' in performance) {
performance.mark(name);
}
};
// 在图片加载回调中添加
img.onload = () => {
perfMark(`image_loaded_${img.id}`);
reportToAnalytics();
};
关键指标:
- LCP(最大内容绘制)
- 图片加载完成时间
- 视口内图片加载比例
8. 常见问题排查
8.1 图片不加载
可能原因:
- 观察器未正确绑定 - 检查ref是否获取到DOM
- 阈值设置过高 - 尝试降低threshold
- 容器溢出隐藏 - 确保root设置正确
8.2 图片闪烁
解决方案:
css复制.lazy-img {
opacity: 0;
transition: opacity 0.3s;
}
.lazy-img.loaded {
opacity: 1;
}
8.3 内存泄漏
确保在组件卸载时:
javascript复制onUnmounted(() => {
observer?.disconnect();
});
9. 服务端渲染(SSR)处理
在Nuxt等SSR框架中需要注意:
- 仅在客户端初始化懒加载
javascript复制onMounted(() => {
if (process.client) {
initLazyLoad();
}
});
- 避免hydration不匹配
html复制<img
:src="isHydrating ? realSrc : placeholder"
:data-src="realSrc"
/>
10. 移动端特别优化
- 更小的rootMargin:移动屏幕较小,建议设置为20-30px
- 网络感知:根据网络类型调整加载策略
javascript复制const isSlowNetwork = navigator.connection?.effectiveType.includes('2g');
if (isSlowNetwork) {
// 更保守的加载策略
}
- 触摸预加载:在touchstart时预加载可能点击的图片
在实际项目中,我通常会根据页面特点和用户设备动态调整懒加载策略。比如对于图片密集型页面,会结合IntersectionObserver和requestIdleCallback实现分级加载,确保关键内容优先渲染的同时,不影响页面滚动体验。