在涉及大量图片展示的前端页面(如商品列表、相册、朋友圈)中,按需加载图片是提升首屏性能和节省带宽的常用手段。传统方案依赖 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 监听方案。 |