文章目录
-
- **一、什么是懒加载?**
-
- **核心思想**
- **应用场景**
- **懒加载 vs 预加载**
- **二、为什么需要图片懒加载?**
-
- **性能收益**
- **SEO 友好**
- **三、图片懒加载实现方案**
-
- **方案1:原生 Intersection Observer API(推荐)**
- **方案2:兼容性更好的滚动监听**
- **方案3:响应式图片懒加载**
- **方案4:背景图片懒加载**
- **四、现代最佳实践**
-
- **1. 使用原生 loading="lazy"**
- **2. 结合 IntersectionObserver 和原生支持**
- **3. 图片优化组合拳**
- **4. 性能监控**
- **五、常见问题与解决方案**
-
- **问题1:布局抖动(Layout Shift)**
- **问题2:SEO 优化**
- **问题3:打印支持**
- **六、框架集成**
-
- **React 实现**
- **Vue 实现**
- **七、性能对比数据**
- **总结**
-
- **选择策略**
- **推荐库**
- **黄金法则**
一、什么是懒加载?
懒加载是一种延迟加载资源的优化技术,只在资源需要显示或使用时才进行加载,而不是在页面初始化时一次性加载所有资源。
核心思想
- 按需加载:可视区域内资源优先加载
- 延迟加载:非关键资源延后加载
- 异步加载:不阻塞页面渲染
应用场景
懒加载 vs 预加载
| 时机 | 需要时加载 | 提前加载 |
| 目标 | 非关键资源 | 关键资源 |
| 网络 | 节省流量 | 占用带宽 |
| 体验 | 可能延迟显示 | 更快呈现 |
二、为什么需要图片懒加载?
性能收益
// 假设一个页面有 100 张图片
// 无懒加载:同时发起 100 个请求,下载 10MB+
// 有懒加载:首屏只加载 5-10 张,后续按需加载
// 性能提升指标:
// – 页面加载时间减少 30-50%
// – 带宽节省 40-70%
// – 内存占用降低
// – 电池续航提升(移动端)
SEO 友好
- 现代搜索引擎(Google)支持懒加载
- 不影响爬虫抓取内容
- 提升页面速度得分(Core Web Vitals)
三、图片懒加载实现方案
方案1:原生 Intersection Observer API(推荐)
<!– HTML 结构 –>
<img
class="lazy"
data-src="real-image.jpg"
src="placeholder.jpg"
alt="描述"
loading="lazy" <!– 浏览器原生懒加载 —>
width="800" <!– 避免布局偏移 –>
height="600"
>
// 基础实现
document.addEventListener("DOMContentLoaded", function() {
const lazyImages = document.querySelectorAll('img.lazy');
// 创建观察器
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 替换 src
img.src = img.dataset.src;
img.classList.remove('lazy');
// 图片加载完成后的回调
img.onload = () => {
img.classList.add('loaded');
};
// 停止观察
observer.unobserve(img);
}
});
}, {
// 配置选项
root: null, // 视口
rootMargin: '50px', // 提前 50px 加载
threshold: 0.1 // 10% 可见时触发
});
// 观察所有懒加载图片
lazyImages.forEach(img => imageObserver.observe(img));
});
优化版本:
class LazyLoader {
constructor(options = {}) {
this.options = {
root: null,
rootMargin: '50px',
threshold: 0.01,
placeholder: 'data:image/svg+xml;base64,…', // 1×1 透明图
errorImage: 'error.jpg',
…options
};
this.observer = null;
this.init();
}
init() {
// 检测浏览器支持
if ('IntersectionObserver' in window) {
this.initObserver();
} else {
this.fallbackPolyfill();
}
// 错误处理
this.addErrorHandlers();
}
initObserver() {
this.observer = new IntersectionObserver(this.handleIntersect.bind(this), this.options);
document.querySelectorAll('[data-src], [data-srcset]').forEach(el => {
// 设置占位图
if (el.tagName === 'IMG' && !el.src) {
el.src = this.options.placeholder;
}
this.observer.observe(el);
});
}
handleIntersect(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(element) {
// 处理 src 和 srcset
if (element.dataset.src) {
element.src = element.dataset.src;
delete element.dataset.src;
}
if (element.dataset.srcset) {
element.srcset = element.dataset.srcset;
delete element.dataset.srcset;
}
// 处理背景图
if (element.dataset.bg) {
element.style.backgroundImage = `url(${element.dataset.bg})`;
delete element.dataset.bg;
}
}
addErrorHandlers() {
document.addEventListener('error', e => {
if (e.target.tagName === 'IMG' && e.target.classList.contains('lazy')) {
e.target.src = this.options.errorImage;
e.target.onerror = null; // 防止循环
}
}, true);
}
fallbackPolyfill() {
// 兼容方案:滚动事件 + 节流
const lazyLoad = () => {
const scrollTop = window.pageYOffset;
document.querySelectorAll('[data-src]').forEach(img => {
if (img.offsetTop < window.innerHeight + scrollTop) {
this.loadImage(img);
}
});
};
window.addEventListener('scroll', throttle(lazyLoad, 200));
window.addEventListener('resize', lazyLoad);
window.addEventListener('orientationchange', lazyLoad);
}
}
// 节流函数
function throttle(fn, delay) {
let timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
};
}
// 使用
const lazyLoader = new LazyLoader({
rootMargin: '100px',
threshold: 0
});
方案2:兼容性更好的滚动监听
// 适用于不支持 IntersectionObserver 的浏览器
function lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]');
const scrollTop = window.pageYOffset;
const windowHeight = window.innerHeight;
images.forEach(img => {
const imgTop = img.getBoundingClientRect().top + scrollTop;
// 判断是否进入可视区域
if (imgTop < scrollTop + windowHeight + 200) { // 提前 200px 加载
// 加载图片
img.src = img.dataset.src;
// 处理响应式图片
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
// 移除 data 属性
img.removeAttribute('data-src');
// 图片加载完成
img.onload = () => {
img.classList.add('loaded');
};
// 加载失败处理
img.onerror = () => {
img.src = 'fallback.jpg';
img.classList.add('error');
};
}
});
}
// 使用节流优化
const throttleLazyLoad = throttle(lazyLoadImages, 100);
// 监听事件
window.addEventListener('scroll', throttleLazyLoad);
window.addEventListener('resize', throttleLazyLoad);
window.addEventListener('orientationchange', throttleLazyLoad);
// 初始加载
document.addEventListener('DOMContentLoaded', lazyLoadImages);
方案3:响应式图片懒加载
<!– 支持 srcset 和 sizes –>
<img
class="lazy"
data-src="image-800w.jpg"
data-srcset="
image-400w.jpg 400w,
image-800w.jpg 800w,
image-1200w.jpg 1200w
"
data-sizes="
(max-width: 480px) 100vw,
(max-width: 768px) 50vw,
33vw
"
src="placeholder.jpg"
alt="响应式图片"
>
// 处理响应式图片
function loadResponsiveImage(img) {
if (img.dataset.src) {
img.src = img.dataset.src;
}
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
if (img.dataset.sizes) {
img.sizes = img.dataset.sizes;
}
// 清理 data 属性
['src', 'srcset', 'sizes'].forEach(attr => {
delete img.dataset[attr];
});
}
方案4:背景图片懒加载
<div
class="lazy-bg"
data-bg="image.jpg"
style="background-image: url('placeholder.jpg');"
></div>
// 背景图片懒加载
function lazyLoadBackgrounds() {
const elements = document.querySelectorAll('[data-bg]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
// 创建图片预加载
const img = new Image();
img.src = element.dataset.bg;
img.onload = () => {
// 应用背景图
element.style.backgroundImage = `url(${element.dataset.bg})`;
element.classList.add('loaded');
delete element.dataset.bg;
};
observer.unobserve(element);
}
});
});
elements.forEach(el => observer.observe(el));
}
四、现代最佳实践
1. 使用原生 loading=“lazy”
<!– 最简单的方式,现代浏览器支持 –>
<img
src="image.jpg"
loading="lazy"
alt="描述"
width="800"
height="600"
>
<!– iframe 也支持 –>
<iframe
src="video.html"
loading="lazy"
width="800"
height="600"
></iframe>
浏览器支持检测:
if ('loading' in HTMLImageElement.prototype) {
// 浏览器支持原生懒加载
console.log('Native lazy loading supported');
} else {
// 回退到 JavaScript 实现
}
2. 结合 IntersectionObserver 和原生支持
// 智能懒加载选择器
class SmartLazyLoad {
constructor() {
this.supportsNative = 'loading' in HTMLImageElement.prototype;
this.init();
}
init() {
if (this.supportsNative) {
this.enableNativeLazyLoad();
} else {
this.enfallbackLazyLoad();
}
}
enableNativeLazyLoad() {
// 为所有图片添加 loading="lazy"(首屏图片除外)
document.querySelectorAll('img:not([loading])').forEach((img, index) => {
// 前 3 张图片立即加载(首屏)
if (index >= 3) {
img.loading = 'lazy';
}
});
}
// … 回退方案
}
3. 图片优化组合拳
<!– 完整优化方案 –>
<picture>
<!– WebP 格式(优先) –>
<source
data-srcset="image.webp"
type="image/webp"
media="(min-width: 768px)"
>
<source
data-srcset="image-mobile.webp"
type="image/webp"
media="(max-width: 767px)"
>
<!– 回退格式 –>
<source
data-srcset="image.jpg"
type="image/jpeg"
media="(min-width: 768px)"
>
<source
data-srcset="image-mobile.jpg"
type="image/jpeg"
media="(max-width: 767px)"
>
<!– 最终回退 –>
<img
class="lazy"
data-src="image.jpg"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjYwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48L3N2Zz4="
alt="优化图片"
loading="lazy"
width="800"
height="600"
onload="this.classList.add('loaded')"
>
</picture>
4. 性能监控
// 监控懒加载性能
const lazyLoadMetrics = {
images: [],
startTime: performance.now()
};
// 记录图片加载时间
document.querySelectorAll('img.lazy').forEach(img => {
const start = performance.now();
img.onload = () => {
const loadTime = performance.now() – start;
lazyLoadMetrics.images.push({
src: img.src,
loadTime,
visible: img.offsetTop < window.innerHeight
});
// 上报到监控系统
if (lazyLoadMetrics.images.length === 1) {
const firstImageLoad = performance.now() – lazyLoadMetrics.startTime;
reportMetric('firstLazyImageLoad', firstImageLoad);
}
};
});
五、常见问题与解决方案
问题1:布局抖动(Layout Shift)
解决:
/* 1. 固定宽高 */
.lazy-img {
width: 100%;
height: 0;
padding-bottom: 75%; /* 4:3 比例 */
background: #f5f5f5;
overflow: hidden;
}
/* 2. 使用占位容器 */
.img-wrapper {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%; /* 16:9 */
}
.img-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* 3. 渐入效果 */
.lazy {
opacity: 0;
transition: opacity 0.3s;
}
.lazy.loaded {
opacity: 1;
}
问题2:SEO 优化
<!– 添加结构化数据 –>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ImageObject",
"contentUrl": "real-image.jpg",
"name": "图片标题",
"description": "图片描述"
}
</script>
<!– 确保爬虫能抓取 –>
<noscript>
<img src="real-image.jpg" alt="描述">
</noscript>
问题3:打印支持
/* 打印时加载所有图片 */
@media print {
img.lazy {
content: attr(data-src);
}
.lazy {
background-image: attr(data-bg) !important;
}
}
六、框架集成
React 实现
import React, { useEffect, useRef } from 'react';
import { throttle } from 'lodash';
const LazyImage = ({ src, alt, placeholder, …props }) => {
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
},
{ rootMargin: '50px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
data-src={src}
src={placeholder}
alt={alt}
{…props}
/>
);
};
// 使用
<LazyImage
src="real-image.jpg"
placeholder="data:image/svg+xml;base64,…"
alt="描述"
width={800}
height={600}
/>
Vue 实现
<template>
<img
:src="placeholder"
:data-src="realSrc"
:alt="alt"
ref="lazyImage"
class="lazy-image"
/>
</template>
<script>
export default {
name: 'LazyImage',
props: {
realSrc: String,
placeholder: {
type: String,
default: 'data:image/svg+xml;base64,…'
},
alt: String
},
mounted() {
this.initLazyLoad();
},
methods: {
initLazyLoad() {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage();
observer.unobserve(this.$refs.lazyImage);
}
});
}, { threshold: 0.01 });
observer.observe(this.$refs.lazyImage);
} else {
this.fallbackLoad();
}
},
loadImage() {
this.$refs.lazyImage.src = this.realSrc;
this.$emit('loaded');
},
fallbackLoad() {
window.addEventListener('scroll', this.checkVisibility);
this.checkVisibility();
},
checkVisibility: throttle(function() {
const rect = this.$refs.lazyImage.getBoundingClientRect();
if (rect.top < window.innerHeight + 100) {
this.loadImage();
window.removeEventListener('scroll', this.checkVisibility);
}
}, 100)
}
};
</script>
七、性能对比数据
| 无懒加载 | 慢 | 100% | 高 | 100% |
| 滚动监听 | 快 | 30-40% | 中 | 99% |
| IntersectionObserver | 很快 | 20-30% | 低 | 95% |
| 原生 loading=“lazy” | 最快 | 15-25% | 最低 | 85% |
总结
选择策略
推荐库
- lozad.js:超轻量(1KB)
- lazysizes:功能全面
- react-lazyload:React 专用
- vue-lazyload:Vue 专用
黄金法则
懒加载是现代 Web 优化的必备技术,合理使用可显著提升页面性能,特别是在移动端和弱网环境下效果更为明显。




