基于Vue3+IntersectionObserver實(shí)現(xiàn)高性能圖片懶加載
一、原理概述
圖片懶加載的核心思想是:圖片進(jìn)入用戶可視區(qū)域時(shí)才加載真實(shí)圖片,未進(jìn)入時(shí)顯示占位圖。
Vue3 中實(shí)現(xiàn)懶加載最優(yōu)雅的方式是使用 IntersectionObserver API,相比傳統(tǒng)的 scroll 事件監(jiān)聽,它具備以下優(yōu)勢(shì):
- 性能更好:瀏覽器自動(dòng)優(yōu)化交叉觀察,無需手動(dòng)計(jì)算位置
- 更省資源:元素離開視口后自動(dòng)暫停監(jiān)聽
- 代碼更簡(jiǎn)潔:幾行配置即可完成復(fù)雜的懶加載邏輯
懶加載實(shí)現(xiàn)流程:
- 頁面初始時(shí),圖片
src使用占位圖,真實(shí)地址存在data-src屬性中 - 創(chuàng)建
IntersectionObserver實(shí)例,監(jiān)聽所有圖片元素 - 當(dāng)圖片進(jìn)入視口(露出比例超過閾值)時(shí),將
data-src的值賦給src - 圖片加載完成后取消觀察,釋放資源
二、核心代碼實(shí)現(xiàn)
配置項(xiàng)定義
<script setup lang="ts">
/** 圖片總數(shù) */
const TOTAL_ITEMS = 99
/** 默認(rèn)占位圖 - 頁面初始時(shí)顯示的輕量圖片 */
const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg'
/** 真實(shí)圖片地址模板 - 接收索引參數(shù),生成不同的隨機(jī)圖片 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 會(huì)自動(dòng)把所有 DOM 存入一個(gè)數(shù)組里 * ref<HTMLImageElement[]> 表示引用數(shù)組類型 */ const imgRefs = ref<HTMLImageElement[]>([]) </script>
懶加載核心邏輯
/** IntersectionObserver 實(shí)例引用,組件銷毀時(shí)需要手動(dòng)清理 */
let observer: IntersectionObserver | null = null
/**
* 初始化懶加載監(jiān)聽
* 使用 async 是為了確保 DOM 渲染完成后再執(zhí)行監(jiān)聽
*/
async function initLazyLoad() {
// 創(chuàng)建觀察者實(shí)例,傳入回調(diào)函數(shù)和配置項(xiàng)
observer = new IntersectionObserver(
// entries: 觸發(fā)回調(diào)時(shí),傳入所有發(fā)生交叉變化的元素?cái)?shù)組
// observer: 觀察者實(shí)例本身,用于調(diào)用 unobserve 取消觀察
(entries, observer) => {
// 遍歷所有發(fā)生變化的元素
for (const entry of entries) {
// isIntersecting: 元素是否進(jìn)入視口
// ! 為 false 時(shí)表示元素離開了視口,無需處理,直接跳過
if (!entry.isIntersecting) continue
// 將 entry.target 斷言為 HTMLImageElement 類型
// 因?yàn)?ref 數(shù)組中存儲(chǔ)的正是圖片 DOM 元素
const img = entry.target as HTMLImageElement
// dataset: 獲取元素上 data-* 自定義屬性
// data-src="真實(shí)圖片地址" 存儲(chǔ)在 dataset.src 中
const realSrc = img.dataset.src
// 將真實(shí)圖片地址賦值給 src,觸發(fā)瀏覽器加載真實(shí)圖片
if (realSrc) img.src = realSrc
// 加載完成后立即取消觀察該圖片
// 避免已加載的圖片占用觀察者資源,提升性能
observer.unobserve(img)
}
},
{
// threshold: 交叉比例閾值,0.01 表示圖片露出 1% 就觸發(fā)回調(diào)
// 值范圍 0~1,值越小越早觸發(fā),但可能浪費(fèi)帶寬
threshold: 0.01,
},
)
// 等待 DOM 渲染完成后再開始監(jiān)聽
// nextTick 確保 v-for 循環(huán)的圖片 DOM 已經(jīng)渲染到頁面
await nextTick()
// 遍歷所有圖片 DOM,逐個(gè)注冊(cè)到觀察者中
// observe 之后,觀察者就會(huì)開始監(jiān)聽該元素的可見性變化
imgRefs.value.forEach((img) => observer?.observe(img))
}
資源清理(防止內(nèi)存泄漏)
/**
* 銷毀觀察者實(shí)例
* ?? 組件銷毀時(shí)必須調(diào)用!否則會(huì)內(nèi)存泄漏
*/
function destroyLazyLoad() {
// 未初始化則直接返回,避免報(bào)錯(cuò)
if (!observer) return
// 遍歷所有圖片,先取消對(duì)每個(gè)圖片的觀察
// disconnect 之前建議先調(diào)用 unobserve,避免遺留監(jiān)聽
imgRefs.value.forEach((img) => observer!.unobserve(img))
// disconnect: 完全銷毀觀察者,釋放所有資源
observer.disconnect()
// 重置為 null,標(biāo)記已清理
observer = null
}
生命周期鉤子綁定
/** 組件掛載到頁面后,立即初始化懶加載監(jiān)聽 */
onMounted(() => {
initLazyLoad()
})
/**
* 組件銷毀前,清理觀察者實(shí)例
* 防止用戶切換頁面后,觀察者仍在后臺(tái)運(yùn)行消耗資源
*/
onUnmounted(() => {
destroyLazyLoad()
})
三、完整代碼示例
<template>
<div class="app-content">
<!-- 功能說明區(qū)域:突出懶加載的核心優(yōu)勢(shì) -->
<div class="lazy-desc">?? 圖片懶加載功能 | 核心優(yōu)勢(shì):進(jìn)入視口才加載圖片 → 首屏加載速度提升 80%、節(jié)省帶寬資源、避免頁面卡頓,大幅優(yōu)化多圖場(chǎng)景用戶體驗(yàn)</div>
<!-- 圖片列表容器,使用 grid 布局實(shí)現(xiàn)響應(yīng)式排版 -->
<div class="card-list">
<!-- v-for 循環(huán)生成 99 張圖片 -->
<!-- ref="imgRefs" 會(huì)將每個(gè)圖片 DOM 存入 imgRefs 數(shù)組 -->
<!-- :src 初始為占位圖,:data-src 存儲(chǔ)真實(shí)圖片地址 -->
<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">
/** 圖片總數(shù) - 控制列表中顯示的圖片數(shù)量 */
const TOTAL_ITEMS = 99
/** 默認(rèn)占位圖 - 未加載前顯示的輕量圖片 */
const DEFAULT_IMG = 'https://pica.zhimg.com/v2-f052aa50ca65df4bad1c3b7e4084d00e_1440w.jpg'
/** 真實(shí)圖片地址生成函數(shù) - 接收索引,返回唯一隨機(jī)圖片 URL */
const IMG_URL_TEMPLATE = (index: number) => `https://picsum.photos/400/600?r=${index}`
/**
* DOM 引用數(shù)組 - 用于存儲(chǔ)所有需要懶加載的圖片 DOM
* Vue 會(huì)自動(dòng)將 v-for 中的 ref 收集到這個(gè)數(shù)組
*/
const imgRefs = ref<HTMLImageElement[]>([])
/** 觀察者實(shí)例 - 全局保存,組件銷毀時(shí)需要手動(dòng)清理 */
let observer: IntersectionObserver | null = null
/**
* 初始化懶加載核心邏輯
* 1. 創(chuàng)建 IntersectionObserver 實(shí)例
* 2. 等待 DOM 渲染完成后開始監(jiān)聽
*/
async function initLazyLoad() {
// 創(chuàng)建觀察者,配置交叉閾值為 1%
observer = new IntersectionObserver(
(entries, observer) => {
// entries: 當(dāng)前幀內(nèi)所有發(fā)生交叉變化的元素列表
for (const entry of entries) {
// 只處理「進(jìn)入視口」的元素,「離開視口」時(shí)跳過
if (!entry.isIntersecting) continue
// 獲取觸發(fā)回調(diào)的圖片 DOM 元素
const img = entry.target as HTMLImageElement
// 從 data-src 屬性讀取真實(shí)圖片地址
const realSrc = img.dataset.src
// 將真實(shí)地址賦值給 src,觸發(fā)圖片加載
if (realSrc) img.src = realSrc
// ?? 關(guān)鍵:加載完成后立即取消觀察
// 避免已加載圖片繼續(xù)占用觀察者資源
observer.unobserve(img)
}
},
{
// threshold: 觸發(fā)加載的可見比例
// 0.01 = 圖片露出 1% 時(shí)就觸發(fā),適合需要提前加載的場(chǎng)景
threshold: 0.01,
},
)
// 等待 Vue 更新 DOM 后再執(zhí)行監(jiān)聽
// 確保 v-for 循環(huán)的 img 元素已經(jīng)渲染到頁面
await nextTick()
// 將所有圖片 DOM 注冊(cè)到觀察者,開始監(jiān)聽
imgRefs.value.forEach((img) => observer?.observe(img))
}
/**
* 銷毀觀察者,釋放資源
* ?? 必須在組件銷毀時(shí)調(diào)用,防止內(nèi)存泄漏
*/
function destroyLazyLoad() {
if (!observer) return
// 先取消所有圖片的觀察
imgRefs.value.forEach((img) => observer!.unobserve(img))
// 完全銷毀觀察者實(shí)例
observer.disconnect()
// 重置為 null
observer = null
}
/** 組件掛載時(shí)啟動(dòng)懶加載 */
onMounted(() => {
initLazyLoad()
})
/** 組件銷毀前清理資源 */
onUnmounted(() => {
destroyLazyLoad()
})
</script>
<style lang="scss" scoped>
.app-content {
/* CSS 變量:統(tǒng)一樣式配置,方便維護(hù) */
--item-gap: 16px; /* 網(wǎng)格項(xiàng)之間的間距 */
--item-min-width: 150px; /* 網(wǎng)格項(xiàng)的最小寬度,響應(yīng)式適配 */
--item-height: 300px; /* 圖片卡片固定高度 */
}
/* 功能描述樣式 - 左側(cè)藍(lán)色邊框提示框 */
.lazy-desc {
margin-bottom: 16px;
padding: 8px 16px;
background: #f0f9ff; /* 淺藍(lán)色背景 */
border-left: 4px solid #409eff; /* 左側(cè)藍(lán)色強(qiáng)調(diào)條 */
border-radius: 4px;
color: #1f2937;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
}
/* 響應(yīng)式網(wǎng)格布局 - 自動(dòng)填充,最小寬度 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; /* 隱藏圖片放大時(shí)超出邊框的部分 */
}
.card-list .item:hover img {
transform: scale(1.5); /* 鼠標(biāo)懸停時(shí)圖片放大 1.5 倍 */
}
.card-list .item img {
display: block;
width: 100%;
height: 100%;
transition: all 0.32s; /* 過渡動(dòng)畫,使縮放更平滑 */
}
</style>四、核心總結(jié)
本文通過 Vue3 + IntersectionObserver 實(shí)現(xiàn)了高性能圖片懶加載方案,核心要點(diǎn):
| 要點(diǎn) | 說明 |
|---|---|
IntersectionObserver | 替代 scroll 事件,瀏覽器自動(dòng)優(yōu)化,性能更優(yōu) |
占位圖 + data-src | 初始顯示占位圖,真實(shí)地址存在 data-src 中 |
observer.unobserve() | 加載完成后取消監(jiān)聽,避免資源浪費(fèi) |
onUnmounted 清理 | 組件銷毀時(shí)調(diào)用 disconnect(),防止內(nèi)存泄漏 |
該方案在多圖列表場(chǎng)景下效果顯著,可直接應(yīng)用于商品列表、朋友圈圖片流、相冊(cè)等業(yè)務(wù)場(chǎng)景。
以上就是基于Vue3+IntersectionObserver實(shí)現(xiàn)高性能圖片懶加載的詳細(xì)內(nèi)容,更多關(guān)于Vue3 IntersectionObserver圖片懶加載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue自定義組件實(shí)現(xiàn)v-model雙向綁定數(shù)據(jù)的實(shí)例代碼
vue中父子組件通信,都是單項(xiàng)的,直接在子組件中修改prop傳的值vue也會(huì)給出一個(gè)警告,接下來就用一個(gè)小列子一步一步實(shí)現(xiàn)了vue自定義的組件實(shí)現(xiàn)v-model雙向綁定,需要的朋友可以參考下2021-10-10
vue如何使用element-ui 實(shí)現(xiàn)自定義分頁
這篇文章主要介紹了vue如何使用element-ui 實(shí)現(xiàn)自定義分頁,可以通過插槽實(shí)現(xiàn)自定義的分頁,本文通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-07-07
jdk1.8+vue elementui實(shí)現(xiàn)多級(jí)菜單功能
這篇文章主要介紹了jdk1.8+vue elementui實(shí)現(xiàn)多級(jí)菜單功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
Vite+TS+Vue開啟eslint和prettier規(guī)范及校驗(yàn)方式
這篇文章主要介紹了Vite+TS+Vue開啟eslint和prettier規(guī)范及校驗(yàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
vue+webpack實(shí)現(xiàn)異步組件加載的方法

