跳到主要内容

图片优化

问题

前端如何优化图片加载性能?有哪些图片格式可选?如何实现图片懒加载?

答案

图片通常占网页总大小的 50% 以上,是性能优化的重点。优化策略包括格式选择、压缩、懒加载、响应式图片等。


图片格式对比

格式特点透明动画压缩兼容性适用场景
JPEG有损压缩全部照片
PNG无损压缩全部图标、截图
GIF256色全部简单动画
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="响应式图片"
>

浏览器会根据:

  1. 设备像素比(DPR)
  2. 视口宽度
  3. 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">

相关链接