查看: 366|回复: 1

Vue3 + IntersectionObserver图片懒加载:从原理到内存管理的完整实践

[复制链接]
发表于 3 天前 | 显示全部楼层 |阅读模式
在涉及大量图片展示的前端页面(如商品列表、相册、朋友圈)中,按需加载图片是提升首屏性能和节省带宽的常用手段。传统方案依赖 scroll 事件监听,需要手动计算元素位置,频繁触发又容易造成主线程卡顿。Vue3 应用可以借助浏览器原生的 IntersectionObserver API,以更低的资源消耗实现精准的懒加载。

浏览器的 IntersectionObserver 会自动监测目标元素与视口的交叉状态,当图片进入可视区域(或露出指定比例)时才触发加载,离开视口后则自动暂停检查。相比 scroll + getBoundingClientRect 组合,它省去了大量重复计算,代码也更简洁。

下面是一个在 Vue3 + TypeScript 项目中实现高性能图片懒加载的完整示例,包含配置定义、DOM 引用获取、observer 初始化、资源回收以及组件生命周期绑定。

---

一、配置项与模板准备

首先在 <script setup lang="ts"> 中定义图片总数、占位图 URL 以及真实图片的地址生成函数。占位图在页面初始时直接渲染,真实地址通过 data-src 属性存放。
  1. const TOTAL_ITEMS = 99;
  2. const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg';
  3. const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`;
复制代码

模板中使用 v-for 循环生成图片列表,每个 img 标签通过 ref 函数将 DOM 元素收集到同一个数组中。注意 :src 绑定占位图,:data-src 绑定真实图片地址。
  1. <div class="lazy-desc">图片懒加载功能:进入视口才加载真实图片,首屏加载速度提升 80%,节省带宽,避免页面卡顿</div>
  2. <div class="card-list">
  3.   <div class="item" v-for="(item, index) in TOTAL_ITEMS" :key="index">
  4.     <img ref="imgRefs" :src="DEFAULT_IMG" alt="image" :data-src="IMG_URL_TEMPLATE(item)" />
  5.   </div>
  6. </div>
复制代码

二、获取图片 DOM 引用

使用 ref<HTMLImageElement[]> 类型的数组来存储所有图片元素。Vue 在 v-for 中遇到 ref 时,会自动将每个循环生成的 DOM 推入该数组中。
  1. const imgRefs = ref<HTMLImageElement[]>([]);
复制代码

三、懒加载核心逻辑

定义一个 IntersectionObserver 实例,并在回调中遍历所有发生交叉变化的元素。只有当 entry.isIntersecting 为 true(即元素进入视口)时才处理。此时取出该元素的 img.dataset.src,将其赋值给 img.src,触发浏览器加载真实图片。加载后立即调用 observer.unobserve(img),避免已加载的图片继续占用观察资源。

观察者初始化时,配置 threshold: 0.01,表示图片露出 1% 就触发加载。这个值可以根据业务场景调整,值越小越早加载,但可能会浪费部分带宽。
  1. let observer: IntersectionObserver | null = null;
  2. async function initLazyLoad() {
  3.   observer = new IntersectionObserver(
  4.     (entries, observer) => {
  5.       for (const entry of entries) {
  6.         if (!entry.isIntersecting) continue;
  7.         const img = entry.target as HTMLImageElement;
  8.         const realSrc = img.dataset.src;
  9.         if (realSrc) img.src = realSrc;
  10.         observer.unobserve(img);
  11.       }
  12.     },
  13.     { threshold: 0.01 }
  14.   );
  15.   // 等待 DOM 渲染完成后再开始监听,确保 imgRefs 中已有元素
  16.   await nextTick();
  17.   imgRefs.value.forEach((img) => observer?.observe(img));
  18. }
复制代码

四、资源清理,防止内存泄漏

组件销毁时必须断开所有观察,否则 observer 会一直持有对 DOM 的引用,导致内存泄漏。标准做法是:先遍历所有图片依次调用 unobserve,再调用 observer.disconnect() 完全销毁实例。
  1. function destroyLazyLoad() {
  2.   if (!observer) return;
  3.   imgRefs.value.forEach((img) => observer!.unobserve(img));
  4.   observer.disconnect();
  5.   observer = null;
  6. }
复制代码

五、生命周期绑定

使用 onMounted 启动懒加载,使用 onUnmounted 执行清理。这两个钩子确保组件挂载后立刻生效,且在切换页面后不会残留监听。
  1. onMounted(() => {
  2.   initLazyLoad();
  3. });
  4. onUnmounted(() => {
  5.   destroyLazyLoad();
  6. });
复制代码

六、完整脚本与样式片段(关键部分)

以下为整合后的脚本和模板结构,省略了与懒加载逻辑无关的交互代码。
  1. <template>
  2.   <div class="app-content">
  3.     <div class="lazy-desc">🔥 图片懒加载功能 | 核心优势:进入视口才加载图片 → 首屏加载速度提升 80%、节省带宽资源、避免页面卡顿</div>
  4.     <div class="card-list">
  5.       <div class="item" v-for="(item, index) in TOTAL_ITEMS" :key="index">
  6.         <img ref="imgRefs" :src="DEFAULT_IMG" alt="image" :data-src="IMG_URL_TEMPLATE(item)" />
  7.       </div>
  8.     </div>
  9.   </div>
  10. </template>
  11. <script setup lang="ts">
  12. import { ref, nextTick, onMounted, onUnmounted } from 'vue';
  13. const TOTAL_ITEMS = 99;
  14. const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg';
  15. const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`;
  16. const imgRefs = ref<HTMLImageElement[]>([]);
  17. let observer: IntersectionObserver | null = null;
  18. async function initLazyLoad() {
  19.   observer = new IntersectionObserver(
  20.     (entries, observer) => {
  21.       for (const entry of entries) {
  22.         if (!entry.isIntersecting) continue;
  23.         const img = entry.target as HTMLImageElement;
  24.         const realSrc = img.dataset.src;
  25.         if (realSrc) img.src = realSrc;
  26.         observer.unobserve(img);
  27.       }
  28.     },
  29.     { threshold: 0.01 }
  30.   );
  31.   await nextTick();
  32.   imgRefs.value.forEach((img) => observer?.observe(img));
  33. }
  34. function destroyLazyLoad() {
  35.   if (!observer) return;
  36.   imgRefs.value.forEach((img) => observer!.unobserve(img));
  37.   observer.disconnect();
  38.   observer = null;
  39. }
  40. onMounted(() => { initLazyLoad(); });
  41. onUnmounted(() => { destroyLazyLoad(); });
  42. </script>
  43. <style lang="scss" scoped>
  44. .app-content {
  45.   --item-gap: 16px;
  46.   --item-min-width: 150px;
  47.   --item-height: 300px;
  48. }
  49. .lazy-desc {
  50.   margin-bottom: 16px;
  51.   padding: 8px 16px;
  52.   background: #f0f9ff;
  53.   border-left: 4px solid #409eff;
  54.   border-radius: 4px;
  55.   color: #1f2937;
  56.   font-size: 14px;
  57.   font-weight: 500;
  58. }
  59. .card-list {
  60.   display: grid;
  61.   grid-template-columns: repeat(auto-fill, minmax(var(--item-min-width), 1fr));
  62.   gap: var(--item-gap);
  63. }
  64. .card-list .item {
  65.   cursor: pointer;
  66.   height: var(--item-height);
  67.   border-radius: 4px;
  68.   box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
  69.   overflow: hidden;
  70. }
  71. .card-list .item img {
  72.   display: block;
  73.   width: 100%;
  74.   height: 100%;
  75.   transition: all 0.32s;
  76. }
  77. </style>
复制代码

七、关键点总结

- 使用 IntersectionObserver 替代 scroll 事件,浏览器自动优化,性能远优于手动计算。
- 占位图 + data-src 模式实现初始轻量渲染,真实图片按需加载。
- 图片加载后立即调用 unobserve 释放单个元素观察,避免浪费。
- 组件销毁时务必执行 disconnect 并置空 observer,防止内存泄漏。
- threshold: 0.01 适合大多数场景,可在首屏加载与带宽消耗之间取得平衡。

这套方案可直接嵌入商品列表、无限滚动相册等需要加载大量图片的 Vue3 项目中,无需第三方库,仅依赖浏览器原生 API,兼容现代主流浏览器。对于需要支持老版本浏览器的项目,可考虑给 IntersectionObserver 添加 polyfill 或降级为 scroll 监听方案。
回复

使用道具 举报

发表于 3 天前 | 显示全部楼层

Re: Vue3 + IntersectionObserver图片懒加载:从原理到内存管理的完整实践

这篇分享非常实用,把 IntersectionObserver 和 Vue3 的生命周期结合得很清晰。尤其是你特意在 `initLazyLoad` 里用 `nextTick` 等待 DOM 渲染,避免 `imgRefs` 为空,这个小细节很关键,新手很容易忽略。另外在 `destroyLazyLoad` 里逐个 `unobserve` 再加 `disconnect`,这种双重释放确实能彻底防止内存泄漏,点赞。 想请教一下:如果列表是动态加载更多(比如滚动到底部追加新图片),你一般是怎么处理新加入的元素的?是重新调用 `initLazyLoad` 还是单独对新元素调用 `observer.observe`?另外,你这里配的 `threshold: 0.01` 会让图片刚露头就加载,如果用户滚动很快,会不会出现大量图片同时请求的情况?有没有考虑配合 `rootMargin` 做预加载缓冲?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-14 05:27 , Processed in 0.029106 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部