图片压缩与优化全指南
为什么图片优化至关重要?
图片是前端页面中体积最大的资源类型。根据 HTTP Archive 的统计数据,图片通常占据页面总传输量的 60% 以上。一张未经优化的高清图片可能达到数 MB,而一个完整页面的 HTML + CSS + JS 加起来可能只有几百 KB。
来看一组优化前后的对比数据:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 页面总大小 | 5.2 MB | 1.8 MB | 65%↓ |
| 图片总大小 | 4.0 MB | 0.9 MB | 77%↓ |
| 首屏加载时间 | 6.5s | 2.1s | 68%↓ |
| LCP | 4.8s | 1.9s | 60%↓ |
| 每月流量成本 | ¥5000 | ¥1500 | 70%↓ |
图片优化是投入产出比最高的性能优化手段 —— 不需要改动业务代码,仅通过图片格式、压缩、加载策略的优化就能大幅提升页面性能。
图片加载对关键性能指标有直接影响:
图片格式全解析
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)
截至 2025 年,WebP 的全球浏览器支持率已超过 97%,可以放心在生产环境中使用。仅 IE 不支持,但 IE 已经退役。
AVIF — 下一代格式
AVIF(AV1 Image File Format)基于 AV1 视频编码,是目前压缩率最高的图片格式。
特点:
- 压缩率比 WebP 再高 20-50%
- 支持 HDR(高动态范围)
- 支持透明度和动画
- 编码速度较慢
- 兼容性:Chrome 85+、Firefox 93+、Safari 16.4+
AVIF 的编码速度明显慢于 WebP 和 JPEG,不适合实时压缩场景(如用户上传后即时处理)。建议在构建时预压缩或使用 CDN 离线转码。
SVG — 矢量图的天下
SVG(Scalable Vector Graphics)是基于 XML 的矢量图格式,任意缩放不失真。
特点:
- 矢量图形,无限缩放不失真
- 本质是文本(XML),可以用 CSS/JS 操控
- 适合图标、Logo、简单插图
- 复杂图形体积可能很大
- 可以内联到 HTML 中
- 支持动画
格式对比总表
| 特性 | JPEG | PNG | GIF | WebP | AVIF | SVG |
|---|---|---|---|---|---|---|
| 压缩方式 | 有损 | 无损 | 无损 | 有损/无损 | 有损/无损 | 无损 |
| 透明度 | ❌ | ✅ | ⚠️ 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 为例:
- 色彩空间转换:RGB → YCbCr(亮度 + 色度),人眼对亮度更敏感,对色度分辨率要求低
- 色度下采样:将色度通道的分辨率降低(如 4:2:0,色度分辨率减半)
- DCT 变换:将 8×8 像素块做离散余弦变换,将空间域转换为频率域
- 量化:对高频分量(细节)进行更大幅度的量化舍入(这是信息丢失的主要步骤)
- 熵编码:使用霍夫曼编码或算术编码进一步压缩
两种方式的核心区别:
| 特性 | 无损压缩 | 有损压缩 |
|---|---|---|
| 数据完整性 | 可 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 压缩有以下局限性:
- 仅支持有损压缩:
toBlob的quality参数只对image/jpeg和image/webp有效,PNG 是无损的 - EXIF 信息丢失:Canvas 重绘后会丢失原图的拍摄参数、方向等 EXIF 元数据
- 色彩偏差:Canvas 使用 premultiplied alpha,某些半透明图片可能出现色差
- 大图内存问题:超大图片可能导致 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 已被 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-minimizer | Vite + viteImagemin |
|---|---|---|
| 底层库 | sharp / squoosh | mozjpeg / optipng / svgo |
| WebP 生成 | 支持(generator) | 支持 |
| AVIF 生成 | 支持 | 需额外插件 |
| 压缩速度 | sharp 很快,squoosh 较慢 | 中等 |
| 配置复杂度 | 中等 | 简单 |
| 小图 Base64 | asset 模块内置支持 | 内置支持 |
- 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: 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>
- 必须设置
width和height:浏览器需要在图片加载前知道尺寸来预留空间,避免布局偏移(CLS) - 首屏图片不要懒加载:首屏的 LCP 图片使用
loading="eager"(默认值),加lazy反而会影响 LCP 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 头返回最优格式 | 自动适配 | 高 |
| ⭐ | 根据网络状况自适应加载 | 弱网体验优化 | 中 |
| ⭐ | 用户上传前端压缩 | 减少上传时间和存储成本 | 中 |
小于 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:前端有哪些常见的图片优化手段?
答:前端图片优化可以从以下几个维度入手:
- 格式优化:使用 WebP/AVIF 替代 JPEG/PNG,减少 30-50% 体积
- 压缩优化:构建时自动压缩(Webpack/Vite 插件)+ 用户上传前端 Canvas 压缩
- 尺寸优化:通过
srcset+sizes提供响应式图片,避免加载过大尺寸 - 加载优化:首屏图片 preload,非首屏图片
loading="lazy"懒加载 - CDN 优化:利用 CDN 的图片处理能力(实时裁剪、缩放、格式转换)
- 请求优化:小图内联 Base64,图标用 SVG/icon font,雪碧图合并
- 缓存优化:合理设置 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 类型和质量参数导出,从而实现压缩。
核心步骤:
- 使用
FileReader将用户上传的 File 读取为 DataURL - 创建 Image 对象加载图片
- 创建 Canvas,按目标尺寸绘制图片
- 调用
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 图片优化有哪些策略?
答:
- 实时处理:通过 URL 参数实现裁剪、缩放、格式转换(如
?w=800&format=webp) - 内容协商:CDN 根据请求头
Accept自动返回浏览器支持的最优格式(AVIF > WebP > JPEG) - 就近访问:CDN 节点缓存图片,用户从最近的节点获取资源,减少延迟
- 缓存策略:图片 URL 加 hash,设置长效缓存(
Cache-Control: max-age=31536000, immutable) - 智能压缩:部分 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:如何衡量图片优化的效果?有哪些关键指标?
答:
- LCP(Largest Contentful Paint):最大内容绘制时间,直接反映首屏最大图片的加载速度。目标 < 2.5s。
- CLS(Cumulative Layout Shift):累积布局偏移,未设置宽高的图片加载后会导致页面跳动。目标 < 0.1。
- Speed Index:页面视觉完成度的加载速度,图片优化直接影响此指标。
- Total Page Weight:页面总大小,优化后图片部分应显著减小。
- 图片传输大小:单张图片的实际传输大小,可在 DevTools Network 面板查看。
- 请求数量:图片请求总数,通过懒加载和内联可以减少首屏请求数。
测量工具:
- Chrome DevTools → Network 面板 / Performance 面板
- Lighthouse 审计(Performance 得分)
- WebPageTest(详细的图片加载瀑布图)
web-vitals库实时采集 LCP/CLS 数据上报到监控平台