Vue3 + IntersectionObserver图片懒加载:从原理到内存管理的完整实践
在涉及大量图片展示的前端页面(如商品列表、相册、朋友圈)中,按需加载图片是提升首屏性能和节省带宽的常用手段。传统方案依赖 scroll 事件监听,需要手动计算元素位置,频繁触发又容易造成主线程卡顿。Vue3 应用可以借助浏览器原生的 IntersectionObserver API,以更低的资源消耗实现精准的懒加载。浏览器的 IntersectionObserver 会自动监测目标元素与视口的交叉状态,当图片进入可视区域(或露出指定比例)时才触发加载,离开视口后则自动暂停检查。相比 scroll + getBoundingClientRect 组合,它省去了大量重复计算,代码也更简洁。
下面是一个在 Vue3 + TypeScript 项目中实现高性能图片懒加载的完整示例,包含配置定义、DOM 引用获取、observer 初始化、资源回收以及组件生命周期绑定。
---
一、配置项与模板准备
首先在 <script setup lang="ts"> 中定义图片总数、占位图 URL 以及真实图片的地址生成函数。占位图在页面初始时直接渲染,真实地址通过 data-src 属性存放。
const TOTAL_ITEMS = 99;
const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg';
const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`;
模板中使用 v-for 循环生成图片列表,每个 img 标签通过 ref 函数将 DOM 元素收集到同一个数组中。注意 :src 绑定占位图,:data-src 绑定真实图片地址。
<div class="lazy-desc">图片懒加载功能:进入视口才加载真实图片,首屏加载速度提升 80%,节省带宽,避免页面卡顿</div>
<div class="card-list">
<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>
二、获取图片 DOM 引用
使用 ref<HTMLImageElement[]> 类型的数组来存储所有图片元素。Vue 在 v-for 中遇到 ref 时,会自动将每个循环生成的 DOM 推入该数组中。
const imgRefs = ref<HTMLImageElement[]>([]);
三、懒加载核心逻辑
定义一个 IntersectionObserver 实例,并在回调中遍历所有发生交叉变化的元素。只有当 entry.isIntersecting 为 true(即元素进入视口)时才处理。此时取出该元素的 img.dataset.src,将其赋值给 img.src,触发浏览器加载真实图片。加载后立即调用 observer.unobserve(img),避免已加载的图片继续占用观察资源。
观察者初始化时,配置 threshold: 0.01,表示图片露出 1% 就触发加载。这个值可以根据业务场景调整,值越小越早加载,但可能会浪费部分带宽。
let observer: IntersectionObserver | null = null;
async function initLazyLoad() {
observer = new IntersectionObserver(
(entries, observer) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const img = entry.target as HTMLImageElement;
const realSrc = img.dataset.src;
if (realSrc) img.src = realSrc;
observer.unobserve(img);
}
},
{ threshold: 0.01 }
);
// 等待 DOM 渲染完成后再开始监听,确保 imgRefs 中已有元素
await nextTick();
imgRefs.value.forEach((img) => observer?.observe(img));
}
四、资源清理,防止内存泄漏
组件销毁时必须断开所有观察,否则 observer 会一直持有对 DOM 的引用,导致内存泄漏。标准做法是:先遍历所有图片依次调用 unobserve,再调用 observer.disconnect() 完全销毁实例。
function destroyLazyLoad() {
if (!observer) return;
imgRefs.value.forEach((img) => observer!.unobserve(img));
observer.disconnect();
observer = null;
}
五、生命周期绑定
使用 onMounted 启动懒加载,使用 onUnmounted 执行清理。这两个钩子确保组件挂载后立刻生效,且在切换页面后不会残留监听。
onMounted(() => {
initLazyLoad();
});
onUnmounted(() => {
destroyLazyLoad();
});
六、完整脚本与样式片段(关键部分)
以下为整合后的脚本和模板结构,省略了与懒加载逻辑无关的交互代码。
<template>
<div class="app-content">
<div class="lazy-desc">🔥 图片懒加载功能 | 核心优势:进入视口才加载图片 → 首屏加载速度提升 80%、节省带宽资源、避免页面卡顿</div>
<div class="card-list">
<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">
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
const TOTAL_ITEMS = 99;
const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg';
const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`;
const imgRefs = ref<HTMLImageElement[]>([]);
let observer: IntersectionObserver | null = null;
async function initLazyLoad() {
observer = new IntersectionObserver(
(entries, observer) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const img = entry.target as HTMLImageElement;
const realSrc = img.dataset.src;
if (realSrc) img.src = realSrc;
observer.unobserve(img);
}
},
{ threshold: 0.01 }
);
await nextTick();
imgRefs.value.forEach((img) => observer?.observe(img));
}
function destroyLazyLoad() {
if (!observer) return;
imgRefs.value.forEach((img) => observer!.unobserve(img));
observer.disconnect();
observer = null;
}
onMounted(() => { initLazyLoad(); });
onUnmounted(() => { destroyLazyLoad(); });
</script>
<style lang="scss" scoped>
.app-content {
--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;
}
.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 img {
display: block;
width: 100%;
height: 100%;
transition: all 0.32s;
}
</style>
七、关键点总结
- 使用 IntersectionObserver 替代 scroll 事件,浏览器自动优化,性能远优于手动计算。
- 占位图 + data-src 模式实现初始轻量渲染,真实图片按需加载。
- 图片加载后立即调用 unobserve 释放单个元素观察,避免浪费。
- 组件销毁时务必执行 disconnect 并置空 observer,防止内存泄漏。
- threshold: 0.01 适合大多数场景,可在首屏加载与带宽消耗之间取得平衡。
这套方案可直接嵌入商品列表、无限滚动相册等需要加载大量图片的 Vue3 项目中,无需第三方库,仅依赖浏览器原生 API,兼容现代主流浏览器。对于需要支持老版本浏览器的项目,可考虑给 IntersectionObserver 添加 polyfill 或降级为 scroll 监听方案。
Re: Vue3 + IntersectionObserver图片懒加载:从原理到内存管理的完整实践
这篇分享非常实用,把 IntersectionObserver 和 Vue3 的生命周期结合得很清晰。尤其是你特意在 `initLazyLoad` 里用 `nextTick` 等待 DOM 渲染,避免 `imgRefs` 为空,这个小细节很关键,新手很容易忽略。另外在 `destroyLazyLoad` 里逐个 `unobserve` 再加 `disconnect`,这种双重释放确实能彻底防止内存泄漏,点赞。 想请教一下:如果列表是动态加载更多(比如滚动到底部追加新图片),你一般是怎么处理新加入的元素的?是重新调用 `initLazyLoad` 还是单独对新元素调用 `observer.observe`?另外,你这里配的 `threshold: 0.01` 会让图片刚露头就加载,如果用户滚动很快,会不会出现大量图片同时请求的情况?有没有考虑配合 `rootMargin` 做预加载缓冲?Re: Vue3 + IntersectionObserver图片懒加载:从原理到内存管理的完整实践
这个实践非常清晰,结合 Vue3 的 ref 和生命周期钩子来管理 IntersectionObserver,代码结构也很整洁。用 data-src 存放真实地址、加载后立即 unobserve 的做法,既避免了重复加载也减少了观察器负担,能有效防止内存泄漏。想请教一下:如果之后有动态新增的图片(比如用户滚动到底部加载更多),你建议在新增元素时再次调用 observer.observe 吗?还是说可以在某个全局的容器上使用 MutationObserver 来自动监听?Re: Vue3 + IntersectionObserver图片懒加载:从原理到内存管理的完整实践
很详细的实践分享,代码结构清晰,从配置到清理都覆盖了。有个小细节想请教:如果列表中的图片是异步加载的(比如分页或者无限滚动后新增的),initLazyLoad 里的 nextTick 只执行一次,新追加的图片该怎么被 observer 观察到?是在新增后手动调用 observer.observe 吗?另外 threshold: 0.01 对于首屏性能优化来说,1% 的露出触发会不会偏慢?有些场景可能希望图片即将进入视口时就预加载,不知道楼主有没有试过 rootMargin 参数的搭配?
页:
[1]