图片懒加载实现方案:原理、代码与优化(原生 / 框架通用)
图片懒加载(Lazy Loading)是 优化页面性能的核心手段—— 核心原理是:图片进入(或即将进入)浏览器可视区域时,才加载图片资源(替换 src 或 srcset),避免页面初始化时加载所有图片导致的资源浪费、加载缓慢问题。
以下是 3 种主流实现方案(原生 / Intersection Observer / 框架适配),覆盖从简单场景到生产环境的全需求,附完整代码和优化细节。
一、核心概念铺垫
1. 为什么需要懒加载?
-
长页面(如电商列表、新闻流)通常包含大量图片,初始化加载所有图片会导致:
- 网络请求过多,带宽占用大(尤其移动端);
- 页面加载时间延长,首屏渲染慢;
- 内存占用过高,可能导致页面卡顿。
-
懒加载只加载 “用户能看到” 的图片,显著提升首屏加载速度和用户体验。
2. 实现核心思路
- 页面初始化时,图片元素不设置真实
src(避免默认加载),而是将真实地址存放在自定义属性中(如data-src/data-srcset); - 监听图片是否进入可视区域(通过滚动事件、Intersection Observer API 等);
- 图片进入可视区域时,将
data-src赋值给src(或data-srcset赋值给srcset),触发图片加载; - 加载完成后,移除监听(避免重复触发)。
二、方案 1:原生滚动监听(兼容旧浏览器,简单易懂)
最基础的实现方式:监听 scroll 事件,通过 getBoundingClientRect() 判断图片是否进入可视区域。适合简单场景或需要兼容旧浏览器(如 IE)的情况。
1. 实现步骤
(1)HTML 结构(关键:用 data-src 存真实地址)
<!-- 懒加载图片:src 用占位图(可选),data-src 存真实地址 -->
<img
class="lazy"
data-src="https://example.com/real-img-1.jpg"
src="https://example.com/placeholder.jpg" <!-- 占位图(可选,优化用户体验) -->
alt="商品图片"
width="300"
height="200" <!-- 建议设置宽高,避免布局偏移(CLS) -->
>
<img
class="lazy"
data-src="https://example.com/real-img-2.jpg"
src="https://example.com/placeholder.jpg"
alt="商品图片"
width="300"
height="200"
>
(2)CSS 优化(可选:占位图样式、淡入效果)
/* 占位图样式(灰色背景+居中文字) */
.lazy {
background: #f5f5f5;
display: inline-block;
overflow: hidden;
}
/* 图片加载完成后淡入(优化体验) */
.lazy.loaded {
opacity: 1;
transition: opacity 0.3s ease;
}
.lazy:not(.loaded) {
opacity: 0;
}
(3)JavaScript 实现(原生无依赖)
// 1. 获取所有懒加载图片
const lazyImages = document.querySelectorAll('img.lazy');
// 2. 核心判断:图片是否进入可视区域
function isInViewport(el) {
const rect = el.getBoundingClientRect();
// 可视区域高度(窗口高度 + 额外偏移量100px,提前加载,优化体验)
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
// 图片顶部 <= 可视区域底部 + 偏移量,且图片底部 >= 可视区域顶部
return (
rect.top <= viewHeight + 100 &&
rect.bottom >= 0
);
}
// 3. 加载图片(替换 data-src 到 src)
function loadImage(el) {
if (el.dataset.src && !el.classList.contains('loaded')) {
// 替换真实地址
el.src = el.dataset.src;
// 处理 srcset(适配响应式图片,可选)
if (el.dataset.srcset) {
el.srcset = el.dataset.srcset;
}
// 加载完成后添加类名(触发淡入效果)
el.onload = () => {
el.classList.add('loaded');
};
// 移除自定义属性(避免重复处理)
delete el.dataset.src;
delete el.dataset.srcset;
}
}
// 4. 批量处理所有图片
function handleLazyLoad() {
lazyImages.forEach(el => {
if (isInViewport(el)) {
loadImage(el);
}
});
}
// 5. 监听触发事件(滚动、窗口 resize、页面加载完成)
window.addEventListener('scroll', handleLazyLoad);
window.addEventListener('resize', handleLazyLoad); // 窗口大小变化时重新判断
window.addEventListener('load', handleLazyLoad); // 页面初始化时先处理一次(首屏可见图片)
// 6. 初始执行一次(处理首屏图片)
handleLazyLoad();
2. 优点与缺点
- 优点:原生无依赖、兼容所有浏览器(包括 IE)、实现简单易懂;
- 缺点:
scroll事件触发频率高(每滚动 1px 就触发),可能导致性能问题(尤其长列表);需手动处理窗口 resize 等场景。
3. 性能优化(可选)
通过 throttle(节流)减少 scroll 事件触发次数(推荐使用 Lodash 的 throttle,或原生实现):
// 原生节流函数(避免依赖 Lodash)
function throttle(fn, delay = 100) {
let lastTime = 0;
return () => {
const now = Date.now();
if (now - lastTime >= delay) {
fn();
lastTime = now;
}
};
}
// 用节流包装处理函数(100ms 内仅触发一次)
const throttledLazyLoad = throttle(handleLazyLoad);
window.addEventListener('scroll', throttledLazyLoad);
window.addEventListener('resize', throttledLazyLoad);
三、方案 2:Intersection Observer API(推荐生产环境,高性能)
Intersection Observer 是浏览器原生 API(现代浏览器支持),专门用于监听 “元素是否进入可视区域”,无需手动监听 scroll 事件,性能更优(浏览器底层优化,触发频率低)。
1. 实现步骤
(1)HTML/CSS 结构(与方案 1 完全一致)
<img
class="lazy"
data-src="https://example.com/real-img-1.jpg"
src="https://example.com/placeholder.jpg"
alt="商品图片"
width="300"
height="200"
>
(2)JavaScript 实现(高性能核心)
// 1. 兼容性判断(可选:降级处理旧浏览器)
if ('IntersectionObserver' in window) {
// 2. 创建观察者实例(配置回调函数和观察选项)
const lazyObserver = new IntersectionObserver((entries, observer) => {
// entries:所有被观察元素的状态变化数组
entries.forEach(entry => {
// entry.isIntersecting:元素是否进入可视区域
if (entry.isIntersecting) {
const img = entry.target; // 当前进入可视区域的图片
// 加载图片(替换 data-src 到 src)
if (img.dataset.src) {
img.src = img.dataset.src;
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
// 加载完成后添加淡入效果
img.onload = () => {
img.classList.add('loaded');
};
// 停止观察(避免重复处理)
observer.unobserve(img);
// 移除自定义属性
delete img.dataset.src;
delete img.dataset.srcset;
}
}
});
}, {
rootMargin: '100px 0px', // 提前100px加载(优化体验,避免用户看到占位图)
threshold: 0.1 // 元素进入可视区域10%时触发回调
});
// 3. 观察所有懒加载图片
document.querySelectorAll('img.lazy').forEach(img => {
lazyObserver.observe(img);
});
} else {
// 降级处理:旧浏览器使用方案1的滚动监听
handleLazyLoad(); // 方案1中的 handleLazyLoad 函数
window.addEventListener('scroll', throttle(handleLazyLoad));
window.addEventListener('resize', throttle(handleLazyLoad));
}
2. 核心配置说明(IntersectionObserver 选项)
| 选项 | 含义 |
|---|---|
root | 观察的根元素(默认是视口 viewport),可指定某个 DOM 元素(如滚动容器) |
rootMargin | 根元素的外边距(提前 / 延迟触发),如 100px 0px 表示提前 100px 加载 |
threshold | 触发回调的阈值(元素进入可视区域的比例),如 0.1 表示 10% 进入时触发 |
3. 优点与缺点
- 优点:高性能(浏览器底层优化,无频繁事件触发)、代码简洁、支持自定义观察规则(如提前加载);
- 缺点:不兼容 IE 浏览器(现代浏览器均支持,如 Chrome 51+、Firefox 55+、Edge 16+)。
4. 兼容性处理(可选)
如果需要兼容 IE,可使用 polyfill(推荐 intersection-observer 库):
# 安装 polyfill
npm install intersection-observer --save
在项目入口文件(如 main.js)引入:
import 'intersection-observer'; // 引入后,IE 会支持 IntersectionObserver
四、方案 3:框架适配(Vue/React 项目专用)
在 Vue/React 等框架中,可基于上述原理封装成组件或指令,更贴合框架开发习惯。以下以 Vue 3 指令 和 React 组件 为例:
1. Vue 3 自定义指令(推荐)
封装成全局指令 v-lazy,在模板中直接使用:
// main.js(全局注册指令)
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
// 注册 v-lazy 指令
app.directive('lazy', {
// 元素挂载到 DOM 时执行
mounted(el, binding) {
// binding.value 是指令的值(即真实图片地址)
el.dataset.src = binding.value;
el.src = 'https://example.com/placeholder.jpg'; // 占位图
// 利用 IntersectionObserver 监听
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = el.dataset.src;
el.onload = () => {
el.classList.add('loaded');
};
observer.unobserve(el);
}
});
}, { rootMargin: '100px 0px' });
observer.observe(el);
} else {
// 降级处理(滚动监听)
const handleScroll = () => {
const rect = el.getBoundingClientRect();
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
if (rect.top <= viewHeight + 100) {
el.src = el.dataset.src;
el.onload = () => el.classList.add('loaded');
window.removeEventListener('scroll', handleScroll);
}
};
window.addEventListener('scroll', handleScroll);
}
}
});
app.mount('#app');
模板中使用:
<template>
<!-- 直接用 v-lazy 绑定真实图片地址 -->
<img
v-lazy="imageUrl"
class="lazy"
alt="商品图片"
width="300"
height="200"
>
</template>
<script setup>
const imageUrl = 'https://example.com/real-img.jpg';
</script>
<style>
/* 同方案1的 CSS 样式 */
.lazy { background: #f5f5f5; opacity: 0; transition: opacity 0.3s; }
.lazy.loaded { opacity: 1; }
</style>
2. React 组件封装
封装 LazyImage 组件,直接传入真实地址和占位图:
// LazyImage.jsx
import { useEffect, useRef } from 'react';
const LazyImage = ({ src, placeholder = 'https://example.com/placeholder.jpg', alt, width, height }) => {
const imgRef = useRef(null);
useEffect(() => {
const img = imgRef.current;
if (!img) return;
// 初始化占位图
img.src = placeholder;
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
img.src = src;
img.onload = () => {
img.classList.add('loaded');
};
observer.unobserve(img);
}
});
}, { rootMargin: '100px 0px' });
observer.observe(img);
return () => observer.disconnect(); // 组件卸载时停止观察
} else {
// 降级处理
const handleScroll = () => {
const rect = img.getBoundingClientRect();
const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
if (rect.top <= viewHeight + 100) {
img.src = src;
img.onload = () => img.classList.add('loaded');
window.removeEventListener('scroll', handleScroll);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}
}, [src, placeholder]);
return (
<img
ref={imgRef}
alt={alt}
style={{ width, height, background: '#f5f5f5', opacity: 0, transition: 'opacity 0.3s' }}
className="lazy"
/>
);
};
export default LazyImage;
使用组件:
// 父组件
import LazyImage from './LazyImage';
const App = () => {
return (
<div>
<LazyImage
src="https://example.com/real-img.jpg"
alt="商品图片"
width="300"
height="200"
/>
</div>
);
};
export default App;
五、生产环境优化细节(必看)
1. 避免布局偏移(CLS 优化)
- 图片标签必须设置
width和height属性(或通过 CSS 固定宽高比),让浏览器提前预留空间,避免图片加载后布局偏移(影响 Core Web Vitals 评分); - 示例:
width="300" height="200"或 CSSaspect-ratio: 3/2(宽高比 3:2)。
2. 占位图优化
- 使用低质量占位图(LQIP):先加载模糊的小图,再替换为高清图;
- 使用纯色占位图:与页面风格一致的纯色背景,提升视觉体验;
- 避免空占位(无
src):会导致浏览器发送无效请求(如about:blank),建议用 1x1 透明像素图作为默认占位。
3. 响应式图片支持
如果需要适配不同屏幕尺寸,使用 data-srcset 和 sizes 属性:
<img
class="lazy"
data-srcset="
https://example.com/img-small.jpg 480w,
https://example.com/img-medium.jpg 768w,
https://example.com/img-large.jpg 1200w
"
data-sizes="(max-width: 600px) 480px, (max-width: 900px) 768px, 1200px"
src="placeholder.jpg"
alt="响应式图片"
>
加载时替换:el.srcset = el.dataset.srcset; el.sizes = el.dataset.sizes;
4. 错误处理
图片加载失败时显示备用图:
el.onerror = () => {
el.src = 'https://example.com/error-img.jpg'; // 备用图
};
5. 优先使用原生 loading="lazy"(极简方案)
现代浏览器(Chrome 76+、Firefox 75+、Edge 79+)支持原生懒加载属性 loading="lazy",无需写 JS 代码,直接在图片标签中使用:
<img
src="https://example.com/real-img.jpg"
alt="原生懒加载"
loading="lazy" <!-- 原生懒加载属性 -->
width="300"
height="200"
>
- 优点:零 JS 代码、浏览器原生优化、性能最好;
- 缺点:兼容性有限(不支持 IE 和旧版浏览器)、无法自定义占位图 / 淡入效果;
- 适用场景:简单场景(无需复杂优化)、目标浏览器是现代浏览器。
六、方案对比与选型建议
| 方案 | 兼容性 | 性能 | 灵活性(自定义) | 适用场景 |
|---|---|---|---|---|
| 原生滚动监听 | 所有浏览器 | 一般 | 高 | 兼容旧浏览器(如 IE)、简单场景 |
| Intersection Observer | 现代浏览器 | 优秀 | 高 | 生产环境、长列表、复杂优化需求 |
| Vue/React 框架封装 | 框架适配 | 优秀 | 中 | Vue/React 项目、组件化开发 |
原生 loading="lazy" | 现代浏览器 | 最优 | 低 | 极简场景、无需复杂自定义 |
选型优先级
- 生产环境(现代浏览器为主) :Intersection Observer API(方案 2);
- Vue/React 项目:框架封装方案(方案 3);
- 兼容 IE 或简单场景:原生滚动监听(方案 1);
- 极简需求(无自定义) :原生
loading="lazy"(方案 5 优化细节)。
总结
图片懒加载的核心是 “按需加载”,关键在于 高效判断图片是否进入可视区域:
- 简单场景用原生
loading="lazy",零成本实现; - 生产环境优先用
Intersection Observer,兼顾性能和灵活性; - 框架项目用封装好的指令 / 组件,贴合开发习惯;
- 优化重点:设置图片宽高(避免 CLS)、添加占位图、提前加载(rootMargin)、错误处理。
按上述方案实现,可显著提升长页面的首屏加载速度和用户体验,是前端性能优化的必备手段。