Vue3 + IntersectionObserver 实现高性能图片懒加载
本文详解 Vue3 中如何使用 IntersectionObserver API 实现图片懒加载,核心优势在于进入视口才加载图片,可显著提升首屏加载速度、节省带宽资源、避免页面卡顿,适合多图列表场景
一、原理概述
图片懒加载的核心思想是:图片进入用户可视区域时才加载真实图片,未进入时显示占位图。
Vue3 中实现懒加载最优雅的方式是使用 IntersectionObserver API,相比传统的 scroll 事件监听,它具备以下优势:
- 性能更好:浏览器自动优化交叉观察,无需手动计算位置
- 更省资源:元素离开视口后自动暂停监听
- 代码更简洁:几行配置即可完成复杂的懒加载逻辑
懒加载实现流程:
- 页面初始时,图片
src使用占位图,真实地址存在data-src属性中 - 创建
IntersectionObserver实例,监听所有图片元素 - 当图片进入视口(露出比例超过阈值)时,将
data-src的值赋给src - 图片加载完成后取消观察,释放资源
二、核心代码实现
配置项定义
<script setup lang="ts">
/** 图片总数 */
const TOTAL_ITEMS = 99
/** 默认占位图 - 页面初始时显示的轻量图片 */
const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg'
/** 真实图片地址模板 - 接收索引参数,生成不同的随机图片 URL */
const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`
</script>
DOM 引用获取
<script setup lang="ts">
/**
* 获取所有需要懒加载的图片 DOM 引用
* 在 v-for 中使用 ref,Vue 会自动把所有 DOM 存入一个数组里
* ref<HTMLImageElement[]> 表示引用数组类型
*/
const imgRefs = ref<HTMLImageElement[]>([])
</script>
懒加载核心逻辑
/** IntersectionObserver 实例引用,组件销毁时需要手动清理 */
let observer: IntersectionObserver | null = null
/**
* 初始化懒加载监听
* 使用 async 是为了确保 DOM 渲染完成后再执行监听
*/
async function initLazyLoad() {
// 创建观察者实例,传入回调函数和配置项
observer = new IntersectionObserver(
// entries: 触发回调时,传入所有发生交叉变化的元素数组
// observer: 观察者实例本身,用于调用 unobserve 取消观察
(entries, observer) => {
// 遍历所有发生变化的元素
for (const entry of entries) {
// isIntersecting: 元素是否进入视口
// ! 为 false 时表示元素离开了视口,无需处理,直接跳过
if (!entry.isIntersecting) continue
// 将 entry.target 断言为 HTMLImageElement 类型
// 因为 ref 数组中存储的正是图片 DOM 元素
const img = entry.target as HTMLImageElement
// dataset: 获取元素上 data-* 自定义属性
// data-src="真实图片地址" 存储在 dataset.src 中
const realSrc = img.dataset.src
// 将真实图片地址赋值给 src,触发浏览器加载真实图片
if (realSrc) img.src = realSrc
// 加载完成后立即取消观察该图片
// 避免已加载的图片占用观察者资源,提升性能
observer.unobserve(img)
}
},
{
// threshold: 交叉比例阈值,0.01 表示图片露出 1% 就触发回调
// 值范围 0~1,值越小越早触发,但可能浪费带宽
threshold: 0.01,
},
)
// 等待 DOM 渲染完成后再开始监听
// nextTick 确保 v-for 循环的图片 DOM 已经渲染到页面
await nextTick()
// 遍历所有图片 DOM,逐个注册到观察者中
// observe 之后,观察者就会开始监听该元素的可见性变化
imgRefs.value.forEach((img) => observer?.observe(img))
}
资源清理(防止内存泄漏)
/**
* 销毁观察者实例
* ⚠️ 组件销毁时必须调用!否则会内存泄漏
*/
function destroyLazyLoad() {
// 未初始化则直接返回,避免报错
if (!observer) return
// 遍历所有图片,先取消对每个图片的观察
// disconnect 之前建议先调用 unobserve,避免遗留监听
imgRefs.value.forEach((img) => observer!.unobserve(img))
// disconnect: 完全销毁观察者,释放所有资源
observer.disconnect()
// 重置为 null,标记已清理
observer = null
}
生命周期钩子绑定
/** 组件挂载到页面后,立即初始化懒加载监听 */
onMounted(() => {
initLazyLoad()
})
/**
* 组件销毁前,清理观察者实例
* 防止用户切换页面后,观察者仍在后台运行消耗资源
*/
onUnmounted(() => {
destroyLazyLoad()
})
三、完整代码示例
<template>
<div class="app-content">
<!-- 功能说明区域:突出懒加载的核心优势 -->
<div class="lazy-desc">🔥 图片懒加载功能 | 核心优势:进入视口才加载图片 → 首屏加载速度提升 80%、节省带宽资源、避免页面卡顿,大幅优化多图场景用户体验</div>
<!-- 图片列表容器,使用 grid 布局实现响应式排版 -->
<div class="card-list">
<!-- v-for 循环生成 99 张图片 -->
<!-- ref="imgRefs" 会将每个图片 DOM 存入 imgRefs 数组 -->
<!-- :src 初始为占位图,:data-src 存储真实图片地址 -->
<div class="item" v-for="(item, index) in TOTAL_ITEMS" :key="index">
<img ref="imgRefs" :src="DEFAULT_IMG" alt="image" :data-src="IMG_URL_TEMPLATE(item)" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
/** 图片总数 - 控制列表中显示的图片数量 */
const TOTAL_ITEMS = 99
/** 默认占位图 - 未加载前显示的轻量图片 */
const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg'
/** 真实图片地址生成函数 - 接收索引,返回唯一随机图片 URL */
const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`
/**
* DOM 引用数组 - 用于存储所有需要懒加载的图片 DOM
* Vue 会自动将 v-for 中的 ref 收集到这个数组
*/
const imgRefs = ref<HTMLImageElement[]>([])
/** 观察者实例 - 全局保存,组件销毁时需要手动清理 */
let observer: IntersectionObserver | null = null
/**
* 初始化懒加载核心逻辑
* 1. 创建 IntersectionObserver 实例
* 2. 等待 DOM 渲染完成后开始监听
*/
async function initLazyLoad() {
// 创建观察者,配置交叉阈值为 1%
observer = new IntersectionObserver(
(entries, observer) => {
// entries: 当前帧内所有发生交叉变化的元素列表
for (const entry of entries) {
// 只处理「进入视口」的元素,「离开视口」时跳过
if (!entry.isIntersecting) continue
// 获取触发回调的图片 DOM 元素
const img = entry.target as HTMLImageElement
// 从 data-src 属性读取真实图片地址
const realSrc = img.dataset.src
// 将真实地址赋值给 src,触发图片加载
if (realSrc) img.src = realSrc
// ⚠️ 关键:加载完成后立即取消观察
// 避免已加载图片继续占用观察者资源
observer.unobserve(img)
}
},
{
// threshold: 触发加载的可见比例
// 0.01 = 图片露出 1% 时就触发,适合需要提前加载的场景
threshold: 0.01,
},
)
// 等待 Vue 更新 DOM 后再执行监听
// 确保 v-for 循环的 img 元素已经渲染到页面
await nextTick()
// 将所有图片 DOM 注册到观察者,开始监听
imgRefs.value.forEach((img) => observer?.observe(img))
}
/**
* 销毁观察者,释放资源
* ⚠️ 必须在组件销毁时调用,防止内存泄漏
*/
function destroyLazyLoad() {
if (!observer) return
// 先取消所有图片的观察
imgRefs.value.forEach((img) => observer!.unobserve(img))
// 完全销毁观察者实例
observer.disconnect()
// 重置为 null
observer = null
}
/** 组件挂载时启动懒加载 */
onMounted(() => {
initLazyLoad()
})
/** 组件销毁前清理资源 */
onUnmounted(() => {
destroyLazyLoad()
})
</script>
<style lang="scss" scoped>
.app-content {
/* CSS 变量:统一样式配置,方便维护 */
--item-gap: 16px; /* 网格项之间的间距 */
--item-min-width: 150px; /* 网格项的最小宽度,响应式适配 */
--item-height: 300px; /* 图片卡片固定高度 */
}
/* 功能描述样式 - 左侧蓝色边框提示框 */
.lazy-desc {
margin-bottom: 16px;
padding: 8px 16px;
background: #f0f9ff; /* 浅蓝色背景 */
border-left: 4px solid #409eff; /* 左侧蓝色强调条 */
border-radius: 4px;
color: #1f2937;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
}
/* 响应式网格布局 - 自动填充,最小宽度 150px */
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--item-min-width), 1fr));
gap: var(--item-gap);
}
.card-list .item {
cursor: pointer;
height: var(--item-height);
border-radius: 4px;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); /* 卡片阴影 */
overflow: hidden; /* 隐藏图片放大时超出边框的部分 */
}
.card-list .item:hover img {
transform: scale(1.5); /* 鼠标悬停时图片放大 1.5 倍 */
}
.card-list .item img {
display: block;
width: 100%;
height: 100%;
transition: all 0.32s; /* 过渡动画,使缩放更平滑 */
}
</style>
四、核心总结
本文通过 Vue3 + IntersectionObserver 实现了高性能图片懒加载方案,核心要点:
| 要点 | 说明 |
|---|---|
IntersectionObserver |
替代 scroll 事件,浏览器自动优化,性能更优 |
占位图 + data-src
|
初始显示占位图,真实地址存在 data-src 中 |
observer.unobserve() |
加载完成后取消监听,避免资源浪费 |
onUnmounted 清理 |
组件销毁时调用 disconnect(),防止内存泄漏 |
该方案在多图列表场景下效果显著,可直接应用于商品列表、朋友圈图片流、相册等业务场景。