图片优化
问题
前端如何优化图片加载性能?有哪些图片格式可选?如何实现图片懒加载?
答案
图片通常占网页总大小的 50% 以上,是性能优化的重点。优化策略包括格式选择、压缩、懒加载、响应式图片等。
图片格式对比
| 格式 | 特点 | 透明 | 动画 | 压缩 | 兼容性 | 适用场景 |
|---|---|---|---|---|---|---|
| JPEG | 有损压缩 | ❌ | ❌ | 高 | 全部 | 照片 |
| PNG | 无损压缩 | ✅ | ❌ | 中 | 全部 | 图标、截图 |
| GIF | 256色 | ✅ | ✅ | 低 | 全部 | 简单动画 |
| WebP | 有损/无损 | ✅ | ✅ | 更高 | 现代浏览器 | 通用 |
| AVIF | 有损/无损 | ✅ | ✅ | 最高 | 较新浏览器 | 通用 |
| SVG | 矢量 | ✅ | ✅ | - | 全部 | 图标、插图 |
格式选择与降级
<!-- picture 元素实现格式降级 -->
<picture>
<!-- 优先 AVIF -->
<source srcset="image.avif" type="image/avif">
<!-- 次选 WebP -->
<source srcset="image.webp" type="image/webp">
<!-- 兜底 JPEG -->
<img src="image.jpg" alt="描述" loading="lazy">
</picture>
// 检测浏览器支持
function supportsWebP(): Promise<boolean> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img.width > 0 && img.height > 0);
img.onerror = () => resolve(false);
img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAQAcJYgCdAEO/hOMAA==';
});
}
// 动态加载对应格式
async function getImageSrc(baseName: string): Promise<string> {
const supportsAvif = await supportsAVIF();
const supportsWebp = await supportsWebP();
if (supportsAvif) return `${baseName}.avif`;
if (supportsWebp) return `${baseName}.webp`;
return `${baseName}.jpg`;
}
图片压缩
构建时压缩
// vite.config.ts
import viteImagemin from 'vite-plugin-imagemin';
export default {
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 3 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.7, 0.9] },
webp: { quality: 80 },
svgo: {
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'removeEmptyAttrs', active: true }
]
}
})
]
};
在线压缩工具
图片懒加载
原生懒加载
<!-- loading="lazy" 原生支持 -->
<img src="image.jpg" loading="lazy" alt="懒加载图片">
<!-- 配合 decoding -->
<img
src="image.jpg"
loading="lazy"
decoding="async"
alt="异步解码"
>
Intersection Observer 实现
class LazyImageLoader {
private observer: IntersectionObserver;
constructor() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target as HTMLImageElement);
this.observer.unobserve(entry.target);
}
});
},
{
rootMargin: '50px 0px', // 提前 50px 加载
threshold: 0.01
}
);
}
observe(images: NodeListOf<HTMLImageElement>): void {
images.forEach((img) => this.observer.observe(img));
}
private loadImage(img: HTMLImageElement): void {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (src) img.src = src;
if (srcset) img.srcset = srcset;
img.classList.add('loaded');
}
disconnect(): void {
this.observer.disconnect();
}
}
// 使用
const loader = new LazyImageLoader();
const lazyImages = document.querySelectorAll<HTMLImageElement>('img[data-src]');
loader.observe(lazyImages);
React 懒加载组件
import { useState, useEffect, useRef } from 'react';
interface LazyImageProps {
src: string;
alt: string;
placeholder?: string;
className?: string;
}
function LazyImage({ src, alt, placeholder, className }: LazyImageProps) {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ rootMargin: '50px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isInView ? src : placeholder || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}
alt={alt}
className={`${className} ${isLoaded ? 'loaded' : 'loading'}`}
onLoad={() => setIsLoaded(true)}
/>
);
}
响应式图片
srcset 和 sizes
<!-- 根据设备像素比选择 -->
<img
src="image-1x.jpg"
srcset="
image-1x.jpg 1x,
image-2x.jpg 2x,
image-3x.jpg 3x
"
alt="响应式图片"
>
<!-- 根据视口宽度选择 -->
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
800px
"
alt="响应式图片"
>
picture 元素艺术指导
<!-- 不同屏幕显示不同裁剪 -->
<picture>
<!-- 移动端:竖版裁剪 -->
<source
media="(max-width: 600px)"
srcset="hero-mobile.jpg"
>
<!-- 平板:方形裁剪 -->
<source
media="(max-width: 1024px)"
srcset="hero-tablet.jpg"
>
<!-- 桌面:横版 -->
<img src="hero-desktop.jpg" alt="Hero">
</picture>
图片预加载
<!-- 预加载关键图片 -->
<link rel="preload" as="image" href="hero.webp" type="image/webp">
<!-- 响应式预加载 -->
<link
rel="preload"
as="image"
href="hero.webp"
imagesrcset="hero-400.webp 400w, hero-800.webp 800w"
imagesizes="100vw"
>
// JavaScript 预加载
function preloadImage(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = reject;
img.src = src;
});
}
// 预加载多张图片
async function preloadImages(srcs: string[]): Promise<void> {
await Promise.all(srcs.map(preloadImage));
}
CDN 图片处理
// 阿里云 OSS 图片处理
function getOSSImageUrl(url: string, options: {
width?: number;
height?: number;
quality?: number;
format?: 'webp' | 'avif' | 'jpg';
}): string {
const params: string[] = [];
if (options.width) params.push(`w_${options.width}`);
if (options.height) params.push(`h_${options.height}`);
if (options.quality) params.push(`q_${options.quality}`);
if (options.format) params.push(`format,${options.format}`);
return `${url}?x-oss-process=image/resize,${params.join(',')}`;
}
// 七牛云图片处理
function getQiniuImageUrl(url: string, width: number, quality = 80): string {
return `${url}?imageView2/2/w/${width}/q/${quality}/format/webp`;
}
// 使用示例
const optimizedUrl = getOSSImageUrl('https://cdn.example.com/image.jpg', {
width: 800,
quality: 80,
format: 'webp'
});
占位符策略
LQIP(低质量图片占位)
// 生成模糊占位图
function generateLQIP(src: string): string {
// 通常由后端/CDN 生成 10x10 的极小图片
return `${src}?w=10&blur=20`;
}
// React 组件
function BlurImage({ src, alt }: { src: string; alt: string }) {
const [loaded, setLoaded] = useState(false);
const lqip = generateLQIP(src);
return (
<div className="blur-image-container">
<img
src={lqip}
alt=""
className={`placeholder ${loaded ? 'hidden' : ''}`}
style={{ filter: 'blur(20px)' }}
/>
<img
src={src}
alt={alt}
className={`main-image ${loaded ? 'visible' : ''}`}
onLoad={() => setLoaded(true)}
/>
</div>
);
}
骨架屏占位
.image-skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
常见面试问题
Q1: 如何选择图片格式?
答案:
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 照片 | WebP > AVIF > JPEG | 压缩率高,质量好 |
| 透明图 | WebP > PNG | 支持透明且更小 |
| 图标 | SVG | 矢量无损,可缩放 |
| 简单动画 | WebP > GIF | 更小,更多色彩 |
| 复杂动画 | MP4/WebM | 视频比 GIF 小很多 |
<!-- 最佳实践:格式降级 -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="图片">
</picture>
Q2: 图片懒加载的实现方式?
答案:
// 方式1:原生 loading 属性(推荐)
<img src="image.jpg" loading="lazy" alt="">
// 方式2:Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
// 方式3:第三方库(lozad.js、lazysizes)
Q3: srcset 和 sizes 属性的作用?
答案:
- srcset:提供不同尺寸/分辨率的图片候选
- sizes:告诉浏览器图片在不同视口下的显示尺寸
<img
src="default.jpg"
srcset="
small.jpg 400w,
medium.jpg 800w,
large.jpg 1200w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
600px
"
alt="响应式图片"
>
浏览器会根据:
- 设备像素比(DPR)
- 视口宽度
- sizes 定义的显示尺寸
自动选择最合适的图片。
Q4: 如何优化大量图片的页面?
答案:
// 综合优化策略
const imageOptimizationStrategy = {
// 1. 格式优化
format: '使用 WebP/AVIF,配合 picture 降级',
// 2. 尺寸优化
size: '根据容器大小提供合适尺寸,使用 srcset',
// 3. 压缩
compression: '构建时压缩,CDN 实时压缩',
// 4. 懒加载
lazyLoad: '首屏外图片使用懒加载',
// 5. 预加载
preload: '关键图片使用 preload',
// 6. CDN
cdn: '使用 CDN 加速,配合边缘缓存',
// 7. 占位符
placeholder: 'LQIP 或骨架屏,提升体验',
// 8. 雪碧图/内联
sprite: '小图标使用 SVG 雪碧图或内联'
};
Q5: 首屏图片如何优化?
答案:
<!-- 1. 预加载关键图片 -->
<link rel="preload" as="image" href="hero.webp">
<!-- 2. 使用高优先级 -->
<img src="hero.webp" fetchpriority="high" alt="Hero">
<!-- 3. 避免 CLS -->
<img
src="hero.webp"
width="1200"
height="600"
style="aspect-ratio: 2/1"
alt="Hero"
>
<!-- 4. 内联关键小图 -->
<img src="data:image/svg+xml;base64,..." alt="Logo">