前言
图片资源是 Web 应用中最大的性能负担。据统计,图片通常占据网页总带宽的 70% 以上 ,直接影响着 LCP(最大内容绘制)、FCP(首次内容绘制)等核心性能指标。一个未经优化的图片策略,可能导致首屏加载时间增加数秒。
Vite 作为现代前端构建工具,提供了强大的图片优化能力。本文将深入探讨从压缩算法原理到实际配置的全链路优化方案,并手写一个简单的图片压缩插件。
为什么图片是性能的头号杀手?
一个真实的问题
// 假设我们的页面有这些图片
const images = [
{ name: 'hero-banner.jpg', size: '850KB' }, // 首屏大图
{ name: 'product-1.jpg', size: '320KB' }, // 商品图
{ name: 'product-2.jpg', size: '310KB' }, // 商品图
{ name: 'product-3.jpg', size: '315KB' }, // 商品图
{ name: 'icon-home.png', size: '45KB' }, // 图标
{ name: 'icon-user.png', size: '42KB' }, // 图标
{ name: 'logo.png', size: '120KB' } // Logo
]
上述图片有 2MB,需要几秒才能加载完;而通常情况下,我们的用户会在 2 秒内就直接关掉页面!
图片优化的核心目标
目标1:减小体积
- 压缩:丢掉人眼看不到的细节
- 转换:用更高效的格式(WebP、AVIF)
- 结果:体积减少 30-70%
目标2:减少请求
- 雪碧图:把多个小图标合并成一个
- 内联:小图片变成代码,不发起请求
- 结果:请求数从 N 降到 1
目标3:加速加载
- 懒加载:只加载用户看得到的
- 预加载:提前加载即将看到的
- 结果:首屏加载快,后面加载不影响
图片压缩 - 让每张图都"瘦身"
图片压缩工具的核心区别在于有损与无损、压缩算法以及实现语言。
压缩算法对比
| 压缩类型 | 原理 | 适用场景 | 代表工具 |
|---|---|---|---|
| 无损压缩 | 优化编码结构,移除元数据,像素完全不变 | Logo、图标、法律文档 | jpegtran, SVGO |
| 有损压缩 | 丢弃人眼不易察觉的颜色信息 | 照片、UI 背景图 | pngquant, WebP |
Sharp 与 Imagemin 的压缩工具对比
Sharp:
- 基于 libvips,使用 C 语言编写,Node.js 绑定
- 优势:处理速度极快(比 ImageMagick 快 4-5 倍),内存占用低
- 适用:实时处理、批量转换、现代格式(AVIF/WebP)生成
Imagemin:
- 基于独立的二进制工具(mozjpeg、pngquant、SVGO)
- 优势:插件生态丰富,配置精细,社区成熟
- 适用:构建时优化,精细控制每种格式的压缩参数
1.3 有损 vs 无损的实战选择
// PNG 有损压缩(pngquant)- 适合 UI 图标
imageminPngquant({
quality: [0.6, 0.8], // 60-80% 质量,人眼几乎无感知
speed: 4 // 平衡压缩率与速度
})
// JPEG 无损压缩(jpegtran)- 适合需要绝对精度的图片
imageminJpegtran({
progressive: true, // 渐进式 JPEG,提升用户体验
arithmetic: false // 使用霍夫曼编码
})
// SVG 优化(SVGO)- 纯文本优化,移除冗余
imageminSvgo({
plugins: [
{ name: 'removeViewBox', active: false }, // 保留 viewBox 以便缩放
{ name: 'cleanupIDs', active: true } // 移除冗余 ID
]
})
实践建议 :
- 对用户上传内容(如头像)使用有损压缩,设置 quality: 75-85
- 对品牌 Logo 优先尝试无损方案,再根据体积决定是否启用有损
- SVG 是文本格式,通过移除注释、空格和元数据可减少 30-50% 体积
格式转换 - 用更快的格式
为什么需要 WebP/AVIF?
| 格式 | 相比 JPEG 体积减少 | 浏览器支持 |
|---|---|---|
| WebP | 30% | 95%+(Chrome/Firefox/Edge 全支持,Safari 14+) |
| AVIF | 50% | 90%+(基于 AV1 编码,压缩率更高) |
使用 vite-plugin-imagemin 实现自动转换
// vite.config.js
import viteImagemin from 'vite-plugin-imagemin'
export default {
plugins: [
viteImagemin({
// 有损压缩配置
gifsicle: {
optimizationLevel: 3, // 1-3,级别越高压缩越强
colors: 64 // 减少颜色数以压缩体积
},
// PNG 有损压缩
pngquant: {
quality: [0.65, 0.8], // 最小/最大质量范围
speed: 4 // 1(慢)-11(快)
},
// JPEG 无损优化
jpegtran: {
progressive: true // 生成渐进式 JPEG
},
// SVG 优化
svgo: {
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'cleanupIDs', active: true }
]
},
// WebP 转换
webp: {
quality: 80, // 有损质量 (0-100)
lossless: false // 是否无损模式
}
})
]
}
自动生成 WebP 和 AVIF
// vite.config.js
import viteImagemin from 'vite-plugin-imagemin'
export default {
plugins: [
viteImagemin({
// 生成 WebP 版本
webp: {
quality: 80,
lossless: false
},
// 生成 AVIF 版本(更高压缩率)
avif: {
quality: 60,
lossless: false
}
})
]
}
自动降级策略:<picture> 元素
现代浏览器可以通过 <picture> 元素支持内容协商,实现平滑降级:
<picture>
<!-- 浏览器按顺序匹配第一个支持的格式 -->
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="fallback" loading="lazy">
</picture>
工作原理 :
- 浏览器检查 Accept 请求头
- 依次尝试 AVIF → WebP → JPEG
- 只下载第一个支持的格式
Nginx/CDN 层面的内容协商
# nginx.conf
map $http_accept $img_format {
default jpg;
~*avif avif;
~*webp webp;
}
server {
location ~* ^/images/(.*)$ {
add_header Vary "Accept"; # 告知 CDN 根据 Accept 头缓存不同版本
try_files /images/$1.$img_format /images/$1.jpg =404;
}
}
雪碧图 - 减少请求数
为什么需要雪碧图?
对于大量的小图标(尤其是 SVG)会导致过多的 HTTP 请求。浏览器对并发请求有限制(通常 6-8 个),过多的请求会造成加载排队 。
假设页面有 100 个 SVG 图标,将会产生 100 个 HTTP 请求 。
雪碧图的工作原理
graph LR
A[多个SVG图标] --> B[合并为单个SVG文件]
B --> C[每个图标作为 symbol]
C --> D[通过 use 引用]
vite-plugin-sprites 配置示例
// vite.config.js
import { createSpritesPlugin } from 'vite-plugin-sprites'
export default {
plugins: [
createSpritesPlugin({
// 图标目录
input: 'src/assets/icons',
// 输出配置
output: {
filename: 'sprite.[hash].svg', // 添加哈希用于缓存
prefix: 'icon-', // symbol ID 前缀
css: true // 同时生成 CSS 文件
},
// 优化选项
svgo: true, // 对合并后的 SVG 进行优化
padding: 2 // 图标间距
})
]
}
雪碧图的使用方式
<!-- 生成的雪碧图包含所有 symbol -->
<svg style="display: none;">
<symbol id="icon-home" viewBox="0 0 24 24">
<!-- 图标路径 -->
</symbol>
<symbol id="icon-user" viewBox="0 0 24 24">
<!-- 图标路径 -->
</symbol>
</svg>
<!-- 使用时只需一个 HTTP 请求 -->
<svg class="icon">
<use xlink:href="#icon-home"></use>
</svg>
优点 :
- 只会发起 1 次 HTTP 请求(对比之前的 100 次)
- SVG 作为矢量图形,不会受文字抗锯齿算法影响
- 可通过 CSS 控制颜色和大小
内联阈值 - 小图片变代码
什么是内联?
原始图片:logo.png (3KB)
↓
转换成 Base64:data:image/png;base64,iVBORw0KGgo...
↓
嵌入到 JS/CSS 中
↓
不发 HTTP 请求,直接显示
Vite 的默认策略
Vite 内置了智能的资源内联机制 :
- 体积 < 4KB:转换为 Base64 内联,避免额外 HTTP 请求
- 体积 ≥ 4KB:提取为单独文件,通过哈希命名
为什么是 4KB?
- 4KB 是 HTTP/1.1 的典型阈值
- 小于 4KB 的文件,内联比发请求更快
内联阈值的配置
// vite.config.js
export default {
build: {
// 设置内联阈值 (单位:字节)
assetsInlineLimit: 8 * 1024, // 8KB
// 静态资源输出目录
assetsDir: 'static',
rollupOptions: {
output: {
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
}
}
阈值怎么选?
| 阈值 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 4KB(默认) | 避免过多内联导致 JS/CSS 膨胀 | 小文件仍需请求 | 通用项目 |
| 8KB | 减少更多请求数 | 增加 JS/CSS 体积 | 移动端、HTTP/1.1 场景 |
| 0(禁用) | 所有资源单独文件 | 请求数爆炸 | HTTP/2 多路复用场景 |
重要注意事项
1. 库模式下的特殊行为
如果指定了 build.lib,内联阈值将被忽略;此时无论文件大小,资源都会被内联!
2. SVG 的特殊处理
- Vite 不支持 SVG 转 Base64(设计如此)
- 原因:Base64 会使 SVG 体积增大 33-36%,直接使用 UTF-8 Data URL 更优
- 替代方案:
import iconSvg from './icon.svg?raw'; // 获取纯文本
const toSVGDataUrl = (str) => {
// 推荐使用 UTF-8 编码,而非 Base64
return `data:image/svg+xml;utf8,${encodeURIComponent(str)}`;
};
缓存策略 - 让图片只下载一次
文件名哈希的原理
// 构建前的文件名
logo.png
banner.jpg
// 构建后的文件
logo.abc123.png // 哈希基于内容生成
logo.def456.png // 图片内容变化,哈希变化
优势 :
- 设置长期缓存:
Cache-Control: max-age=31536000, immutable - 内容变化时,URL 自动变化,强制客户端更新
- 当浏览器访问时:
- 这个 URL 我没见过 → 重新下载
- 这个 URL 我见过 → 直接用缓存
Vite 的哈希配置
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
// 静态资源命名规则
assetFileNames: (assetInfo) => {
// 根据文件类型和内容生成哈希
const ext = assetInfo.name.split('.').pop()
return `assets/${ext}/[name]-[hash].[ext]`
}
}
},
// 生成 manifest.json,方便服务器端映射
manifest: true
}
}
CDN 缓存刷新策略
// 构建时生成的 manifest 文件
{
"src/assets/logo.png": "assets/png/logo-abc123.png",
"src/assets/banner.jpg": "assets/jpg/banner-def456.jpg"
}
// 在代码中动态获取
const getAssetUrl = (src) => {
return manifest[src] || src
}
Nginx 缓存配置
# nginx.conf
location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|avif)$ {
# 长期缓存
expires 1y;
# 告诉浏览器可以永久缓存
add_header Cache-Control "public, immutable";
# 如果有版本参数,去掉
if ($args ~* "^v=") {
rewrite ^(.*)$ $1? permanent;
}
}
手写一个简单的 Vite 图片压缩插件
插件模板与钩子
Vite 插件的基本结构 :
// vite-plugin-image-optimizer.ts
import type { PluginOption } from 'vite'
import fs from 'fs/promises'
import path from 'path'
import imagemin from 'imagemin'
import imageminPngquant from 'imagemin-pngquant'
import imageminJpegtran from 'imagemin-jpegtran'
import imageminSvgo from 'imagemin-svgo'
interface PluginOptions {
pngQuality?: [number, number]
jpegQuality?: number
svgOptimize?: boolean
include?: RegExp
exclude?: RegExp
}
export default function viteImageOptimizer(
options: PluginOptions = {}
): PluginOption {
const {
pngQuality = [0.6, 0.8],
jpegQuality = 80,
svgOptimize = true,
include = /\.(png|jpe?g|svg)$/,
exclude = /node_modules/
} = options
return {
name: 'vite-plugin-image-optimizer',
// 在构建结束时执行优化
async closeBundle() {
console.log('🖼️ 开始优化图片资源...')
// 需要优化的文件目录
const buildDir = path.resolve('dist/assets')
try {
await fs.access(buildDir)
} catch {
console.log('没有找到图片资源')
return
}
const files = await imagemin([`${buildDir}/**/*`], {
destination: buildDir,
plugins: [
// PNG 优化
imageminPngquant({
quality: pngQuality,
speed: 4
}),
// JPEG 优化
imageminJpegtran({
progressive: true
}),
// SVG 优化
...(svgOptimize ? [imageminSvgo({
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'cleanupIDs', active: true }
]
})] : [])
]
})
console.log(`✅ 已优化 ${files.length} 个图片文件`)
// 输出压缩统计
let totalSaved = 0
for (const file of files) {
const originalPath = file.history[0]
const originalSize = (await fs.stat(originalPath)).size
const saved = originalSize - file.data.length
totalSaved += saved
if (saved > 0) {
console.log(` ${path.basename(originalPath)}: 减少 ${(saved / 1024).toFixed(2)}KB (${((saved / originalSize) * 100).toFixed(1)}%)`)
}
}
console.log(`📊 总计节省: ${(totalSaved / 1024 / 1024).toFixed(2)}MB`)
}
}
}
插件配置与使用
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteImageOptimizer from './vite-plugin-image-optimizer'
export default defineConfig({
plugins: [
vue(),
viteImageOptimizer({
pngQuality: [0.65, 0.8], // PNG 压缩质量范围
jpegQuality: 75, // JPEG 压缩质量
svgOptimize: true, // 是否优化 SVG
include: /\.(png|jpe?g|svg)$/,
exclude: /node_modules|already-optimized/
})
],
build: {
assetsInlineLimit: 8 * 1024, // 8KB 以下内联
rollupOptions: {
output: {
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
}
}
})
进阶功能:生成 WebP 副本
// 扩展插件,添加 WebP 生成功能
import imageminWebp from 'imagemin-webp'
// 在 closeBundle 中添加
async function generateWebpCopies() {
const files = await imagemin([`${buildDir}/**/*.{png,jpg,jpeg}`], {
destination: buildDir,
plugins: [
imageminWebp({
quality: 80,
lossless: false
})
]
})
console.log(`✅ 已生成 ${files.length} 个 WebP 副本`)
}
最佳实践清单
配置清单
- 安装 vite-plugin-imagemin 并配置压缩参数
- 配置 build.assetsInlineLimit 为 8KB
- 使用
<picture>实现 WebP/AVIF 降级 - 小图标合并成雪碧图
- 配置文件名哈希(Vite 默认已做)
- 配置 CDN 长期缓存
优化策略矩阵
| 优化维度 | 推荐配置 | 收益 |
|---|---|---|
| 压缩算法 | PNG: pngquant quality 65-80 JPEG: jpegtran progressive SVG: SVGO | 减少 30-70% 体积 |
| 格式转换 | 同时生成 WebP + AVIF | WebP 比 JPEG 小 30% |
| 雪碧图 | 合并小图标,使用 <symbol> | 请求数从 N 降到 1 |
| 内联阈值 | 8KB(HTTP/2 可适当提高) | 减少 20-30% 请求 |
| 缓存策略 | 哈希命名 + max-age=1y | 缓存命中率 90%+ |
快速落地清单
- 批量转换存量图片为 WebP/AVIF,保留原格式作为回退
- 配置 build.assetsInlineLimit 为 8KB(根据项目特点调整)
- 集成图片压缩插件,自动优化新增图片
- 合并小图标为雪碧图,减少请求数
- 配置 CDN 缓存:max-age=31536000 + immutable
- 通过 Lighthouse 审计,设置性能预算告警
结语
图片优化是投入产出比最高的性能优化手段。一个配置得当的 Vite 构建流程,可以在完全不改变开发体验的前提下,让图片加载耗时减少 40-60%,首屏加载速度提升 30% 以上 。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!