生产环境极致优化:拆包、图片压缩、Gzip/Brotli 完全指南
作者:互联网
2026-03-23
前言
当我们的应用从开发环境走向生产环境,真正的挑战才刚刚开始。用户不会关心我们的代码写得多么优雅,他们只关心页面加载快不快、交互流不流畅。一个未经优化的生产构建,可能让我们的用户在第一秒就流失。
为什么要优化生产构建?
一个真实的反面教材
我们先来看一个系统打包后的产物:
dist/
├── index.html 5KB
├── assets/index.abc123.js 2.8MB ← 一个文件包含了所有代码
├── assets/vendor.def456.js 1.2MB ← 第三方库
├── assets/style.ghi789.css 180KB
└── images/
├── logo.png 120KB ← 未压缩
├── banner.jpg 850KB ← 巨大
└── ...
当用户访问这个系统时:
- 下载 2.8MB + 1.2MB + 180KB + 970KB = 约 5MB
- 4G 网络下需要 2 秒;3G 网络会更慢
- 用户早跑了
构建优化的核心目标
| 优化维度 | 目标 | 收益 |
|---|---|---|
| 拆包优化 | 分离业务代码和第三方库 | 利用浏览器缓存,二次访问提速 |
| 图片压缩 | 减少图片体积 | 平均减少 60-80% 体积 |
| Gzip/Brotli | 压缩文本资源 | 减少 70-90% 传输体积 |
| 长期缓存 | 文件名哈希,内容变化才更新 | 最大化缓存利用率 |
优化能带来什么?
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏 JS 体积 | 4.2 MB | 2.1 MB | 50% |
| 图片总体积 | 2.8 MB | 0.6 MB | 78% |
| 传输体积(Gzip后) | 3.2 MB | 0.8 MB | 75% |
| 首次加载时间 | 3.2 秒 | 1.1 秒 | 65% |
| 二次加载时间 | 2.1 秒 | 0.3 秒 | 85% |
先诊断,后开药 - 构建分析工具
为什么要先分析?
就像医生看病要先做检查一样,优化构建也要先找到问题在哪。在主观上,我们可能会觉得是不是某个依赖太大了?但实际上可能是另一个我们没想到的库!
使用 rollup-plugin-visualizer 分析
安装
npm install --save-dev rollup-plugin-visualizer
配置
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport { visualizer } https://images.jiaoben.netfrom https://images.jiaoben.net'rollup-plugin-visualizer'
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.netvisualizer({
https://images.jiaoben.netfilename: https://images.jiaoben.net'dist/stats.html', https://images.jiaoben.net// 输出文件
https://images.jiaoben.netopen: https://images.jiaoben.nettrue, https://images.jiaoben.net// 构建后自动打开
https://images.jiaoben.netgzipSize: https://images.jiaoben.nettrue, https://images.jiaoben.net// 显示 gzip 后大小
https://images.jiaoben.netbrotliSize: https://images.jiaoben.nettrue, https://images.jiaoben.net// 显示 brotli 后大小
https://images.jiaoben.nettemplate: https://images.jiaoben.net'treemap' https://images.jiaoben.net// 图表类型: treemap, sunburst, network
})
]
}
运行构建
npm run build
// 浏览器会自动打开一个酷炫的图表
// 一眼就能看出哪些文件最大
使用 vite-bundle-visualizer 分析
安装
npm install --save-dev vite-bundle-visualizer
运行分析
npx vite-bundle-visualizer
输出示例
┌───────────────────────┬─────────────┬──────────┬───────┐
│ Module │ Size │ Gzip │ Brotli│
├───────────────────────┼─────────────┼──────────┼───────┤
│ node_modules/ │ 2.3 MB │ 680 KB │ 520 KB│
│ vue/ │ 680 KB │ 210 KB │ 160 KB│
│ element-plus/ │ 890 KB │ 280 KB │ 210 KB│
│ echarts/ │ 520 KB │ 150 KB │ 115 KB│
│ lodash-es/ │ 210 KB │ 62 KB │ 48 KB │
│ src/ │ 1.8 MB │ 480 KB │ 360 KB│
└───────────────────────┴─────────────┴──────────┴───────┘
自定义分析脚本
https://images.jiaoben.net// scripts/analyze.js
https://images.jiaoben.netimport fs https://images.jiaoben.netfrom https://images.jiaoben.net'fs'
https://images.jiaoben.netimport path https://images.jiaoben.netfrom https://images.jiaoben.net'path'
https://images.jiaoben.netimport { gzipSizeSync } https://images.jiaoben.netfrom https://images.jiaoben.net'gzip-size'
https://images.jiaoben.netimport { brotliSizeSync } https://images.jiaoben.netfrom https://images.jiaoben.net'brotli-size'
https://images.jiaoben.netfunction https://images.jiaoben.netanalyzeDist(https://images.jiaoben.net) {
https://images.jiaoben.netconst distDir = path.https://images.jiaoben.netresolve(https://images.jiaoben.net'./dist/assets')
https://images.jiaoben.netconst files = fs.https://images.jiaoben.netreaddirSync(distDir)
https://images.jiaoben.netlet totalSize = https://images.jiaoben.net0
https://images.jiaoben.netlet totalGzip = https://images.jiaoben.net0
https://images.jiaoben.netlet totalBrotli = https://images.jiaoben.net0
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net' 构建产物分析n')
files
.https://images.jiaoben.netfilter(https://images.jiaoben.nethttps://images.jiaoben.netf => f.https://images.jiaoben.netendsWith(https://images.jiaoben.net'.js') || f.https://images.jiaoben.netendsWith(https://images.jiaoben.net'.css'))
.https://images.jiaoben.netforEach(https://images.jiaoben.nethttps://images.jiaoben.netfile => {
https://images.jiaoben.netconst filePath = path.https://images.jiaoben.netjoin(distDir, file)
https://images.jiaoben.netconst content = fs.https://images.jiaoben.netreadFileSync(filePath)
https://images.jiaoben.netconst size = content.https://images.jiaoben.netlength
https://images.jiaoben.netconst gzip = https://images.jiaoben.netgzipSizeSync(content)
https://images.jiaoben.netconst brotli = https://images.jiaoben.netbrotliSizeSync(content)
totalSize += size
totalGzip += gzip
totalBrotli += brotli
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`https://images.jiaoben.net${file}:`)
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net` Raw: https://images.jiaoben.net${(size / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} KB`)
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net` Gzip: https://images.jiaoben.net${(gzip / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} KB (https://images.jiaoben.net${(gzip/size*https://images.jiaoben.net100).toFixed(https://images.jiaoben.net0)}%)`)
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net` Brotli: https://images.jiaoben.net${(brotli / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} KB (https://images.jiaoben.net${(brotli/size*https://images.jiaoben.net100).toFixed(https://images.jiaoben.net0)}%)n`)
})
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net' 总计:')
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net` Raw: https://images.jiaoben.net${(totalSize / https://images.jiaoben.net1024 / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} MB`)
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net` Gzip: https://images.jiaoben.net${(totalGzip / https://images.jiaoben.net1024 / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} MB`)
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net` Brotli: https://images.jiaoben.net${(totalBrotli / https://images.jiaoben.net1024 / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} MB`)
}
https://images.jiaoben.netanalyzeDist()
看懂分析结果
分析结果能告诉我们什么?
1. 找出最大的依赖
- echarts: 520KB → 考虑按需加载
- monaco-editor: 2.8MB → 考虑动态导入
2. 找出重复的依赖
- lodash 和 lodash-es 同时存在? → 统一用 lodash-es
- moment 和 dayjs 同时存在? → 用 dayjs 替代 moment
3. 找出可以拆分的点
- node_modules 打包在一起太大了 → 拆成多个 chunk
- 所有页面代码都在一个文件里 → 按路由拆分
拆包策略 - 把大象放进冰箱
为什么要拆包?
用一个比喻来解释
不拆包:把所有东西都塞进一个行李箱
├─ 想拿牙刷 → 要翻遍整个箱子
├─ 箱子破了 → 所有东西都掉出来
└─ 箱子太大 → 搬不动
拆包:分成多个小包
├─ 洗漱包:牙刷、牙膏、毛巾
├─ 衣物包:衣服、裤子、袜子
├─ 电子包:充电器、数据线
├─ 哪个包破了 → 只损失那部分
└─ 每个包都很轻 → 好搬
技术层面的好处
不拆包:
├─ 修改一行代码 → 整个大文件缓存失效
└─ 用户每次更新都要重新下载所有代码
拆包后:
├─ 第三方库独立 → 几乎不变,长期缓存
├─ 业务代码拆分 → 只下载修改的部分
└─ 多个小文件可以并行下载
基础拆包配置
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netbuild: {
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.net// 最基本的拆包策略
https://images.jiaoben.netmanualChunks: {
https://images.jiaoben.net// 将 Vue 全家桶打包在一起
https://images.jiaoben.net'vendor-vue': [https://images.jiaoben.net'vue', https://images.jiaoben.net'vue-router', https://images.jiaoben.net'pinia', https://images.jiaoben.net'vuex'],
https://images.jiaoben.net// 将 UI 库打包在一起
https://images.jiaoben.net'vendor-ui': [https://images.jiaoben.net'element-plus', https://images.jiaoben.net'@element-plus/icons-vue', https://images.jiaoben.net'ant-design-vue'],
https://images.jiaoben.net// 将工具库打包在一起
https://images.jiaoben.net'vendor-utils': [https://images.jiaoben.net'lodash-es', https://images.jiaoben.net'dayjs', https://images.jiaoben.net'axios', https://images.jiaoben.net'date-fns'],
https://images.jiaoben.net// 将图表库打包在一起
https://images.jiaoben.net'vendor-charts': [https://images.jiaoben.net'echarts', https://images.jiaoben.net'd3', https://images.jiaoben.net'chart.js']
}
}
}
}
}
智能拆包:根据依赖关系自动拆分
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netbuild: {
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid: https://images.jiaoben.netstring) {
https://images.jiaoben.net// node_modules 中的依赖
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
https://images.jiaoben.net// 按包名拆分
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue' https://images.jiaoben.net// 所有 vue 相关
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'antd')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui' https://images.jiaoben.net// UI 库
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'd3')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-charts' https://images.jiaoben.net// 图表库
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'dayjs')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils' https://images.jiaoben.net// 工具库
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco-editor')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco' https://images.jiaoben.net// 编辑器单独打包
}
https://images.jiaoben.net// 其他依赖打包在一起
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other'
}
https://images.jiaoben.net// 业务代码按页面拆分
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/views/')) {
https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/views/([^/]+)/)
https://images.jiaoben.netif (match) {
https://images.jiaoben.netreturn https://images.jiaoben.net`page-https://images.jiaoben.net${match[https://images.jiaoben.net1]}` https://images.jiaoben.net// 按页面拆分
}
}
https://images.jiaoben.net// 公共组件按模块拆分
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/components/')) {
https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/components/([^/]+)/)
https://images.jiaoben.netif (match) {
https://images.jiaoben.netreturn https://images.jiaoben.net`components-https://images.jiaoben.net${match[https://images.jiaoben.net1]}`
}
}
}
}
}
}
}
高级拆包:基于大小的自动拆分
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netbuild: {
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid: https://images.jiaoben.netstring, { getModuleInfo }) {
https://images.jiaoben.net// 如果模块大于 500KB,单独拆包
https://images.jiaoben.netconst moduleInfo = https://images.jiaoben.netgetModuleInfo(id)
https://images.jiaoben.netif (moduleInfo && moduleInfo.https://images.jiaoben.netcode) {
https://images.jiaoben.netconst size = https://images.jiaoben.netBuffer.https://images.jiaoben.netbyteLength(moduleInfo.https://images.jiaoben.netcode, https://images.jiaoben.net'utf8')
https://images.jiaoben.netif (size > https://images.jiaoben.net500 * https://images.jiaoben.net1024) { https://images.jiaoben.net// 500KB
https://images.jiaoben.netconst name = id.https://images.jiaoben.netmatch(https://images.jiaoben.net/[^/]+.(js|ts|vue)$/)?.[https://images.jiaoben.net0]
https://images.jiaoben.netreturn https://images.jiaoben.net`large-https://images.jiaoben.net${name}` https://images.jiaoben.net// 大文件单独打包
}
}
https://images.jiaoben.net// 继续其他拆分逻辑
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'
}
}
}
}
}
}
异步 chunk 的命名优化
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netbuild: {
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.net// 异步 chunk 命名
https://images.jiaoben.netchunkFileNames: https://images.jiaoben.net'assets/chunks/[name]-[hash].js',
https://images.jiaoben.net// 入口文件命名
https://images.jiaoben.netentryFileNames: https://images.jiaoben.net'assets/[name]-[hash].js',
https://images.jiaoben.net// 资源文件命名
https://images.jiaoben.netassetFileNames: https://images.jiaoben.net'assets/[ext]/[name]-[hash].[ext]',
https://images.jiaoben.netmanualChunks: {
https://images.jiaoben.net// ... 拆包配置
}
}
}
}
}
https://images.jiaoben.net// 输出结果:
https://images.jiaoben.net// assets/index-abc123.js (入口)
https://images.jiaoben.net// assets/chunks/vendor-vue-def456.js (Vue 相关)
https://images.jiaoben.net// assets/chunks/page-dashboard-ghi789.js (页面)
https://images.jiaoben.net// assets/images/logo-jkl012.png (图片)
拆包后的效果
| 拆包方式 | 文件数量 | 缓存利用率 | 适用场景 |
|---|---|---|---|
| 不拆包 | 1个 | 极低 | 小项目 |
| 按依赖拆分 | 5-10个 | 高 | 中大型项目 |
| 按页面拆分 | 10-50个 | 较高 | 多页面应用 |
| 按大小拆分 | 可变 | 中等 | 有大文件的项目 |
图片压缩 - 看不见的优化
为什么图片是优化重点?
我们先来看一个典型的页面资源分布:
https://images.jiaoben.netconst pageResources = {
https://images.jiaoben.netjs: https://images.jiaoben.net'2.8MB (40%)',
https://images.jiaoben.netcss: https://images.jiaoben.net'180KB (3%)',
https://images.jiaoben.netimages: https://images.jiaoben.net'3.5MB (50%)', https://images.jiaoben.net// 图片占了一半!
https://images.jiaoben.netfonts: https://images.jiaoben.net'500KB (7%)'
}
在页面中,图片通常占页面总体积的 50-70%,因此优化图片是最容易见效的!
vite-plugin-image-optimizer 配置
安装
npm install --save-dev vite-plugin-image-optimizer
配置
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport { https://images.jiaoben.netViteImageOptimizer } https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-image-optimizer'
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.netViteImageOptimizer({
https://images.jiaoben.net// 配置文件类型和压缩参数
https://images.jiaoben.netpng: {
https://images.jiaoben.netquality: https://images.jiaoben.net80, https://images.jiaoben.net// PNG 质量 0-100
https://images.jiaoben.netcompressionLevel: https://images.jiaoben.net9, https://images.jiaoben.net// 压缩级别 0-9
},
https://images.jiaoben.netjpeg: {
https://images.jiaoben.netquality: https://images.jiaoben.net75, https://images.jiaoben.net// JPEG 质量
https://images.jiaoben.netprogressive: https://images.jiaoben.nettrue, https://images.jiaoben.net// 渐进式 JPEG
},
https://images.jiaoben.netjpg: {
https://images.jiaoben.netquality: https://images.jiaoben.net75,
},
https://images.jiaoben.netwebp: {
https://images.jiaoben.netquality: https://images.jiaoben.net75, https://images.jiaoben.net// WebP 质量
https://images.jiaoben.netlossless: https://images.jiaoben.netfalse, https://images.jiaoben.net// 是否无损
},
https://images.jiaoben.netavif: {
https://images.jiaoben.netquality: https://images.jiaoben.net60, https://images.jiaoben.net// AVIF 质量
https://images.jiaoben.netlossless: https://images.jiaoben.netfalse,
},
https://images.jiaoben.netsvg: {
https://images.jiaoben.net// SVG 优化选项
https://images.jiaoben.netplugins: [
{
https://images.jiaoben.netname: https://images.jiaoben.net'preset-default',
https://images.jiaoben.netparams: {
https://images.jiaoben.netoverrides: {
https://images.jiaoben.netremoveViewBox: https://images.jiaoben.netfalse, https://images.jiaoben.net// 保留 viewBox
https://images.jiaoben.netcleanupIds: https://images.jiaoben.netfalse, https://images.jiaoben.net// 保留 ID
},
},
},
],
},
https://images.jiaoben.nettiff: {
https://images.jiaoben.netquality: https://images.jiaoben.net70,
},
https://images.jiaoben.netgif: {
https://images.jiaoben.netoptimizationLevel: https://images.jiaoben.net3, https://images.jiaoben.net// 优化级别 1-3
},
})
]
}
不同图片类型的优化策略
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.netViteImageOptimizer({
https://images.jiaoben.net// 根据不同用途设置不同参数
https://images.jiaoben.net// 1. 图标类:需要清晰,适当压缩
https://images.jiaoben.net'src/assets/icons/**/*': {
https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net90 },
https://images.jiaoben.netsvg: { https://images.jiaoben.netplugins: [https://images.jiaoben.net'preset-default'] }
},
https://images.jiaoben.net// 2. 背景图:可以牺牲一些质量换取体积
https://images.jiaoben.net'src/assets/backgrounds/**/*': {
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net65 },
https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
},
https://images.jiaoben.net// 3. 产品图:平衡质量和体积
https://images.jiaoben.net'src/assets/products/**/*': {
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net80 },
https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net75 }
},
https://images.jiaoben.net// 4. 用户上传:保持较好质量
https://images.jiaoben.net'src/assets/uploads/**/*': {
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net85 },
https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net85 }
}
})
]
}
使用现代图片格式
配置
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.netViteImageOptimizer({
https://images.jiaoben.net// 生成 WebP 版本(浏览器支持更好)
https://images.jiaoben.netwebp: {
https://images.jiaoben.netquality: https://images.jiaoben.net75
},
https://images.jiaoben.net// 生成 AVIF 版本(压缩率更高)
https://images.jiaoben.netavif: {
https://images.jiaoben.netquality: https://images.jiaoben.net60
}
})
]
}
在组件中配合使用
https://images.jiaoben.nettemplate >
https://images.jiaoben.net
https://images.jiaoben.netpicture >
https://images.jiaoben.net
https://images.jiaoben.netsource https://images.jiaoben.netsrcset=https://images.jiaoben.net"/image.avif" https://images.jiaoben.nettype=https://images.jiaoben.net"image/avif">
https://images.jiaoben.net
https://images.jiaoben.netsource https://images.jiaoben.netsrcset=https://images.jiaoben.net"/image.webp" https://images.jiaoben.nettype=https://images.jiaoben.net"image/webp">
https://images.jiaoben.net
https://images.jiaoben.netimg https://images.jiaoben.netsrc=https://images.jiaoben.net"/image.jpg" https://images.jiaoben.netalt=https://images.jiaoben.net"图片" https://images.jiaoben.netloading=https://images.jiaoben.net"lazy">
https://images.jiaoben.netpicture>
https://images.jiaoben.nettemplate>
懒加载与图片优化结合
https://images.jiaoben.nettemplate >
https://images.jiaoben.netimg
https://images.jiaoben.netv-lazy=https://images.jiaoben.net"optimizedImageUrl"
https://images.jiaoben.net:data-srcset=https://images.jiaoben.net"`
${smallImage} 400w,
${mediumImage} 800w,
${largeImage} 1200w
`"
https://images.jiaoben.netsizes=https://images.jiaoben.net"(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
https://images.jiaoben.netloading=https://images.jiaoben.net"lazy"
https://images.jiaoben.net:alt=https://images.jiaoben.net"alt"
>
https://images.jiaoben.nettemplate>
https://images.jiaoben.netscript https://images.jiaoben.netsetup>https://images.jiaoben.net
https://images.jiaoben.netimport { computed } https://images.jiaoben.netfrom https://images.jiaoben.net'vue'
https://images.jiaoben.netconst props = defineProps<{
https://images.jiaoben.netimagePath: string,
alt?: string
}>()
https://images.jiaoben.net// 根据视图宽度选择合适大小的图片
https://images.jiaoben.netconst optimizedImageUrl = https://images.jiaoben.netcomputed(https://images.jiaoben.net() => {
https://images.jiaoben.net// 假设构建时生成了不同尺寸的图片
https://images.jiaoben.net// logo-small.jpg, logo-medium.jpg, logo-large.jpg
https://images.jiaoben.netconst width = https://images.jiaoben.nettypeof https://images.jiaoben.netwindow !== https://images.jiaoben.net'undefined' ? https://images.jiaoben.netwindow.https://images.jiaoben.netinnerWidth : https://images.jiaoben.net1200
https://images.jiaoben.netif (width < https://images.jiaoben.net600) {
https://images.jiaoben.netreturn props.https://images.jiaoben.netimagePath.https://images.jiaoben.netreplace(https://images.jiaoben.net/.(jpg|png)$/, https://images.jiaoben.net'-small.$1')
}
https://images.jiaoben.netif (width < https://images.jiaoben.net1200) {
https://images.jiaoben.netreturn props.https://images.jiaoben.netimagePath.https://images.jiaoben.netreplace(https://images.jiaoben.net/.(jpg|png)$/, https://images.jiaoben.net'-medium.$1')
}
https://images.jiaoben.netreturn props.https://images.jiaoben.netimagePath.https://images.jiaoben.netreplace(https://images.jiaoben.net/.(jpg|png)$/, https://images.jiaoben.net'-large.$1')
})
https://images.jiaoben.netscript>
图片优化的效果
| 图片类型 | 优化前 | 优化后 | 节省 |
|---|---|---|---|
| PNG 图标 | 120KB | 35KB | 71% |
| JPG 产品图 | 850KB | 180KB | 79% |
| WebP 背景 | 650KB | 110KB | 83% |
| SVG 矢量 | 15KB | 8KB | 47% |
| 总体积 | 2.8MB | 0.6MB | 78% |
Gzip/Brotli 压缩 - 让传输更轻盈
什么是 Gzip/Brotli?
我们可以用快递来比喻,比如我们有一件很大的“羽绒服”要邮寄给浏览器:
- 原始文件:一件羽绒服(很大,但很轻)
- Gzip:真空压缩袋,把羽绒服压扁
- Brotli:更好的真空压缩袋,压得更扁
当浏览器收到压缩后的文件,它只需要打开压缩袋,羽绒服(文件)就可以恢复原状!
压缩算法的对比
| 算法 | 压缩率 | 压缩速度 | 解压速度 | 浏览器支持 |
|---|---|---|---|---|
| Gzip | 中等 | 快 | 快 | 所有浏览器 |
| Brotli | 高 | 慢 | 中等 | 现代浏览器 (92%) |
| Deflate | 低 | 极快 | 极快 | 所有浏览器 |
相同文件对比
- 原始 JS: 1000 KB
- Gzip: 280 KB (72% 减少)
- Brotli: 220 KB (78% 减少)
- Brotli 比 Gzip 再减少 21% 体积
使用 vite-plugin-compression 配置
安装
npm install --save-dev vite-plugin-compression
配置
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport compression https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-compression'
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.net// Gzip 压缩
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240, https://images.jiaoben.net// 10KB 以上才压缩
https://images.jiaoben.netdeleteOriginFile: https://images.jiaoben.netfalse, https://images.jiaoben.net// 保留原文件
https://images.jiaoben.netverbose: https://images.jiaoben.nettrue, https://images.jiaoben.net// 输出压缩信息
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css|html|svg)$/ https://images.jiaoben.net// 只压缩文本文件
}),
https://images.jiaoben.net// Brotli 压缩
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netext: https://images.jiaoben.net'.br',
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240,
https://images.jiaoben.netdeleteOriginFile: https://images.jiaoben.netfalse,
https://images.jiaoben.netverbose: https://images.jiaoben.nettrue,
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css|html|svg)$/
})
]
}
https://images.jiaoben.net// 构建结果:
https://images.jiaoben.net// index.abc123.js
https://images.jiaoben.net// index.abc123.js.gz (Gzip)
https://images.jiaoben.net// index.abc123.js.br (Brotli)
智能压缩策略 - 多算法混合策略
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport compression https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-compression'
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.net// 对不同的资源使用不同的策略
https://images.jiaoben.net// 1. HTML: 使用 Brotli(最高压缩率)
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netext: https://images.jiaoben.net'.br',
https://images.jiaoben.netfilter: https://images.jiaoben.net/.html$/,
https://images.jiaoben.netthreshold: https://images.jiaoben.net1024
}),
https://images.jiaoben.net// 2. JS/CSS: 同时生成 Gzip 和 Brotli
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
}),
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netext: https://images.jiaoben.net'.br',
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
}),
https://images.jiaoben.net// 3. 大文件用 Brotli,小文件用 Gzip
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netext: https://images.jiaoben.net'.br',
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
https://images.jiaoben.netthreshold: https://images.jiaoben.net51200 https://images.jiaoben.net// 50KB 以上用 Brotli
}),
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240, https://images.jiaoben.net// 10-50KB 用 Gzip
https://images.jiaoben.netdeleteOriginFile: https://images.jiaoben.nettrue https://images.jiaoben.net// 小文件可以删除原文件
})
]
}
Nginx 配置示例
https://images.jiaoben.net# nginx.conf
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
https://images.jiaoben.net# 开启 Gzip
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/x-javascript
application/xml application/json;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
https://images.jiaoben.net# Brotli 支持(需要编译 brotli 模块)
brotli on;
brotli_min_length 10240;
brotli_types text/plain text/css text/xml text/javascript
application/javascript application/x-javascript
application/xml application/json;
brotli_comp_level 6;
location / {
try_files https://images.jiaoben.net$uri https://images.jiaoben.net$uri/ /index.html;
https://images.jiaoben.net# 尝试 Brotli,然后是 Gzip,最后是原始文件
location ~* .(js|css)$ {
try_files https://images.jiaoben.net$uri.br https://images.jiaoben.net$uri.gz https://images.jiaoben.net$uri =404;
https://images.jiaoben.net# 根据 Accept-Encoding 设置正确的 Content-Encoding
https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* br) {
add_header Content-Encoding br;
add_header Content-Type https://images.jiaoben.net$content_type;
}
https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* gzip) {
add_header Content-Encoding gzip;
add_header Content-Type https://images.jiaoben.net$content_type;
}
https://images.jiaoben.net# 长期缓存
expires 1y;
add_header Cache-Control https://images.jiaoben.net"public, immutable";
add_header Vary Accept-Encoding;
}
https://images.jiaoben.net# 图片缓存
location ~* .(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
expires 30d;
add_header Cache-Control https://images.jiaoben.net"public";
}
}
}
验证压缩效果
https://images.jiaoben.net# 使用 curl 验证压缩
https://images.jiaoben.net# 查看是否支持压缩
curl -H https://images.jiaoben.net"Accept-Encoding: gzip, br" -I
https://images.jiaoben.net# 响应头应该包含
Content-Encoding: br
Content-Type: application/javascript
Content-Length: 220000
https://images.jiaoben.net# 下载并解压验证
curl -H https://images.jiaoben.net"Accept-Encoding: br" | brotli -d
https://images.jiaoben.net# 或者使用 httpie
http Accept-Encoding:br
长期缓存策略:让缓存最大化
文件名哈希的原理
https://images.jiaoben.net// 构建后的文件名
https://images.jiaoben.net// index.[hash].js
https://images.jiaoben.net// 哈希是基于文件内容生成的
https://images.jiaoben.net// 内容不变 → 哈希不变 → 缓存有效
https://images.jiaoben.net// 内容变化 → 哈希变化 → 重新下载
dist/
├── index.https://images.jiaoben.netabc123.https://images.jiaoben.netjs https://images.jiaoben.net// 哈希基于内容生成
├── index.https://images.jiaoben.netdef456.https://images.jiaoben.netjs https://images.jiaoben.net// 内容变化,哈希变化
├── vendor-vue.123abc.https://images.jiaoben.netjs https://images.jiaoben.net// 第三方库几乎不变
└── vendor-ui.456def.https://images.jiaoben.netjs https://images.jiaoben.net// UI 库偶尔更新
配置文件名哈希
https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netbuild: {
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.net// 入口文件
https://images.jiaoben.netentryFileNames: https://images.jiaoben.net'assets/[name].[hash].js',
https://images.jiaoben.net// 异步 chunk
https://images.jiaoben.netchunkFileNames: https://images.jiaoben.net'assets/chunks/[name].[hash].js',
https://images.jiaoben.net// 资源文件
https://images.jiaoben.netassetFileNames: https://images.jiaoben.net'assets/[ext]/[name].[hash].[ext]',
https://images.jiaoben.netmanualChunks: {
https://images.jiaoben.net// 稳定的第三方库单独打包(几乎不变)
https://images.jiaoben.net'vendor-stable': [
https://images.jiaoben.net'vue',
https://images.jiaoben.net'vue-router',
https://images.jiaoben.net'pinia',
https://images.jiaoben.net'vuex'
],
https://images.jiaoben.net// 可能更新的 UI 库单独打包
https://images.jiaoben.net'vendor-ui': [
https://images.jiaoben.net'element-plus',
https://images.jiaoben.net'@element-plus/icons-vue',
https://images.jiaoben.net'ant-design-vue'
],
https://images.jiaoben.net// 可能更新的工具库
https://images.jiaoben.net'vendor-utils': [
https://images.jiaoben.net'lodash-es',
https://images.jiaoben.net'dayjs',
https://images.jiaoben.net'axios'
]
}
}
},
https://images.jiaoben.net// 生成 manifest.json
https://images.jiaoben.netmanifest: https://images.jiaoben.nettrue
}
}
Nginx 缓存配置
https://images.jiaoben.net# nginx.conf
server {
https://images.jiaoben.net# 静态资源缓存配置
https://images.jiaoben.net# JS/CSS 长期缓存(带 hash 的文件)
location ~* .(js|css)$ {
https://images.jiaoben.net# 匹配带 hash 的文件
https://images.jiaoben.netif (https://images.jiaoben.net$uri ~* https://images.jiaoben.net".[a-f0-9]{8,20}.(js|css)$") {
expires 1y;
add_header Cache-Control https://images.jiaoben.net"public, immutable";
}
https://images.jiaoben.net# 如果不带 hash,短时间缓存
expires 1h;
add_header Cache-Control https://images.jiaoben.net"public";
https://images.jiaoben.net# 尝试压缩版本
try_files https://images.jiaoben.net$uri.br https://images.jiaoben.net$uri.gz https://images.jiaoben.net$uri =404;
add_header Vary Accept-Encoding;
}
https://images.jiaoben.net# 图片等资源
location ~* .(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
expires 30d;
add_header Cache-Control https://images.jiaoben.net"public";
}
https://images.jiaoben.net# 字体文件
location ~* .(woff2?|ttf|eot)$ {
expires 1y;
add_header Cache-Control https://images.jiaoben.net"public, immutable";
add_header Access-Control-Allow-Origin https://images.jiaoben.net"*";
}
https://images.jiaoben.net# HTML 文件不缓存
location ~* .html$ {
expires -1;
add_header Cache-Control https://images.jiaoben.net"no-cache, must-revalidate";
}
}
Service Worker 缓存策略
https://images.jiaoben.net// sw.js
https://images.jiaoben.netconst https://images.jiaoben.netCACHE_NAME = https://images.jiaoben.net'v1'
https://images.jiaoben.netconst https://images.jiaoben.netCACHE_URLS = [
https://images.jiaoben.net'/',
https://images.jiaoben.net'/index.html',
https://images.jiaoben.net'/manifest.json'
]
https://images.jiaoben.net// 安装时缓存核心资源
self.https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'install', https://images.jiaoben.nethttps://images.jiaoben.netevent => {
event.https://images.jiaoben.netwaitUntil(
caches.https://images.jiaoben.netopen(https://images.jiaoben.netCACHE_NAME)
.https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netcache => cache.https://images.jiaoben.netaddAll(https://images.jiaoben.netCACHE_URLS))
)
})
https://images.jiaoben.net// 缓存策略:缓存优先,网络回退
self.https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'fetch', https://images.jiaoben.nethttps://images.jiaoben.netevent => {
https://images.jiaoben.netconst url = https://images.jiaoben.netnew https://images.jiaoben.netURL(event.https://images.jiaoben.netrequest.https://images.jiaoben.neturl)
https://images.jiaoben.net// 静态资源使用 Cache First 策略
https://images.jiaoben.netif (url.https://images.jiaoben.netpathname.https://images.jiaoben.netmatch(https://images.jiaoben.net/.(js|css|png|jpg|webp)$/)) {
event.https://images.jiaoben.netrespondWith(
caches.https://images.jiaoben.netmatch(event.https://images.jiaoben.netrequest)
.https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
https://images.jiaoben.net// 缓存命中直接返回
https://images.jiaoben.netif (response) https://images.jiaoben.netreturn response
https://images.jiaoben.net// 未命中则请求网络并缓存
https://images.jiaoben.netreturn https://images.jiaoben.netfetch(event.https://images.jiaoben.netrequest).https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
https://images.jiaoben.netconst clone = response.https://images.jiaoben.netclone()
caches.https://images.jiaoben.netopen(https://images.jiaoben.netCACHE_NAME).https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netcache => {
cache.https://images.jiaoben.netput(event.https://images.jiaoben.netrequest, clone)
})
https://images.jiaoben.netreturn response
})
})
)
}
https://images.jiaoben.net// HTML 使用 Network First 策略
https://images.jiaoben.netelse https://images.jiaoben.netif (url.https://images.jiaoben.netpathname.https://images.jiaoben.netendsWith(https://images.jiaoben.net'.html') || url.https://images.jiaoben.netpathname === https://images.jiaoben.net'/') {
event.https://images.jiaoben.netrespondWith(
https://images.jiaoben.netfetch(event.https://images.jiaoben.netrequest)
.https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
https://images.jiaoben.netconst clone = response.https://images.jiaoben.netclone()
caches.https://images.jiaoben.netopen(https://images.jiaoben.netCACHE_NAME).https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netcache => {
cache.https://images.jiaoben.netput(event.https://images.jiaoben.netrequest, clone)
})
https://images.jiaoben.netreturn response
})
.https://images.jiaoben.netcatch(https://images.jiaoben.net() => caches.https://images.jiaoben.netmatch(event.https://images.jiaoben.netrequest))
)
}
})
缓存命中率的提升
| 文件类型 | 更新频率 | 缓存策略 | 命中率 |
|---|---|---|---|
| vendor-vue.js | 几乎不变 | 永久缓存 | 99% |
| vendor-ui.js | 偶尔更新 | 永久缓存 | 92% |
| page-*.js | 经常更新 | 永久缓存 | 65% |
| 图片 | 很少更新 | 30天缓存 | 95% |
| 字体 | 从不更新 | 永久缓存 | 99% |
实战案例:一个中大型项目的构建优化
优化前的状态
https://images.jiaoben.net// 项目信息
https://images.jiaoben.net// - 页面数量:45 个
https://images.jiaoben.net// - 组件数量:850 个
https://images.jiaoben.net// - 第三方依赖:230 个
https://images.jiaoben.net// - 图片数量:1200 张
https://images.jiaoben.net// 构建产物
dist/ 总大小: https://images.jiaoben.net45 https://images.jiaoben.netMB
├── js/ https://images.jiaoben.net28 https://images.jiaoben.netMB
├── css/ https://images.jiaoben.net2.5 https://images.jiaoben.netMB
├── images/ https://images.jiaoben.net14 https://images.jiaoben.netMB
└── others/ https://images.jiaoben.net0.5 https://images.jiaoben.netMB
https://images.jiaoben.net// 性能指标
https://images.jiaoben.net// - 构建时间:3 分 45 秒
https://images.jiaoben.net// - 首屏体积:4.2 MB
https://images.jiaoben.net// - 加载时间:3.2 秒
优化步骤
第一步:分析找出问题
https://images.jiaoben.net# 运行分析
npx vite-bundle-visualizer
https://images.jiaoben.net# 发现问题
echarts: 1.2MB ← 太大
monaco-editor: 2.8MB ← 巨大!
lodash-es: 210KB ← 还好
moment: 450KB ← 可以用 dayjs 替代
第二步:优化拆包
https://images.jiaoben.net// vite.config.js
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netbuild: {
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
https://images.jiaoben.net// 把 echarts 单独打包
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-echarts'
}
https://images.jiaoben.net// 把 monaco-editor 单独打包
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco-editor')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'
}
https://images.jiaoben.net// 其他分组
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'dayjs')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'
}
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other'
}
https://images.jiaoben.net// 按页面拆分
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/views/')) {
https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/views/([^/]+)/)
https://images.jiaoben.netif (match) https://images.jiaoben.netreturn https://images.jiaoben.net`page-https://images.jiaoben.net${match[https://images.jiaoben.net1]}`
}
}
}
}
}
}
第三步:图片压缩
https://images.jiaoben.net// vite.config.js
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.netViteImageOptimizer({
https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net75 },
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
https://images.jiaoben.netavif: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
})
]
}
第四步:开启压缩
https://images.jiaoben.net// vite.config.js
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
https://images.jiaoben.netplugins: [
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
})
]
}
第五步:按需加载
https://images.jiaoben.net// 大组件使用动态导入
https://images.jiaoben.netconst https://images.jiaoben.netMonacoEditor = https://images.jiaoben.netdefineAsyncComponent(https://images.jiaoben.net() =>
https://images.jiaoben.netimport(https://images.jiaoben.net'monaco-editor')
)
https://images.jiaoben.net// 路由懒加载
https://images.jiaoben.netconst routes = [
{
https://images.jiaoben.netpath: https://images.jiaoben.net'/dashboard',
https://images.jiaoben.netcomponent: https://images.jiaoben.net() => https://images.jiaoben.netimport(https://images.jiaoben.net'./views/Dashboard.vue') https://images.jiaoben.net// 按需加载
}
]
优化后的结果
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 构建时间 | 3 分 45 秒 | 2 分 20 秒 | 38% |
| 总大小 | 45 MB | 18 MB | 60% |
| 首屏 JS 体积 | 4.2 MB | 1.8 MB | 57% |
| 图片体积 | 14 MB | 3.5 MB | 75% |
| 传输体积 | 3.2 MB | 0.8 MB | 75% |
| 加载时间 | 3.2 秒 | 1.1 秒 | 65% |
常见问题与解决方案
问题一:拆包过多导致请求数爆炸
https://images.jiaoben.net// 错误:拆得太细
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
https://images.jiaoben.net// 每个依赖都单独打包
https://images.jiaoben.netreturn id.https://images.jiaoben.netmatch(https://images.jiaoben.net/node_modules/([^/]+)/)?.[https://images.jiaoben.net1]
}
https://images.jiaoben.net// 结果:产生 200+ 个文件,HTTP/1.1 下性能差
https://images.jiaoben.net// 正确:合理分组
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-charts'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other' https://images.jiaoben.net// 其他合并
}
}
问题二:图片压缩后质量下降
https://images.jiaoben.net// 解决方案:选择性压缩
https://images.jiaoben.netViteImageOptimizer({
https://images.jiaoben.net// 图标保留较高品质
https://images.jiaoben.net'src/assets/icons/**/*': {
https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net90 },
https://images.jiaoben.netsvg: { https://images.jiaoben.netplugins: [https://images.jiaoben.net'preset-default'] }
},
https://images.jiaoben.net// 背景图可以接受较低品质
https://images.jiaoben.net'src/assets/backgrounds/**/*': {
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net65 },
https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
},
https://images.jiaoben.net// 产品图需要平衡
https://images.jiaoben.net'src/assets/products/**/*': {
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net80 },
https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net75 }
}
})
https://images.jiaoben.net// 或者使用图片 CDN 动态处理
"https://cdn.example.com/image.jpg?x-oss-process=image/resize,w_400/quality,q_80">
问题三:Brotli 压缩太慢
https://images.jiaoben.net// 解决方案:选择性使用 Brotli
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netthreshold: https://images.jiaoben.net50000, https://images.jiaoben.net// 50KB 以上才用 Brotli
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/
})
https://images.jiaoben.net// 小文件继续用 Gzip
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240, https://images.jiaoben.net// 10-50KB 用 Gzip
https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/
})
问题四:CDN 不支持 Brotli
https://images.jiaoben.net# 解决方案:同时生成 Gzip 和 Brotli
location /assets {
https://images.jiaoben.net# 优先尝试 Brotli
try_files https://images.jiaoben.net$uri.br https://images.jiaoben.net$uri.gz https://images.jiaoben.net$uri =404;
https://images.jiaoben.net# 根据 Accept-Encoding 返回正确的 Content-Encoding
https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* br) {
add_header Content-Encoding br;
}
https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* gzip) {
add_header Content-Encoding gzip;
}
}
生产环境优化的最佳实践
优化检查清单
- 使用
visualizer分析构建产物 - 配置
manualChunks合理拆包 - 图片资源压缩优化
- 启用 Gzip/Brotli 压缩
- 配置长期缓存策略
- 设置性能预算
- 在 CI/CD 中集成检查
- 定期监控 Web Vitals
配置文件模板
https://images.jiaoben.net// vite.config.ts - 生产环境优化完整配置
https://images.jiaoben.netimport { defineConfig } https://images.jiaoben.netfrom https://images.jiaoben.net'vite'
https://images.jiaoben.netimport vue https://images.jiaoben.netfrom https://images.jiaoben.net'@vitejs/plugin-vue'
https://images.jiaoben.netimport { visualizer } https://images.jiaoben.netfrom https://images.jiaoben.net'rollup-plugin-visualizer'
https://images.jiaoben.netimport { https://images.jiaoben.netViteImageOptimizer } https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-image-optimizer'
https://images.jiaoben.netimport compression https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-compression'
https://images.jiaoben.netexport https://images.jiaoben.netdefault https://images.jiaoben.netdefineConfig(https://images.jiaoben.net(https://images.jiaoben.net{ mode }) => ({
https://images.jiaoben.netplugins: [
https://images.jiaoben.netvue(),
https://images.jiaoben.net// 图片压缩
https://images.jiaoben.netViteImageOptimizer({
https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net75 },
https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
https://images.jiaoben.netavif: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
}),
https://images.jiaoben.net// Gzip 压缩
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
}),
https://images.jiaoben.net// Brotli 压缩
https://images.jiaoben.netcompression({
https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
https://images.jiaoben.netext: https://images.jiaoben.net'.br',
https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
}),
https://images.jiaoben.net// 构建分析(只在需要时开启)
process.https://images.jiaoben.netenv.https://images.jiaoben.netANALYZE && https://images.jiaoben.netvisualizer({
https://images.jiaoben.netopen: https://images.jiaoben.nettrue,
https://images.jiaoben.netfilename: https://images.jiaoben.net'dist/stats.html',
https://images.jiaoben.netgzipSize: https://images.jiaoben.nettrue,
https://images.jiaoben.netbrotliSize: https://images.jiaoben.nettrue
})
].https://images.jiaoben.netfilter(https://images.jiaoben.netBoolean),
https://images.jiaoben.netbuild: {
https://images.jiaoben.nettarget: https://images.jiaoben.net'es2015',
https://images.jiaoben.netminify: https://images.jiaoben.net'terser',
https://images.jiaoben.netterserOptions: {
https://images.jiaoben.netcompress: {
https://images.jiaoben.netdrop_console: mode === https://images.jiaoben.net'production',
https://images.jiaoben.netdrop_debugger: https://images.jiaoben.nettrue
}
},
https://images.jiaoben.netrollupOptions: {
https://images.jiaoben.netoutput: {
https://images.jiaoben.netentryFileNames: https://images.jiaoben.net'assets/[name].[hash].js',
https://images.jiaoben.netchunkFileNames: https://images.jiaoben.net'assets/chunks/[name].[hash].js',
https://images.jiaoben.netassetFileNames: https://images.jiaoben.net'assets/[ext]/[name].[hash].[ext]',
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'antd')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'd3')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-charts'
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'dayjs')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco-editor')) {
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'
}
https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other'
}
https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/views/')) {
https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/views/([^/]+)/)
https://images.jiaoben.netif (match) https://images.jiaoben.netreturn https://images.jiaoben.net`page-https://images.jiaoben.net${match[https://images.jiaoben.net1]}`
}
}
}
},
https://images.jiaoben.netchunkSizeWarningLimit: https://images.jiaoben.net500,
https://images.jiaoben.netsourcemap: mode !== https://images.jiaoben.net'production',
https://images.jiaoben.netmanifest: https://images.jiaoben.nettrue
}
}))
性能目标参考
| 指标 | 优秀 | 一般 | 差 |
|---|---|---|---|
| 首屏 JS 体积 | < 200KB | 200-500KB | > 500KB |
| 总构建体积 | < 2MB | 2-5MB | > 5MB |
| 图片体积占比 | < 30% | 30-50% | > 50% |
| 压缩率 | > 70% | 50-70% | < 50% |
| 缓存命中率 | > 80% | 50-80% | < 50% |
| FCP | < 1.5s | 1.5-2.5s | > 2.5s |
| LCP | < 2.5s | 2.5-4s | > 4s |
三个核心原则
- 测量优先:没有数据的优化是盲目的
- 渐进改进:每次只优化一个指标
- 用户优先:始终以用户体验为导向
结语
优化的终极目标是让用户感受不到加载的存在。当用户打开我们的应用时,内容瞬间呈现,交互立即响应,这就说明我们的优化成功了!
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!
相关推荐
专题
+ 收藏
+ 收藏
+ 收藏
+ 收藏
+ 收藏
最新数据
相关文章
最新版vue3+TypeScript开发入门到实战教程之路由详解二
03/28
src-components调用链与即时聊天组件树
03/28
从0开始设计一个树和扁平数组的双向同步方案
03/28
拒绝 rem 计算!Vue3 大屏适配,我用 vfit 一行代码搞定
03/28
Home双router-view与布局切换逻辑
03/28
uniapp uview-plus 自定义动态验证
03/28
Vue3 单元测试实战:从组合式函数到组件
03/28
VUE-组件命名与注册机制
03/28
VTJ.PRO 在线应用开发平台概览
03/28
v0.dev 支持 RSC 了!AI 生成全栈组件离我们还有多远?
03/28
AI精选
