跳到主要内容

图片压缩与优化全指南

为什么图片优化至关重要?

图片是前端页面中体积最大的资源类型。根据 HTTP Archive 的统计数据,图片通常占据页面总传输量的 60% 以上。一张未经优化的高清图片可能达到数 MB,而一个完整页面的 HTML + CSS + JS 加起来可能只有几百 KB。

来看一组优化前后的对比数据:

指标优化前优化后提升幅度
页面总大小5.2 MB1.8 MB65%↓
图片总大小4.0 MB0.9 MB77%↓
首屏加载时间6.5s2.1s68%↓
LCP4.8s1.9s60%↓
每月流量成本¥5000¥150070%↓
核心认知

图片优化是投入产出比最高的性能优化手段 —— 不需要改动业务代码,仅通过图片格式、压缩、加载策略的优化就能大幅提升页面性能。

图片加载对关键性能指标有直接影响:


图片格式全解析

JPEG / PNG / GIF — 传统三剑客

JPEG(Joint Photographic Experts Group)

JPEG 是最常见的有损压缩格式,特别适合摄影照片和色彩丰富的图像。

特点:
- 支持 1600 万色(24 位真彩色)
- 有损压缩,压缩率高
- 不支持透明度
- 不支持动画
- 渐进式 JPEG 可以从模糊到清晰逐步加载

PNG(Portable Network Graphics)

PNG 是无损压缩格式,适合需要透明背景的图标、Logo 等。

特点:
- PNG-8:256 色,体积小
- PNG-24:1600 万色,体积较大
- PNG-32:1600 万色 + Alpha 透明通道
- 无损压缩
- 支持透明度
- 不支持动画(APNG 除外)

GIF(Graphics Interchange Format)

GIF 是一种古老但仍在使用的格式,因为它支持动画。

特点:
- 最多 256 色(8 位)
- 支持简单动画
- 支持透明(但仅 1 位透明度,边缘锯齿明显)
- 无损压缩(但色彩有限)
- 动画文件体积大,现代场景建议用视频替代

WebP — 现代格式首选

WebP 是 Google 开发的现代图片格式,同时支持有损和无损压缩,是当前前端的首选图片格式

特点:
- 有损压缩比 JPEG 小 25-35%
- 无损压缩比 PNG 小 26%
- 支持透明度(Alpha 通道)
- 支持动画(替代 GIF)
- 主流浏览器均已支持(Chrome、Firefox、Safari 14+、Edge)
WebP 兼容性

截至 2025 年,WebP 的全球浏览器支持率已超过 97%,可以放心在生产环境中使用。仅 IE 不支持,但 IE 已经退役。

AVIF — 下一代格式

AVIF(AV1 Image File Format)基于 AV1 视频编码,是目前压缩率最高的图片格式。

特点:
- 压缩率比 WebP 再高 20-50%
- 支持 HDR(高动态范围)
- 支持透明度和动画
- 编码速度较慢
- 兼容性:Chrome 85+、Firefox 93+、Safari 16.4+
AVIF 注意事项

AVIF 的编码速度明显慢于 WebP 和 JPEG,不适合实时压缩场景(如用户上传后即时处理)。建议在构建时预压缩或使用 CDN 离线转码。

SVG — 矢量图的天下

SVG(Scalable Vector Graphics)是基于 XML 的矢量图格式,任意缩放不失真。

特点:
- 矢量图形,无限缩放不失真
- 本质是文本(XML),可以用 CSS/JS 操控
- 适合图标、Logo、简单插图
- 复杂图形体积可能很大
- 可以内联到 HTML 中
- 支持动画

格式对比总表

特性JPEGPNGGIFWebPAVIFSVG
压缩方式有损无损无损有损/无损有损/无损无损
透明度⚠️ 1位
动画
压缩率很高最高-
色彩深度24位8/24/32位8位24位最高12位/通道-
浏览器支持全部全部全部97%+92%+全部
适用场景照片图标/透明图简单动画通用替代高压缩需求图标/矢量

如何选择图片格式?


压缩原理:有损 vs 无损

理解压缩原理有助于在「图片质量」和「文件体积」之间做出合理权衡。

无损压缩原理

无损压缩不丢弃任何数据,解压后可以完全还原原始图像,适合需要精确还原的场景。

常见算法:

  • LZ77 / LZW:查找重复数据序列,用更短的引用替代(PNG 使用 Deflate,GIF 使用 LZW)
  • 霍夫曼编码:对出现频率不同的数据分配不等长编码,高频数据用短编码
  • 预测编码:PNG 的行级预测滤波器,利用相邻像素的相关性减少数据量
原始数据: AAABBBCCCCDDDD
LZ77压缩: A(3)B(3)C(4)D(4) ← 记录字符和重复次数
存储空间: 14字节 → 8字节

有损压缩原理

有损压缩会有选择地丢弃人眼不敏感的信息,换取更高的压缩率。以 JPEG 为例:

  1. 色彩空间转换:RGB → YCbCr(亮度 + 色度),人眼对亮度更敏感,对色度分辨率要求低
  2. 色度下采样:将色度通道的分辨率降低(如 4:2:0,色度分辨率减半)
  3. DCT 变换:将 8×8 像素块做离散余弦变换,将空间域转换为频率域
  4. 量化:对高频分量(细节)进行更大幅度的量化舍入(这是信息丢失的主要步骤)
  5. 熵编码:使用霍夫曼编码或算术编码进一步压缩

两种方式的核心区别:

特性无损压缩有损压缩
数据完整性可 100% 还原不可逆,有信息丢失
压缩率较低(2:1 ~ 5:1)较高(10:1 ~ 50:1)
图片质量与原图完全一致取决于压缩质量参数
适用场景图标、Logo、截图、技术图照片、背景图、Banner
典型格式PNG、GIF、WebP(无损)JPEG、WebP(有损)、AVIF
质量参数建议

JPEG/WebP 有损压缩的 quality 参数建议设置在 70-85 之间:

  • quality: 85 — 肉眼几乎看不出区别,体积减少约 50%
  • quality: 75 — 细看能发现轻微模糊,体积减少约 65%
  • quality: 60 — 明显可见压缩痕迹,仅适合缩略图

前端实现图片压缩

Canvas API 压缩方案

Canvas 是前端最常用的图片压缩方案,原理是将图片绘制到 Canvas 上,再以指定质量导出。

完整实现:

/**
* 前端图片压缩函数
* @param {File} file - 用户上传的图片文件
* @param {Object} options - 压缩选项
* @param {number} options.quality - 压缩质量 0-1,默认 0.8
* @param {number} options.maxWidth - 最大宽度,默认 1920
* @param {number} options.maxHeight - 最大高度,默认 1080
* @param {string} options.mimeType - 输出格式,默认 'image/jpeg'
* @returns {Promise<Blob>} 压缩后的 Blob 对象
*/
function compressImage(file, options = {}) {
const {
quality = 0.8,
maxWidth = 1920,
maxHeight = 1080,
mimeType = 'image/jpeg',
} = options;

return new Promise((resolve, reject) => {
// 1. 读取文件为 DataURL
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
// 2. 计算压缩后的尺寸(保持宽高比)
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}

// 3. 绘制到 Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);

// 4. 导出为 Blob
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Canvas toBlob failed'));
}
},
mimeType,
quality
);
};
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}

// 使用示例
const fileInput = document.querySelector('#fileInput');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
console.log('压缩前:', (file.size / 1024).toFixed(1), 'KB');

const compressedBlob = await compressImage(file, {
quality: 0.75,
maxWidth: 1280,
});
console.log('压缩后:', (compressedBlob.size / 1024).toFixed(1), 'KB');
console.log('压缩率:', ((1 - compressedBlob.size / file.size) * 100).toFixed(1) + '%');

// 上传到服务器
const formData = new FormData();
formData.append('image', compressedBlob, file.name);
// fetch('/api/upload', { method: 'POST', body: formData });
});
注意事项

Canvas 压缩有以下局限性:

  1. 仅支持有损压缩toBlobquality 参数只对 image/jpegimage/webp 有效,PNG 是无损的
  2. EXIF 信息丢失:Canvas 重绘后会丢失原图的拍摄参数、方向等 EXIF 元数据
  3. 色彩偏差:Canvas 使用 premultiplied alpha,某些半透明图片可能出现色差
  4. 大图内存问题:超大图片可能导致 Canvas 内存溢出(移动端尤其常见)

File API + Blob 处理

除了 Canvas 压缩,还需要了解如何操作图片文件对象:

// 将 File 转为可预览的 URL
function fileToURL(file) {
return URL.createObjectURL(file);
}

// 将 Blob 转为 File 对象(用于上传)
function blobToFile(blob, fileName) {
return new File([blob], fileName, {
type: blob.type,
lastModified: Date.now(),
});
}

// 将 DataURL 转为 Blob
function dataURLToBlob(dataURL) {
const [header, data] = dataURL.split(',');
const mime = header.match(/:(.*?);/)[1];
const binary = atob(data);
const array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i);
}
return new Blob([array], { type: mime });
}

// 获取图片的原始尺寸
function getImageSize(file) {
return new Promise((resolve) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
resolve({ width: img.naturalWidth, height: img.naturalHeight });
URL.revokeObjectURL(url);
};
img.src = url;
});
}

Web Worker 异步压缩

对于批量压缩或大图处理,将压缩任务放到 Web Worker 中可以避免阻塞主线程:

// compress-worker.js
self.onmessage = async (e) => {
const { imageData, width, height, quality, mimeType } = e.data;

// 使用 OffscreenCanvas 在 Worker 中处理
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');

// 将 ImageData 绘制到 OffscreenCanvas
ctx.putImageData(imageData, 0, 0);

// 导出为 Blob
const blob = await canvas.convertToBlob({
type: mimeType,
quality: quality,
});

// 将结果发送回主线程
self.postMessage({ blob, originalSize: imageData.data.length });
};
// 主线程调用
class ImageCompressor {
constructor() {
this.worker = new Worker('compress-worker.js');
}

compress(file, options = {}) {
const { quality = 0.8, maxWidth = 1920, mimeType = 'image/jpeg' } = options;

return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
// 计算目标尺寸
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}

// 在主线程获取 ImageData
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);

// 发送到 Worker 处理
this.worker.onmessage = (e) => resolve(e.data.blob);
this.worker.onerror = reject;
this.worker.postMessage(
{ imageData, width, height, quality, mimeType },
[imageData.data.buffer] // 转移所有权,避免复制
);
};
img.src = URL.createObjectURL(file);
});
}

destroy() {
this.worker.terminate();
}
}

// 使用示例
const compressor = new ImageCompressor();
const blob = await compressor.compress(file, { quality: 0.75 });
compressor.destroy();
OffscreenCanvas 兼容性

OffscreenCanvas 已被 Chrome、Firefox、Safari 16.4+ 支持。对于不支持的环境,可以降级回主线程 Canvas 方案。

React / Vue 组件封装

React 图片压缩上传组件:

import { useState, useCallback } from 'react';

function compressImage(file: File, quality = 0.8): Promise<Blob> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 1920;
let { width, height } = img;
if (width > MAX_WIDTH) {
height = (height * MAX_WIDTH) / width;
width = MAX_WIDTH;
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d')!.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => blob ? resolve(blob) : reject(), 'image/jpeg', quality);
};
img.src = e.target!.result as string;
};
reader.readAsDataURL(file);
});
}

export default function ImageUploader() {
const [preview, setPreview] = useState<string>('');
const [info, setInfo] = useState({ original: 0, compressed: 0 });

const handleUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

const originalSize = file.size;
const compressed = await compressImage(file, 0.75);
const compressedSize = compressed.size;

setInfo({ original: originalSize, compressed: compressedSize });
setPreview(URL.createObjectURL(compressed));

// 上传
const formData = new FormData();
formData.append('image', compressed, file.name);
// await fetch('/api/upload', { method: 'POST', body: formData });
}, []);

return (
<div>
<input type="file" accept="image/*" onChange={handleUpload} />
{preview && <img src={preview} alt="preview" style={{ maxWidth: 400 }} />}
{info.original > 0 && (
<p>
原始大小: {(info.original / 1024).toFixed(1)} KB →
压缩后: {(info.compressed / 1024).toFixed(1)} KB
(节省 {((1 - info.compressed / info.original) * 100).toFixed(1)}%)
</p>
)}
</div>
);
}

Vue 图片压缩上传组件:

<template>
<div>
<input type="file" accept="image/*" @change="handleUpload" />
<img v-if="preview" :src="preview" alt="preview" style="max-width: 400px" />
<p v-if="info.original > 0">
原始大小: {{ (info.original / 1024).toFixed(1) }} KB →
压缩后: {{ (info.compressed / 1024).toFixed(1) }} KB
(节省 {{ ((1 - info.compressed / info.original) * 100).toFixed(1) }}%)
</p>
</div>
</template>

<script setup>
import { ref } from 'vue';

const preview = ref('');
const info = ref({ original: 0, compressed: 0 });

function compressImage(file, quality = 0.8) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 1920;
let { width, height } = img;
if (width > MAX_WIDTH) {
height = (height * MAX_WIDTH) / width;
width = MAX_WIDTH;
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => (blob ? resolve(blob) : reject()),
'image/jpeg',
quality
);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}

async function handleUpload(e) {
const file = e.target.files?.[0];
if (!file) return;

const compressed = await compressImage(file, 0.75);
info.value = { original: file.size, compressed: compressed.size };
preview.value = URL.createObjectURL(compressed);

// 上传到服务器
const formData = new FormData();
formData.append('image', compressed, file.name);
// await fetch('/api/upload', { method: 'POST', body: formData });
}
</script>

构建时图片压缩

构建时压缩可以在打包阶段自动处理项目中的所有图片,无需手动干预。

Webpack 配置(image-minimizer-webpack-plugin)

// webpack.config.js
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

module.exports = {
optimization: {
minimizer: [
// 使用 sharp 进行有损压缩
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
jpeg: { quality: 80 },
webp: { quality: 80 },
avif: { quality: 65 },
png: { compressionLevel: 9 },
},
},
},
}),
],
},
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
type: 'asset', // Webpack 5 资源模块
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 小于 8KB 转 Base64
},
},
},
],
},
};

Webpack 自动生成 WebP 格式:

// 在 minimizer 配置中新增 generator
new ImageMinimizerPlugin({
generator: [
{
// 自动为每张图片生成 WebP 版本
preset: 'webp',
implementation: ImageMinimizerPlugin.sharpGenerate,
options: {
encodeOptions: {
webp: { quality: 80 },
},
},
},
],
}),

Vite 配置(vite-plugin-imagemin)

// vite.config.js
import { defineConfig } from 'vite';
import viteImagemin from 'vite-plugin-imagemin';

export default defineConfig({
plugins: [
viteImagemin({
// GIF 优化
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
// PNG 无损优化
optipng: {
optimizationLevel: 7,
},
// JPEG 优化
mozjpeg: {
quality: 80,
},
// PNG 有损优化(二选一)
pngquant: {
quality: [0.65, 0.8],
speed: 4,
},
// SVG 优化
svgo: {
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'removeEmptyAttrs', active: false },
],
},
// WebP 转换
webp: {
quality: 80,
},
}),
],
});

构建工具配置对比

特性Webpack + image-minimizerVite + viteImagemin
底层库sharp / squooshmozjpeg / optipng / svgo
WebP 生成支持(generator)支持
AVIF 生成支持需额外插件
压缩速度sharp 很快,squoosh 较慢中等
配置复杂度中等简单
小图 Base64asset 模块内置支持内置支持
推荐方案
  • Webpack 项目:推荐 image-minimizer-webpack-plugin + sharp,压缩速度快且支持 AVIF
  • Vite 项目:推荐 vite-plugin-imagemin,配置简单直观
  • 两者都应开启自动 WebP 生成,配合 <picture> 元素实现格式降级

服务端与 CDN 图片优化

CDN 图片处理参数

主流云服务商的 CDN 都提供了图片实时处理能力,通过 URL 参数即可实现裁剪、缩放、格式转换:

阿里云 OSS 图片处理:

原图:https://cdn.example.com/photo.jpg

# 缩放到宽度 800px,高度等比缩放
https://cdn.example.com/photo.jpg?x-oss-process=image/resize,w_800

# 转换为 WebP 格式
https://cdn.example.com/photo.jpg?x-oss-process=image/format,webp

# 质量压缩 + 缩放 + 格式转换(链式处理)
https://cdn.example.com/photo.jpg?x-oss-process=image/resize,w_800/quality,q_80/format,webp

# 智能裁剪(居中裁剪为 400x400)
https://cdn.example.com/photo.jpg?x-oss-process=image/resize,m_fill,w_400,h_400

# 添加水印
https://cdn.example.com/photo.jpg?x-oss-process=image/watermark,text_5paH5a2X5rC05Y2w

腾讯云 COS 图片处理:

原图:https://cdn.example.com/photo.jpg

# 缩放到宽度 800px
https://cdn.example.com/photo.jpg?imageMogr2/thumbnail/800x

# 转换为 WebP
https://cdn.example.com/photo.jpg?imageMogr2/format/webp

# 质量压缩 + 缩放 + 格式转换
https://cdn.example.com/photo.jpg?imageMogr2/thumbnail/800x/quality/80/format/webp

内容协商:自动返回最优格式

通过 HTTP 内容协商,服务器/CDN 可以根据客户端支持的格式自动返回最优图片:

Nginx 内容协商配置示例:

# 根据浏览器 Accept 头自动选择最优图片格式
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}

map $http_accept $avif_suffix {
default "";
"~*avif" ".avif";
}

server {
location ~* ^(.+)\.(jpe?g|png)$ {
# 优先尝试 AVIF → WebP → 原格式
try_files $1$avif_suffix $1$webp_suffix $uri =404;

# 告诉 CDN/代理缓存:同一 URL 可能返回不同格式
add_header Vary Accept;
}
}
关于 Vary 头

设置 Vary: Accept 非常重要。它告诉 CDN 和代理服务器:同一 URL 根据 Accept 请求头的不同可能返回不同内容,需要按 Accept 分别缓存。如果缺少这个头,可能会把 WebP 图片缓存后返回给不支持 WebP 的浏览器。


响应式图片与自适应加载

不同设备的屏幕尺寸和分辨率差异巨大(手机 375px 到 4K 桌面 3840px),为所有设备发送同一张大图是巨大的浪费。

srcset + sizes 属性

srcset 让浏览器根据设备条件自动选择最合适的图片

<!-- 基于像素密度选择 -->
<img
src="photo-400w.jpg"
srcset="
photo-400w.jpg 1x,
photo-800w.jpg 2x,
photo-1200w.jpg 3x
"
alt="响应式图片"
/>

<!-- 基于视口宽度选择(推荐) -->
<img
src="photo-800w.jpg"
srcset="
photo-400w.jpg 400w,
photo-800w.jpg 800w,
photo-1200w.jpg 1200w,
photo-1600w.jpg 1600w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw
"
alt="响应式图片"
/>

sizes 属性的含义:

  • 视口宽度 ≤ 600px 时,图片占满屏幕宽度(100vw)→ 浏览器选择接近 600px 的图
  • 视口宽度 ≤ 1200px 时,图片占一半宽度(50vw)→ 浏览器选择接近 600px 的图
  • 其他情况图片占三分之一宽度(33vw)→ 浏览器选择接近适配宽度的图

<picture> 元素

<picture> 元素提供了更精细的控制,支持格式降级艺术指导(Art Direction):

<!-- 格式降级:优先使用现代格式 -->
<picture>
<source srcset="photo.avif" type="image/avif" />
<source srcset="photo.webp" type="image/webp" />
<img src="photo.jpg" alt="格式降级示例" />
</picture>

<!-- 格式降级 + 响应式尺寸 -->
<picture>
<source
srcset="photo-400w.avif 400w, photo-800w.avif 800w, photo-1200w.avif 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
type="image/avif"
/>
<source
srcset="photo-400w.webp 400w, photo-800w.webp 800w, photo-1200w.webp 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
type="image/webp"
/>
<img
src="photo-800w.jpg"
srcset="photo-400w.jpg 400w, photo-800w.jpg 800w, photo-1200w.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="完整响应式图片"
/>
</picture>

<!-- 艺术指导:不同屏幕使用不同裁剪 -->
<picture>
<source media="(max-width: 600px)" srcset="photo-mobile.jpg" />
<source media="(max-width: 1200px)" srcset="photo-tablet.jpg" />
<img src="photo-desktop.jpg" alt="艺术指导示例" />
</picture>

根据网络状况自适应加载

利用 Network Information API 根据用户的网络状况动态调整图片质量:

/**
* 根据网络状况获取最佳图片质量
*/
function getImageQuality() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;

if (!connection) return 'high'; // 不支持时默认高质量

const { effectiveType, saveData } = connection;

// 用户开启了省流量模式
if (saveData) return 'low';

switch (effectiveType) {
case '4g':
return 'high'; // 加载高清图
case '3g':
return 'medium'; // 加载中等质量
case '2g':
case 'slow-2g':
return 'low'; // 加载缩略图或占位图
default:
return 'high';
}
}

/**
* 自适应图片加载器
*/
function adaptiveImageSrc(baseUrl) {
const quality = getImageQuality();
const qualityMap = {
high: '?w=1200&q=85',
medium: '?w=800&q=70',
low: '?w=400&q=50',
};
return baseUrl + (qualityMap[quality] || qualityMap.high);
}

// 使用示例
const img = document.querySelector('.hero-image');
img.src = adaptiveImageSrc('https://cdn.example.com/hero.jpg');

// 监听网络变化,动态切换图片质量
navigator.connection?.addEventListener('change', () => {
const images = document.querySelectorAll('[data-adaptive-src]');
images.forEach((img) => {
img.src = adaptiveImageSrc(img.dataset.adaptiveSrc);
});
});

图片懒加载

懒加载的核心思想是延迟加载视口之外的图片,只在图片即将进入可视区域时才开始加载,减少首屏请求数和带宽消耗。

原生 loading="lazy"

HTML 原生支持的懒加载,使用最简单:

<!-- 原生懒加载 —— 一行搞定 -->
<img src="photo.jpg" loading="lazy" alt="懒加载图片" width="800" height="600" />

<!-- 首屏图片不要加 loading="lazy",反而会延迟加载 -->
<img src="hero.jpg" loading="eager" alt="首屏大图" width="1200" height="600" />

<!-- 搭配响应式和格式降级 -->
<picture>
<source srcset="photo.avif" type="image/avif" />
<source srcset="photo.webp" type="image/webp" />
<img
src="photo.jpg"
loading="lazy"
decoding="async"
width="800"
height="600"
alt="完整的懒加载方案"
/>
</picture>
关键提醒
  1. 必须设置 widthheight:浏览器需要在图片加载前知道尺寸来预留空间,避免布局偏移(CLS)
  2. 首屏图片不要懒加载:首屏的 LCP 图片使用 loading="eager"(默认值),加 lazy 反而会影响 LCP
  3. decoding="async":告诉浏览器异步解码图片,避免阻塞主线程渲染

Intersection Observer 实现

对于需要更精细控制的场景,可以用 Intersection Observer API 手动实现:

/**
* 图片懒加载类
* 支持自定义阈值、根元素、加载动画
*/
class LazyImageLoader {
constructor(options = {}) {
const {
rootMargin = '200px 0px', // 提前 200px 开始加载
threshold = 0.01, // 出现 1% 即触发
selector = 'img[data-src]',
} = options;

this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target); // 加载后停止观察
}
});
},
{ rootMargin, threshold }
);

// 观察所有待懒加载的图片
document.querySelectorAll(selector).forEach((img) => {
this.observer.observe(img);
});
}

loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;

if (srcset) img.srcset = srcset;
if (src) img.src = src;

img.classList.add('loaded');
img.removeAttribute('data-src');
img.removeAttribute('data-srcset');
}

destroy() {
this.observer.disconnect();
}
}

// 使用示例
const loader = new LazyImageLoader({
rootMargin: '300px 0px', // 滚动到距离图片 300px 时提前加载
});

配套的 HTML 和 CSS:

<!-- HTML:用 data-src 代替 src -->
<img
data-src="photo.jpg"
data-srcset="photo-400w.jpg 400w, photo-800w.jpg 800w"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='600'%3E%3Crect fill='%23f0f0f0' width='100%25' height='100%25'/%3E%3C/svg%3E"
width="800"
height="600"
alt="懒加载图片"
/>
/* 懒加载图片的加载过渡动画 */
img[data-src] {
opacity: 0.5;
filter: blur(5px);
transition: opacity 0.3s, filter 0.3s;
}

img.loaded {
opacity: 1;
filter: blur(0);
}

loading="lazy" vs Intersection Observer

特性loading="lazy"Intersection Observer
使用复杂度极简(一个属性)需要 JS 实现
浏览器支持96%+97%+
自定义阈值不支持(浏览器内部决定)支持自定义 rootMargin
加载动画不支持支持自定义动画
动态内容自动处理需要手动观察新元素
精细控制完全可控
推荐场景大多数常规场景需要动画/精细控制

实战优化清单

以下是一份完整的图片优化 Checklist,按优先级排序:

优先级优化项预期效果实施难度
⭐⭐⭐使用 WebP/AVIF 格式 + <picture> 降级体积减少 30-50%
⭐⭐⭐根据显示尺寸提供合适大小的图片(srcset)避免加载过大图片
⭐⭐⭐首屏图片 preload,非首屏图片 loading="lazy"改善 LCP + 减少首屏请求
⭐⭐⭐构建时自动压缩(Webpack/Vite 插件)体积减少 40-70%
⭐⭐CDN 图片处理(实时裁剪/缩放/格式转换)按需加载,减少传输量
⭐⭐设置图片尺寸(width/height),防止 CLS改善 CLS 指标
⭐⭐小图标使用 SVG 或 icon font 替代 PNG减少请求数,缩放不失真
⭐⭐小图(< 8KB)内联为 Base64减少 HTTP 请求数
使用渐进式 JPEG改善加载体验感知
内容协商:服务端根据 Accept 头返回最优格式自动适配
根据网络状况自适应加载弱网体验优化
用户上传前端压缩减少上传时间和存储成本
关于 Base64 内联

小于 8KB 的图片可以转换为 Base64 内联到 CSS/HTML 中,减少 HTTP 请求。但 Base64 编码会使体积增大约 33%,且无法利用浏览器缓存。因此只适合小图标,大图绝对不要内联。Webpack 和 Vite 都提供了自动阈值配置:

// Webpack 5
{ type: 'asset', parser: { dataUrlCondition: { maxSize: 8 * 1024 } } }

// Vite(默认 4KB,可调整)
{ build: { assetsInlineLimit: 8 * 1024 } }

面试高频问答

Q1:前端有哪些常见的图片优化手段?

:前端图片优化可以从以下几个维度入手:

  1. 格式优化:使用 WebP/AVIF 替代 JPEG/PNG,减少 30-50% 体积
  2. 压缩优化:构建时自动压缩(Webpack/Vite 插件)+ 用户上传前端 Canvas 压缩
  3. 尺寸优化:通过 srcset + sizes 提供响应式图片,避免加载过大尺寸
  4. 加载优化:首屏图片 preload,非首屏图片 loading="lazy" 懒加载
  5. CDN 优化:利用 CDN 的图片处理能力(实时裁剪、缩放、格式转换)
  6. 请求优化:小图内联 Base64,图标用 SVG/icon font,雪碧图合并
  7. 缓存优化:合理设置 HTTP 缓存头,文件名加 hash 实现长效缓存

Q2:JPEG、PNG、WebP、AVIF 各有什么特点?如何选择?

格式压缩透明动画特点适用场景
JPEG有损压缩率高,适合色彩丰富的照片照片、Banner
PNG无损质量高但体积大图标、Logo、需要透明的图
WebP有损/无损比 JPEG 小 25-35%,支持透明和动画通用替代(97%+ 兼容)
AVIF有损/无损压缩率最高,但编码慢高压缩需求(92%+ 兼容)

选择策略:矢量图用 SVG → 照片类优先 AVIF/WebP(<picture> 降级 JPEG)→ 需要透明用 WebP(降级 PNG)→ 简单动画用 WebP(复杂用视频)。

Q3:有损压缩和无损压缩的区别?

  • 无损压缩:通过找出数据中的重复模式来减小体积(如 LZ77、霍夫曼编码),解压后可以 100% 还原原始数据。典型格式:PNG、GIF。压缩率通常在 2:1 ~ 5:1。
  • 有损压缩:选择性丢弃人眼不敏感的信息来换取更高压缩率。以 JPEG 为例,通过色彩空间转换(RGB→YCbCr)、色度下采样、DCT 变换、量化等步骤,压缩率可达 10:1 ~ 50:1,但解压后无法还原原始数据。

在实际使用中,照片类图片用有损压缩(质量 75-85 肉眼基本无差别),图标/Logo/截图等需要精确还原的场景用无损压缩。

Q4:如何用 Canvas 实现前端图片压缩?原理是什么?

原理:利用 Canvas 的 drawImage() 将图片绘制到画布上(可同时缩放尺寸),然后使用 canvas.toBlob()canvas.toDataURL() 以指定的 MIME 类型和质量参数导出,从而实现压缩。

核心步骤

  1. 使用 FileReader 将用户上传的 File 读取为 DataURL
  2. 创建 Image 对象加载图片
  3. 创建 Canvas,按目标尺寸绘制图片
  4. 调用 canvas.toBlob(callback, 'image/jpeg', quality) 导出压缩后的 Blob

注意事项:质量参数 quality(0-1)仅对 JPEG 和 WebP 有效;Canvas 会丢失 EXIF 信息;超大图可能导致内存溢出,移动端需要注意限制最大尺寸。

Q5:什么是响应式图片?srcset 和 <picture> 的区别?

响应式图片是指根据用户设备的屏幕尺寸、像素密度、网络状况等条件,自动选择最合适的图片资源加载。

srcset + sizes

  • 用于同一张图片的不同尺寸版本
  • 浏览器根据 sizes 描述的显示尺寸和设备像素比自动选择最优版本
  • 适用于「同一内容,不同分辨率」的场景

<picture> 元素

  • 支持格式降级:按 <source>type 属性依次匹配浏览器支持的格式
  • 支持艺术指导:按 <source>media 属性针对不同屏幕使用不同裁剪
  • 适用于「不同格式降级」或「不同屏幕需要不同构图」的场景

简单来说:srcset 让浏览器自动选尺寸,<picture> 让开发者精确控制格式和裁剪。

Q6:图片懒加载的实现原理?loading="lazy" 和 Intersection Observer 的区别?

原理:默认不加载视口外的图片(src 设为占位图或空),当用户滚动使图片接近可视区域时,再将真实 URL 设置到 src 上触发加载。

loading="lazy"

  • 浏览器原生实现,使用极简(加一个属性即可)
  • 浏览器内部决定加载时机,无法自定义阈值
  • 不支持加载动画和精细控制
  • 支持率 96%+

Intersection Observer

  • 需要 JavaScript 代码实现
  • 可以自定义 rootMargin(提前加载距离)和 threshold(触发比例)
  • 可以配合 CSS 实现加载过渡动画
  • 适合需要精细控制或动态内容的场景

推荐策略:大多数场景直接使用 loading="lazy" 即可;需要加载动画或精细控制时用 Intersection Observer。首屏 LCP 图片一定不要加 loading="lazy"

Q7:构建工具中如何自动压缩图片?

  • Webpack:使用 image-minimizer-webpack-plugin,底层可选 sharp 或 squoosh。通过 minimizer 配置压缩参数(JPEG quality、PNG compressionLevel 等),通过 generator 配置自动生成 WebP/AVIF 版本。
  • Vite:使用 vite-plugin-imagemin,集成了 mozjpeg、optipng、svgo、pngquant 等多个优化库,配置简单直观。

两者都建议:开启自动 WebP 生成,小于 8KB 的图片内联 Base64(Webpack 用 asset 模块,Vite 用 assetsInlineLimit),配合 <picture> 元素实现格式降级。

Q8:CDN 图片优化有哪些策略?

  1. 实时处理:通过 URL 参数实现裁剪、缩放、格式转换(如 ?w=800&format=webp
  2. 内容协商:CDN 根据请求头 Accept 自动返回浏览器支持的最优格式(AVIF > WebP > JPEG)
  3. 就近访问:CDN 节点缓存图片,用户从最近的节点获取资源,减少延迟
  4. 缓存策略:图片 URL 加 hash,设置长效缓存(Cache-Control: max-age=31536000, immutable
  5. 智能压缩:部分 CDN 支持自动质量调节,在不影响视觉的前提下进一步压缩

关键注意点:使用内容协商时必须设置 Vary: Accept 响应头,确保 CDN 按格式分别缓存。

Q9:Base64 内联图片的优缺点?什么场景适合使用?

优点

  • 减少 HTTP 请求数(图片直接嵌入 HTML/CSS 中)
  • 不受跨域限制
  • 避免图片加载的额外 RTT

缺点

  • Base64 编码后体积增大约 33%(3 字节数据编码为 4 字符)
  • 无法利用浏览器的独立缓存(每次加载 HTML/CSS 都要重新下载)
  • 增大 HTML/CSS 文件体积,阻塞解析
  • 无法利用 CDN 的图片处理能力

适用场景:小于 8KB 的小图标、装饰图、Loading 动画等。大图(> 10KB)绝对不要内联,会适得其反。构建工具(Webpack/Vite)可以自动判断阈值,小图自动转 Base64。

Q10:如何衡量图片优化的效果?有哪些关键指标?

  1. LCP(Largest Contentful Paint):最大内容绘制时间,直接反映首屏最大图片的加载速度。目标 < 2.5s。
  2. CLS(Cumulative Layout Shift):累积布局偏移,未设置宽高的图片加载后会导致页面跳动。目标 < 0.1。
  3. Speed Index:页面视觉完成度的加载速度,图片优化直接影响此指标。
  4. Total Page Weight:页面总大小,优化后图片部分应显著减小。
  5. 图片传输大小:单张图片的实际传输大小,可在 DevTools Network 面板查看。
  6. 请求数量:图片请求总数,通过懒加载和内联可以减少首屏请求数。

测量工具

  • Chrome DevTools → Network 面板 / Performance 面板
  • Lighthouse 审计(Performance 得分)
  • WebPageTest(详细的图片加载瀑布图)
  • web-vitals 库实时采集 LCP/CLS 数据上报到监控平台