1. 为什么要实现懒加载?
在现代 Web 应用中,图片是流量消耗大户。如果页面包含大量图片(如相册、商品列表),首屏加载时间会显著增加。懒加载的核心思想是:
- 延迟加载:图片进入可视区域时才开始加载
- 提升性能:减少首屏请求数,节省用户流量
- 优化体验:快速响应,避免页面卡顿
2. 核心原理
2.1 技术栈选择
- Vue3 自定义指令:
v-lazy,直接操作 DOM 元素 - Intersection Observer API:现代浏览器原生 API,监听元素是否进入视口
- 图片预加载:创建
Image对象提前加载,避免直接修改src导致闪烁
2.2 工作流程
1. 元素初始化 → 设置默认 loading 图片
2. 开启 IntersectionObserver 监听
3. 元素进入视口 (isIntersecting = true) → 触发加载
4. 预加载图片 → onload → 设置真实 src
5. 预加载失败 → onerror → 设置错误图
6. 关闭监听,防止重复加载
3. 完整代码实现
// src/directives/lazy.js
// 默认图片(建议使用 Base64 或本地路径)
const defaultLoading = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
const defaultError = 'https://cdn.example.com/images/error.png'
const imgLazy = {
mounted(el, binding) {
let observer = null
// 1. 解析指令参数(支持对象和字符串两种形式)
const { src, loading, error } =
typeof binding.value === 'object'
? binding.value
: { src: binding.value }
// 2. 初始化:设置加载中的占位图
el.src = loading || defaultLoading
// 3. 图片预加载函数(核心逻辑)
const imgLoad = () => {
const img = new Image() // 创建虚拟 Image 对象预加载
img.src = src
// 加载成功
img.onload = () => {
el.src = src
// 可选:添加淡入动画
el.style.transition = 'opacity 0.3s'
el.style.opacity = '1'
}
// 加载失败
img.onerror = () => {
el.src = error || defaultError
}
}
// 4. 创建 IntersectionObserver 监听
observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
imgLoad() // 进入视口,开始加载
observer.unobserve(el) // 关闭监听,避免重复加载
}
}, {
rootMargin: '100px', // 提前 100px 开始加载,提升体验
threshold: 0.01 // 只要有一点点可见就触发
})
// 5. 开启监听,并挂载到元素上(方便卸载时清理)
observer.observe(el)
el._imgObserver = observer
},
unmounted(el) {
// 6. 清理监听器,防止内存泄漏
el._imgObserver?.unobserve(el)
el._imgObserver = null
}
}
export default imgLazy
4. 在 Vue3 中使用
4.1 全局注册
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import imgLazy from './directives/lazy'
const app = createApp(App)
app.directive('lazy', imgLazy)
app.mount('#app')
4.2 组件内使用
<template>
<div class="gallery">
<!-- 基础用法:直接传 URL -->
<img v-lazy="item.url" v-for="item in list" :key="item.id" />
<!-- 进阶用法:配置 loading 和 error -->
<img
v-lazy="{
src: item.url,
loading: '/images/loading.gif',
error: '/images/error.png'
}"
v-for="item in list"
:key="item.id"
/>
</div>
</template>
<script>
export default {
data() {
return {
list: [
{ id: 1, url: 'https://example.com/img1.jpg' },
{ id: 2, url: 'https://example.com/img2.jpg' },
// ... 更多图片
]
}
}
}
</script>
<style>
/* 可选:添加加载动画 */
img {
opacity: 0;
transition: opacity 0.3s;
}
img[src] {
opacity: 1;
}
</style>
5. 进阶优化建议
5.1 性能优化
// 优化的 observer 配置
const observer = new IntersectionObserver(callback, {
rootMargin: '200px', // 更大的预加载区域
threshold: 0.1, // 10% 可见时触发
root: null // 相对于视口
})
5.2 支持 WebP 和懒加载降级
const imgLoad = () => {
const img = new Image()
// 检测浏览器是否支持 WebP
const supportsWebP = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0
img.src = supportsWebP ? src.replace('.jpg', '.webp') : src
img.onload = () => { el.src = img.src }
img.onerror = () => { el.src = error || defaultError }
}
5.3 添加加载完成回调
// 扩展指令支持回调
mounted(el, binding) {
const { src, loading, error, onLoaded, onError } = binding.value
const imgLoad = () => {
const img = new Image()
img.src = src
img.onload = () => {
el.src = src
onLoaded?.(el) // 加载完成回调
}
img.onerror = () => {
el.src = error || defaultError
onError?.(el) // 加载失败回调
}
}
// ... 其余代码
}
使用:
<img v-lazy="{
src: item.url,
onLoaded: (el) => console.log('图片加载完成', el),
onError: (el) => console.log('图片加载失败', el)
}" />
6. 注意事项与最佳实践
6.1 内存泄漏防范
- ✅ 必须在
unmounted时调用unobserve - ✅ 避免在组件卸载后继续操作 DOM
- ✅ 使用
?.可选链操作符防止报错
6.2 图片格式建议
- loading 图片:使用 1x1 像素的透明 GIF 或 Base64,避免额外请求
- error 图片:使用稳定的 CDN 地址或本地资源
- 真实图片:优先使用 WebP 格式,体积更小