一、背景
在现代 Web 开发领域,图片懒加载的重要性不容小觑。
首先,它对页面加载速度有着极大提升作用。当页面包含大量图片时,若不采用懒加载,所有图片会在页面初始化时一同请求加载,这会导致加载时间过长。而懒加载仅在图片进入浏览器可视区域时才触发加载,大大减少了初始加载的数据量,使关键资源(如 HTML、CSS 等)能优先高效加载,让用户更快看到页面布局并进行交互。
其次,能有效节省用户流量。对于移动用户或流量有限的情况,懒加载按需加载图片,避免了加载那些用户可能根本不会看到的图片,防止不必要的流量消耗。
二、图片懒加载原理概述
简单解释什么是图片懒加载。即图片在进入浏览器的可视区域时才进行加载,而不是在页面初始化时就全部加载所有图片。
可以用一些简单的示意图或者类比的方式(比如把页面想象成一个画展,只有当观众走到画作前才会仔细去看这幅画,也就是才加载这幅画的细节,类似图片进入可视区才加载)来帮助读者更好地理解懒加载的概念。
三、图片懒加载模块展示
一、图片懒加载核心逻辑(useLazyLoad.js
)
import { reactive, onBeforeUnmount } from 'vue'
export default function useLazyLoad() {
const state = reactive({
observers: new Map(),
imageStates: new Map(),
})
const observe = (imgElement) => {
if (!imgElement || state.observers.has(imgElement)) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loadImage(imgElement)
unobserve(imgElement)
}
},
{
rootMargin: '200px 0px',
threshold: 0.01,
},
)
observer.observe(imgElement)
state.observers.set(imgElement, observer)
state.imageStates.set(imgElement, 'pending')
}
const loadImage = (imgElement) => {
state.imageStates.set(imgElement, 'loading')
const tempImg = new Image()
tempImg.src = imgElement.dataset.src
tempImg.onload = () => {
// 延迟 3 秒后再显示图片
setTimeout(() => {
imgElement.src = imgElement.dataset.src
state.imageStates.set(imgElement, 'loaded')
}, 500)
}
tempImg.onerror = () => {
// 错误处理也可以添加延迟,这里直接显示错误图片
imgElement.src = '/error.jpg'
state.imageStates.set(imgElement, 'error')
}
}
const unobserve = (imgElement) => {
if (!imgElement) return
state.observers.get(imgElement)?.unobserve(imgElement)
state.observers.delete(imgElement)
}
const getImageState = (imgElement) => {
return imgElement ? state.imageStates.get(imgElement) || 'pending' : 'pending'
}
onBeforeUnmount(() => {
state.observers.forEach((observer) => observer.disconnect())
state.observers.clear()
state.imageStates.clear()
})
return { observe, unobserve, getImageState }
}
1. 状态管理部分
const state = reactive({
observers: new Map(),
imageStates: new Map(),
})
reactive
函数:在 Vue 3 中,reactive
用于创建响应式对象。这里我们创建了一个state
对象,它包含两个Map
实例。Map
是一种键值对的数据结构,相比于普通的对象,Map
可以使用任意类型作为键,并且能更方便地进行增删改查操作。observers
:这个Map
用于存储每个图片元素对应的IntersectionObserver
实例。通过图片元素作为键,我们可以方便地管理和操作每个图片的观察器。imageStates
:该Map
用于存储每个图片元素的加载状态,状态值可以是pending
(待加载)、loading
(正在加载)、loaded
(已加载)、error
(加载错误)。这样做的好处是可以方便地跟踪每个图片的加载进度,从而在 UI 上做出相应的展示。
2、observe
函数
const observe = (imgElement) => {
if (!imgElement || state.observers.has(imgElement)) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loadImage(imgElement)
unobserve(imgElement)
}
},
{
rootMargin: '200px 0px',
threshold: 0.01,
},
)
observer.observe(imgElement)
state.observers.set(imgElement, observer)
state.imageStates.set(imgElement, 'pending')
}
- 参数检查:首先检查传入的
imgElement
是否存在,以及该元素是否已经有对应的观察器。如果不满足条件,则直接返回,避免重复操作。 IntersectionObserver
:这是浏览器提供的一个 API,用于监听目标元素与视口(或指定的根元素)的交叉状态。- 回调函数:当目标元素与视口的交叉状态发生变化时,会触发回调函数。这里我们只关注元素是否进入视口(
entry.isIntersecting
为true
),如果进入视口,则调用loadImage
函数加载图片,并调用unobserve
函数停止对该元素的观察,以节省性能。 - 配置选项:
rootMargin
:设置为'200px 0px'
表示在目标元素距离视口顶部或底部还有 200px 时就开始加载图片,这样可以提前加载图片,提升用户体验。threshold
:设置为0.01
表示当目标元素与视口的交叉比例达到 0.01 时就触发回调函数,确保能及时发现元素进入视口。
- 回调函数:当目标元素与视口的交叉状态发生变化时,会触发回调函数。这里我们只关注元素是否进入视口(
- 状态设置:将观察器存储到
observers
中,并将图片的初始状态设置为pending
。
3、loadImage
函数
const loadImage = (imgElement) => {
state.imageStates.set(imgElement, 'loading')
const tempImg = new Image()
tempImg.src = imgElement.dataset.src
tempImg.onload = () => {
// 延迟 3 秒后再显示图片
setTimeout(() => {
imgElement.src = imgElement.dataset.src
state.imageStates.set(imgElement, 'loaded')
}, 500)
}
tempImg.onerror = () => {
// 错误处理也可以添加延迟,这里直接显示错误图片
imgElement.src = '/error.jpg'
state.imageStates.set(imgElement, 'error')
}
}
- 状态更新:将图片的状态设置为
loading
,表示开始加载图片。 - 临时图片对象:创建一个新的
Image
对象tempImg
,并将其src
属性设置为图片元素的data-src
属性值。这样做的目的是在图片真正显示之前先进行预加载,避免在页面上直接显示时出现闪烁。 - 加载成功处理:当
tempImg
加载成功时,会触发onload
事件。这里使用setTimeout
函数延迟 500 毫秒后再将图片元素的src
属性设置为data-src
的值,并将图片状态设置为loaded
。延迟显示可以模拟一些加载效果,提升用户体验。 - 加载失败处理:当
tempImg
加载失败时,会触发onerror
事件。此时将图片元素的src
属性设置为错误图片的路径/error.jpg
,并将图片状态设置为error
。
4、unobserve
函数
const unobserve = (imgElement) => {
if (!imgElement) return
state.observers.get(imgElement)?.unobserve(imgElement)
state.observers.delete(imgElement)
}
- 该函数用于停止对指定图片元素的观察。首先检查元素是否存在,如果存在,则获取对应的观察器并调用其
unobserve
方法停止观察,然后从observers
中删除该元素的记录。
5、getImageState
函数
const getImageState = (imgElement) => {
return imgElement ? state.imageStates.get(imgElement) || 'pending' : 'pending'
}
- 该函数用于获取指定图片元素的加载状态。如果元素存在,则从
imageStates
中获取其状态,如果不存在则返回pending
。
6、资源清理
onBeforeUnmount(() => {
state.observers.forEach((observer) => observer.disconnect())
state.observers.clear()
state.imageStates.clear()
})
onBeforeUnmount
是 Vue 3 的生命周期钩子,在组件卸载之前执行。这里我们遍历observers
中的所有观察器,调用其disconnect
方法停止观察,然后清空observers
和imageStates
,释放资源,避免内存泄漏。
二、懒加载图片组件(LazyImage.vue
)
<template>
<div class="lazy-container">
<img
ref="imgEl"
:data-src="dataSrc"
:alt="alt"
:class="status"
/>
<transition name="fade">
<div
v-show="showPlaceholder"
class="loading-overlay"
>
<div class="spinner"></div>
<span class="loading-text">加载中...</span>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import useLazyLoad from '@/hooks/useLazyLoad';
const props = defineProps({
dataSrc: String,
alt: String
});
const imgEl = ref(null);
const { observe, unobserve, getImageState } = useLazyLoad();
console.log('Actual image dataSrc:', props.dataSrc);
const status = computed(() => {
const state = getImageState(imgEl.value);
console.log('Current image status:', state);
return state;
});
const showPlaceholder = computed(() => status.value === 'loading');
onMounted(() => {
console.log('Component mounted, observing image');
if (imgEl.value) {
observe(imgEl.value);
}
});
onBeforeUnmount(() => {
console.log('Component unmounting, unobserving image');
if (imgEl.value) {
unobserve(imgEl.value);
}
});
</script>
<style scoped>
.lazy-container {
position: relative;
width: 100%;
height: 100%;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.6s ease;
}
.placeholder-img {
width: 100%; /* 或者根据需要调整大小 */
height: auto;
object-fit: cover; /* 保持图片比例 */
}
img.loaded {
opacity: 1;
}
img.placeholder-img {
opacity: 1; /* 占位图始终显示 */
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 0.8s linear infinite;
}
.loading-text {
margin-top: 12px;
color: #666;
font-size: 0.9em;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
1. 模板部分
<template>
<div class="lazy-container">
<img
ref="imgEl"
:data-src="dataSrc"
:alt="alt"
:class="status"
/>
<transition name="fade">
<div
v-show="showPlaceholder"
class="loading-overlay"
>
<div class="spinner"></div>
<span class="loading-text">加载中...</span>
</div>
</transition>
</div>
</template>
- 图片元素:使用
img
标签显示图片,通过ref
绑定到imgEl
,方便在脚本中访问。data-src
属性用于存储图片的真实地址,alt
属性用于提供图片的替代文本,class
属性根据图片的加载状态动态添加类名。 - 过渡组件:使用
transition
组件包裹加载提示层,name
属性设置为fade
,实现淡入淡出的过渡效果。v-show
指令根据showPlaceholder
的值决定是否显示加载提示层。
2. 脚本部分
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import useLazyLoad from '@/hooks/useLazyLoad';
const props = defineProps({
dataSrc: String,
alt: String
});
const imgEl = ref(null);
const { observe, unobserve, getImageState } = useLazyLoad();
console.log('Actual image dataSrc:', props.dataSrc);
const status = computed(() => {
const state = getImageState(imgEl.value);
console.log('Current image status:', state);
return state;
});
const showPlaceholder = computed(() => status.value === 'loading');
onMounted(() => {
console.log('Component mounted, observing image');
if (imgEl.value) {
observe(imgEl.value);
}
});
onBeforeUnmount(() => {
console.log('Component unmounting, unobserving image');
if (imgEl.value) {
unobserve(imgEl.value);
}
});
- 导入和定义:导入必要的 Vue 3 API 和
useLazyLoad
函数,定义组件的props
,包括dataSrc
和alt
。 - 响应式引用:使用
ref
创建imgEl
引用,用于获取图片元素。 - 状态计算:
status
:使用computed
计算属性获取图片的加载状态,并在控制台输出状态信息。showPlaceholder
:根据status
的值判断是否显示加载提示层。
- 生命周期钩子:
onMounted
:在组件挂载后,调用observe
函数开始观察图片元素。onBeforeUnmount
:在组件卸载前,调用unobserve
函数停止观察图片元素。
3. 样式部分
.lazy-container {
position: relative;
width: 100%;
height: 100%;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.6s ease;
}
.placeholder-img {
width: 100%; /* 或者根据需要调整大小 */
height: auto;
object-fit: cover; /* 保持图片比例 */
}
img.loaded {
opacity: 1;
}
img.placeholder-img {
opacity: 1; /* 占位图始终显示 */
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 0.8s linear infinite;
}
.loading-text {
margin-top: 12px;
color: #666;
font-size: 0.9em;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
- 容器样式:
lazy-container
设置为相对定位,方便子元素进行绝对定位。 - 图片样式:初始状态下图片的
opacity
为 0,不可见。当图片加载完成后,添加loaded
类,将opacity
设置为 1,实现淡入效果。 - 加载提示层样式:
loading-overlay
覆盖在图片上方,使用flex
布局居中显示加载图标和文字。 - 加载图标样式:
spinner
通过边框和动画实现
三、图片列表组件(ImageList.vue
)
<template>
<div class="image-grid">
<div
v-for="(img, index) in images"
:key="img.id"
class="image-item"
>
<LazyImage
:data-src="img.url"
:alt="`图片 ${index + 1}`"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import LazyImage from './LazyImage.vue'
// 模拟图片数据
const images = ref([
{id:1,url:'https://img.51miz.com/Element/00/73/12/40/f12ed593_E731240_093fc95a.jpg'},
{id:2,url:'https://ts1.cn.mm.bing.net/th/id/R-C.ac133896b2735871271f35272484cfbf?rik=k0a86ZhyanNt9g&riu=http%3a%2f%2fn.sinaimg.cn%2fsinacn10114%2f600%2fw1920h1080%2f20190518%2f1372-hwzkfpv2143245.jpg&ehk=DU3HSmHLIjdJx1fj6fhR1FLcDsewKDsoKdyrELfab%2bg%3d&risl=&pid=ImgRaw&r=0'},
// ...更多图片数据
])
</script>
<style>
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
@media (max-width: 768px) {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
}
.image-item {
aspect-ratio: 16/9;
background: #f8f8f8;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.image-item:hover {
transform: translateY(-4px);
}
</style>
1. 模板部分
<template>
<div class="image-grid">
<div
v-for="(img, index) in images"
:key="img.id"
class="image-item"
>
<LazyImage
:data-src="img.url"
:alt="`图片 ${index + 1}`"
/>
</div>
</div>
</template>
- 容器结构:使用
image-grid
作为容器,内部通过v-for
循环渲染每个image-item
。 key
值:使用img.id
作为key
,确保列表渲染的高效性和唯一性。- 懒加载组件:每个图片项使用自定义的
LazyImage
组件,并传递data-src
(真实图片地址)和alt
(替代文本)属性。
2. 脚本部分
import { ref } from 'vue'
import LazyImage from './LazyImage.vue'
const images = ref([
{id:1,url:'https://img.51miz.com/Element/00/73/12/40/f12ed593_E731240_093fc95a.jpg'},
// ...更多图片数据
])
- 依赖导入:引入
ref
用于创建响应式数据,导入LazyImage
组件。 - 图片数据:使用
ref
定义images
数组,包含多个图片对象(id
和url
)。这里模拟了真实图片数据,实际项目中可以通过 API 动态获取。
3. 样式部分
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
@media (max-width: 768px) {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
}
.image-item {
aspect-ratio: 16/9;
background: #f8f8f8;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.image-item:hover {
transform: translateY(-4px);
}
- 网格布局:
grid-template-columns
:使用repeat(auto-fill, minmax(300px, 1fr))
创建自适应列数的网格布局,每列最小宽度为 300px,最大宽度为可用空间的 100%。- 响应式设计:通过媒体查询,当屏幕宽度小于 768px 时,调整列宽为 240px,间距为 1rem,提升移动端体验。
- 图片项样式:
aspect-ratio
:设置为16/9
,保持图片容器的宽高比。- 背景色与阴影:使用浅灰色背景(
#f8f8f8
)和轻微阴影(box-shadow
),提升视觉层次感。 - 悬停动画:当鼠标悬停时,图片项向上移动 4px(
transform: translateY(-4px)
),增强交互反馈。
四、设计亮点
-
响应式布局:
- 使用 CSS Grid 实现灵活的图片排列,自动适应不同屏幕尺寸。
- 媒体查询确保移动端和桌面端的显示效果一致。
-
懒加载集成:
- 通过
LazyImage
组件实现图片的懒加载,减少初始请求数量,提升页面加载速度。
- 通过
-
视觉优化:
- 统一的图片容器样式(圆角、阴影)提升整体美观度。
- 悬停动画增强用户交互体验。
-
性能考虑:
- 图片数据通过
ref
响应式管理,确保数据变化时 UI 及时更新。 - 使用
img.id
作为key
值,优化列表渲染性能。
- 图片数据通过
五、实际应用场景
- 图片库或作品集展示:适合展示大量图片,支持响应式布局和懒加载。
- 电商商品列表:通过统一的图片样式和交互效果,提升商品展示的专业性。
- 内容社区:支持用户上传图片,懒加载和响应式设计确保页面流畅性。
六、未来需要优化的地方
- 占位图升级:当前占位图较简单,可采用模糊预览图或低分辨率图片作为占位,在图片加载过程中提供更好预览,优化用户等待体验。
- 性能监控完善:添加加载时间统计、请求成功率等监控指标,分析性能瓶颈,针对性优化加载策略,如调整
rootMargin
和threshold
参数。 - 服务端协作:结合图片 CDN 的响应式图片服务,根据设备屏幕和网络情况返回合适尺寸与格式图片;采用 WebP 格式,进一步减小图片体积,提升加载速度。
- 错误处理增强:目前仅显示固定错误图片,可增加重试机制,多次尝试加载失败后再显示错误信息。