Vue3 + IntersectionObserver 实现高性能图片懒加载

本文详解 Vue3 中如何使用 IntersectionObserver API 实现图片懒加载,核心优势在于进入视口才加载图片,可显著提升首屏加载速度、节省带宽资源、避免页面卡顿,适合多图列表场景

一、原理概述

图片懒加载的核心思想是:图片进入用户可视区域时才加载真实图片,未进入时显示占位图

Vue3 中实现懒加载最优雅的方式是使用 IntersectionObserver API,相比传统的 scroll 事件监听,它具备以下优势:

  • 性能更好:浏览器自动优化交叉观察,无需手动计算位置
  • 更省资源:元素离开视口后自动暂停监听
  • 代码更简洁:几行配置即可完成复杂的懒加载逻辑

懒加载实现流程:

  1. 页面初始时,图片 src 使用占位图,真实地址存在 data-src 属性中
  2. 创建 IntersectionObserver 实例,监听所有图片元素
  3. 当图片进入视口(露出比例超过阈值)时,将 data-src 的值赋给 src
  4. 图片加载完成后取消观察,释放资源

二、核心代码实现

配置项定义

vue 复制代码
<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 引用获取

vue 复制代码
<script setup lang="ts">
/**
 * 获取所有需要懒加载的图片 DOM 引用
 * 在 v-for 中使用 ref,Vue 会自动把所有 DOM 存入一个数组里
 * ref<HTMLImageElement[]> 表示引用数组类型
 */
const imgRefs = ref<HTMLImageElement[]>([])
</script>

懒加载核心逻辑

typescript 复制代码
/** 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))
}

资源清理(防止内存泄漏)

typescript 复制代码
/**
 * 销毁观察者实例
 * ⚠️ 组件销毁时必须调用!否则会内存泄漏
 */
function destroyLazyLoad() {
  // 未初始化则直接返回,避免报错
  if (!observer) return

  // 遍历所有图片,先取消对每个图片的观察
  // disconnect 之前建议先调用 unobserve,避免遗留监听
  imgRefs.value.forEach((img) => observer!.unobserve(img))

  // disconnect: 完全销毁观察者,释放所有资源
  observer.disconnect()

  // 重置为 null,标记已清理
  observer = null
}

生命周期钩子绑定

typescript 复制代码
/** 组件挂载到页面后,立即初始化懒加载监听 */
onMounted(() => {
  initLazyLoad()
})

/**
 * 组件销毁前,清理观察者实例
 * 防止用户切换页面后,观察者仍在后台运行消耗资源
 */
onUnmounted(() => {
  destroyLazyLoad()
})

三、完整代码示例

vue 复制代码
<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(),防止内存泄漏

该方案在多图列表场景下效果显著,可直接应用于商品列表、朋友圈图片流、相册等业务场景。

相关推荐
sakiko_2 小时前
UIKit学习笔记3-布局、滚动视图、隐藏或显示视图
前端·笔记·学习·objective-c·swift·uikit
有一个好名字2 小时前
Agent Loop —— 一切从那个 while 循环开始
前端·javascript·chrome
一天睡25小时2 小时前
Claude Code 指令入门教程
前端
yingyima3 小时前
正则表达式实战:从日志中精准提取关键字段
前端
TeamDev3 小时前
如何在 DotNetBrowser 中使用本地 AI 模型
前端·后端·.net
谢尔登3 小时前
10_从 React Hooks 本质看 useState
前端·ubuntu·react.js
辰同学ovo3 小时前
从全局登录状态管理学习 Redux
前端·javascript·学习·react.js
陈随易3 小时前
2年没用Nodejs了,Bun很香
前端·后端·程序员
donecoding4 小时前
Corepack 完全解析:从懵到懂,包管理器自由了
前端·node.js·前端工程化