Vue 图片懒加载
·
Vue 图片懒加载
图片懒加载
懒加载 是一种延迟加载的技术,当图片进入视图可视区域范围时才进行加载显示。这种技术能显著减少初始加载时间、降低用户带宽消耗、提高用户体验。
核心优势
- 减少页面初始加载时间
- 减少用户流量消耗
- 提升页面流畅度
- 适用于移动端和低网速环境
实现方案
将 img 的 src 暂存起来,当检测到图片进入视图可视窗口后,赋值给 img 的 src,加载显示图片。
1、原生 loading="lazy" 属性(极简)
实现原理
- HTML5 原生提供的
loading属性,属性值:lazy(懒加载),eager(立即加载,默认值)。 - 无需编写
JavaScript代码,由浏览器原生控制懒加载图片逻辑。 - 浏览器会自动判断图片是否进入可视视口,随后加载图片。
代码示例
<!-- 直接添加 loading="lazy" 即可 -->
<img src="https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg" loading="lazy" alt="原生懒加载图片" width="300" height="200">
兼容性
- 桌面端:Chrome 77+、Firefox 75+、Edge 79+、Safari 15.4+。
- 移动端:Chrome for Android 77+、Firefox for Android 75+、Safari on iOS 15.4+。
- 不支持 IE 浏览器、旧版 Safari(15.4 以下)。
优势
- 优势:零代码实现、性能最优(浏览器原生底层优化)、无需依赖第三方库。
注意事项
- 必须指定图片
width和height,否则可能导致布局偏移(CLS 问题)。 - 仅适用于静态图片,无法自定义预加载距离等高级配置,且各浏览器的预加载距离不一,无法确定起预加载距离。
- 若图片在初始视口内,即使设置
loading="lazy",浏览器也会立即加载。
2、基于 Intersection Observer API 实现(更多个性化设置时推荐使用)
实现原理
Intersection Observer(交叉观察器)是浏览器原生提供的 API,用于监听「目标元素」与「视口 / 根容器」的交叉状态变化。- 无需手动监听 scroll 事件,浏览器内部优化了性能,避免高频触发问题。
- 当图片与视口交叉(进入 / 即将进入)时,自动触发回调函数,此时加载真实图片。
代码示例
组件实现
<!-- LazyImage.vue 定义组件实现 -->
<script setup>
/**
* 图片懒加载组件(基于 IntersectionObserver 实现,不兼容直接加载)
* @description 使用时图片key不要使用index,建议使用唯一值,避免销毁 IntersectionObserver 无法正确找到当前元素
* @property {String} src - 目标图片地址
* @property {String} mode - 裁剪模式,见官网说明 (默认 'aspectFill' )
* @property {String} alt - 图片描述
* @property {String|Number} width - 图片宽度
* @property {String|Number} height - 图片高度
* @property {Boolean} lazy - 是否启用懒加载(默认 true )
* @property {Number} threshold - 懒加载阈值(提前加载距离,单位px)
* @slot placeholder - 自定义占位符内容
* @slot error - 自定义加载失败内容
* @event load - 图片加载成功时触发
* @event error - 图片加载失败时触发
* @example <LazyImage width="300" height="200" src="image.jpg" />
*/
import {
ref,
watch,
onMounted,
onUnmounted,
onBeforeUnmount,
computed,
} from "vue";
// 定义 props
const props = defineProps({
// 目标图片地址
src: {
type: String,
required: true,
},
mode: {
type: String,
default: "aspectFill",
},
// 图片描述
alt: {
type: String,
default: "懒加载图片",
},
lazy: {
type: Boolean,
default: true,
},
// 图片宽度
width: {
type: [String, Number],
default: "100%",
},
// 图片高度
height: {
type: [String, Number],
default: "auto",
},
// 懒加载阈值(提前加载距离,单位px)
threshold: {
type: Number,
default: 100,
},
// 是否启用淡入动画
fade: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(["load", "error"]);
const imageRef = ref(null); // 图片元素引用
const isLoaded = ref(false); // 图片是否已加载
const isError = ref(false); // 图片是否加载失败
let observer = null; // IntersectionObserver 实例
// 容器样式
const containerStyle = computed(() => ({
width: typeof props.width === "number" ? `${props.width}px` : props.width,
height: typeof props.height === "number" ? `${props.height}px` : props.height,
overflow: "hidden",
position: "relative",
backgroundColor: "#f0f0f0",
}));
// 图片样式
const imageStyle = computed(() => ({
width: "100%",
height: "100%",
objectFit: "cover",
opacity: isLoaded.value ? 1 : 0,
transition: props.fade ? "opacity .3s ease-in-out" : "none",
position: "absolute",
top: 0,
left: 0,
}));
// 卸载 observer
const fnUnobserve = () => {
if (observer && imageRef.value) {
observer.unobserve(imageRef.value);
}
};
// 加载图片
const loadImage = () => {
if (!props.src) return;
const img = new Image();
img.src = props.src;
img.onload = () => {
isLoaded.value = true;
isError.value = false;
fnUnobserve();
imageRef.value.src = props.src;
// 触发 load 事件
emit("load");
};
img.onerror = () => {
isError.value = true;
isLoaded.value = false;
fnUnobserve();
// 触发 error 事件
emit("error");
};
};
// 初始化 IntersectionObserver
const initObserver = () => {
// 兼容处理(不支持IntersectionObserver的浏览器直接加载)
if (!window.IntersectionObserver || !props.lazy) {
loadImage();
return;
}
observer = new IntersectionObserver(
(entries) => {
if (entries && entries[0]?.isIntersecting) {
loadImage();
}
},
{
rootMargin: `0px 0px ${props.threshold}px 0px`,
threshold: 0,
}
);
// 观察img
if (imageRef.value) {
observer.observe(imageRef.value);
}
};
// 监听路径更换
watch(
() => props.src,
() => {
// 重置状态
isLoaded.value = false;
isError.value = false;
// 重新观察
if (imageRef.value) {
observer.observe(imageRef.value);
}
}
);
onMounted(() => {
initObserver();
});
// 组件卸载停止观察
onBeforeUnmount(() => {
if (observer && imageRef.value) {
observer.unobserve(imageRef.value);
observer.disconnect();
observer = null;
}
});
</script>
<template>
<div class="lazy-image-container" :style="containerStyle">
<img
ref="imageRef"
:alt="alt"
:mode="mode"
class="lazy-image-target"
:style="imageStyle"
/>
<slot name="placeholder" v-if="!isLoaded && !isError">
<div class="lazy-image-placeholder">加载中</div>
</slot>
<slot name="error" v-if="isError">
<div class="lazy-image-error">加载失败</div>
</slot>
</div>
</template>
<style scoped>
.lazy-image-container {
display: inline-block;
}
.lazy-image-placeholder,
.lazy-image-error {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: rgb(192, 196, 204);
vertical-align: middle;
}
.lazy-image-target {
position: absolute;
top: 0;
left: 0;
}
</style>
组件使用
<!-- 组件使用 -->
<script setup lang="ts">
import { reactive } from "vue";
import LazyImage from "./components/LazyImage.vue";
// 模拟图片列表
const imageList = reactive([
{
url: "https://images.dog.ceo/breeds/ovcharka-caucasian/IMG_20190628_144817.jpg",
alt: "图片1",
},
{
url: "https://images.dog.ceo/breeds/beagle/n02088364_17294.jpg",
alt: "图片2",
},
{
url: "https://images.dog.ceo/breeds/whippet/n02091134_10690.jpg",
alt: "图片3",
}
]);
</script>
<template>
<div class="image-list">
<!-- 基础使用 -->
<LazyImage
v-for="(item, index) in imageList"
:key="item.alt"
:src="item.url"
:alt="item.alt"
width="300px"
height="200px"
:threshold="200"
/>
</div>
</template>
<style scoped>
.image-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
}
</style>
指令实现
// lazyDirective.js 指令代码实现
// 定义懒加载指令配置
const lazyOptions = {
loading: "", // 加载中的占位图
error: "", // 加载失败的占位图
threshold: 0.2, // 触发懒加载的可见比例(可覆盖配置)
rootMargin: "0px", // 预加载距离(可覆盖配置)
};
let observer = null;
// 加载图片函数
const loadImage = (el, realSrc) => {
const img = new Image();
img.src = realSrc;
img.onload = () => {
el.src = realSrc;
observer && observer.unobserve(el);
};
img.onerror = () => {
el.src = lazyOptions.error || "";
observer && observer.unobserve(el);
};
};
// 自定义 v-lazy 指令
const lazyDirective = {
mounted(el, binding) {
// 真实图片地址
const realSrc = binding.value;
if (!realSrc) return;
// 设置占位图
el.src = lazyOptions.loading || "";
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadImage(el, realSrc);
}
},
{
rootMargin: lazyOptions.rootMargin,
threshold: lazyOptions.threshold,
}
);
observer.observe(el);
},
unmounted(el) {
if (observer) {
observer.unobserve(el);
observer.disconnect();
observer = null;
}
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
// 真实图片地址
const realSrc = binding.value;
if (!realSrc) return;
// 设置占位图
el.src = realSrc;
observer.observe(el);
}
},
};
// 导出指令
export default lazyDirective;
main 文件指令定义
// main 文件指令定义
import { createApp } from "vue";
import App from "./App.vue";
import lazyDirective from "./utils/lazyDirective";
const app = createApp(App);
app.directive("lazy", lazyDirective);
app.mount("#app");
使用指令
<img v-lazy="imageSrc" alt="图片懒加载指令使用" />
兼容性
- 现代浏览器全支持:Chrome 51+、Firefox 55+、Safari 12.1+、Edge 16+。
- 不支持 IE 浏览器。
- 兼容方案:可引入
Intersection Observer Polyfill,兼容到 IE9+。
优势
- 性能优异:浏览器原生优化,无需手动节流 / 防抖。
- 代码简洁:无需处理滚动事件和位置计算。
- 灵活配置:可自定义根容器、预加载距离等。
3、基于 scroll 事件 + 位置计算(不推荐使用,低版本兼容时使用)
实现原理
- 初始时,图片
src指向占位图(或不设置src,用data-*存储真实地址,如data-src)。 - 监听
window或滚动容器的scroll事件。 - 每次滚动时,通过
getBoundingClientRect()获取图片的位置信息,判断是否进入视口。 - 若进入视口,将
data-src赋值给src,完成图片加载。
代码示例
兼容性
- 支持所有现代浏览器及 IE8+(IE8 需兼容
querySelectorAll,可替换为getElementsByClassName)。 getBoundingClientRect()兼容性:IE4+ 全支持。- 缺点:
scroll事件触发频率极高,会造成「性能抖动」,需配合节流优化。
各方案兼容性对比
| 实现方式 | 浏览器支持版本 | 性能表现 | 场景 |
|---|---|---|---|
原生 loading="lazy" |
Chrome77+、Firefox75+、Safari15.4+ | 最优 | 当前项目、无需复杂配置 |
Intersection Observer |
Chrome51+、Firefox55+、Safari12.1+、可 polyfill 到 IE9+ |
优秀 | 当前项目、追求自定义配置 |
scroll + getBoundingClientRect |
IE8+、所有现代浏览器 | 一般(需节流) | 兼容旧项目、低版本浏览器场景 |
总结
- 原生
loading="lazy":极简、性能最优,优先用于现代浏览器项目。 Intersection Observer:灵活、高性能,推荐用于需要自定义配置的现代项目(需兼容旧浏览器时加 polyfill)。scroll + getBoundingClientRect:兼容性最强(支持 IE8+),仅用于老旧项目维护。- 兼容性优先级:原生懒加载 >
Intersection Observer(带 polyfill)> 传统scroll方案。 - 核心原则:保证图片正常加载的同时,优化性能和用户体验,避免布局偏移。
更多推荐


所有评论(0)