Vite 构建体积优化:从 2MB 到 700KB
前阵子接手一个内部运营后台,随手跑了一次 npm run build,结果控制台赫然打出一行:
dist/assets/index-7f3a9c.js 2,341,890 bytes
dist/assets/vendor-1b8d2e.js 984,213 bytes
dist/assets/index-2c4f1a.css 312,057 bytes
✓ 5.42s
主 chunk 干到了 2.3MB。这是个内网后台,网络不算差,但首屏白屏还是能感觉到明显的卡顿。稍微一查发现,2.3MB 里塞了 moment、整个 antd、lodash 全家桶,外加一个几乎没人用的富文本编辑器。这文章就是这次瘦身过程的记录,最终产物落到 700KB(gzip 后 220KB 左右)。
第一步:用 visualizer 看清产物构成
优化前先得有数据。装上 rollup-plugin-visualizer,它会输出一个交互式的 treemap,让你一眼看清每个依赖占了多少体积。
// vite.config.js
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
filename: 'stats.html',
gzipSize: true,
brotliSize: true,
}),
],
})
构建后打开 stats.html,看到的大头是:moment 占 200KB+、antd 全量 380KB、echarts 320KB(其实只用了折线图)、还有个被多处 import 的 lodash。问题清单一下就清晰了。
第二步:干掉 moment,换 dayjs
moment 最大的坑是它的 locale:默认会把全世界的语言包都打进来,每个 50KB 左右,几十种语言叠加非常可观。而且它本身 API 设计得就重。
迁移到 dayjs 几乎无痛,API 几乎一致,只多了一个 plugin 机制:
// 旧代码
import moment from 'moment'
const now = moment().format('YYYY-MM-DD HH:mm:ss')
// 新代码:dayjs + 中文 locale + 自定义解析插件
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import customParseFormat from 'dayjs/plugin/customParseFormat'
dayjs.extend(customParseFormat)
dayjs.locale('zh-cn')
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
替换后这一块的体积从 200KB+ 掉到了 7KB。注意 antd 的 DatePicker 内部如果还在用 moment,需要确认版本——antd v5 已经默认 dayjs,v4 要配 antd-dayjs-webpack-plugin 或手动替换。
第三步:antd 按需引入的取舍
antd v5 默认已经走 tree-shaking,理论上不用按需引入 babel 插件。但前提是你的导入方式干净:
// ❌ 反例:整包引入会让 tree-shaking 失效
import { Button, DatePicker } from 'antd' // 仅当 antd 版本/配置不对时才危险
// ✅ 推荐:具名导入,配合 Vite 的 tree-shaking
import Button from 'antd/es/button'
import DatePicker from 'antd/es/date-picker'
实践上,我倾向于保留全量引入但依赖 Vite 的优化,只在体积真的扛不住时才退化到深路径引入。深路径引入可读性差,团队里每个人都得记得规矩,维护成本反而更高。
echarts 同理:用 echarts/core 配合 use([LineChart, GridComponent, ...]) 按需注册,能从 320KB 砍到 80KB。
第四步:manualChunks 分包策略
前面三步压的是总量,但生产环境还得分包,避免一次改动让用户把整个 vendor 重新下载。build.rollupOptions.output.manualChunks 是关键:
// vite.config.js
export default defineConfig({
build: {
target: 'es2018',
cssCodeSplit: true,
minify: 'esbuild',
rollupOptions: {
output: {
// 静态、长期不变的依赖单独分包,吃浏览器缓存
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom') || id.includes('react-router')) {
return 'react-vendor'
}
if (id.includes('antd') || id.includes('@ant-design')) {
return 'antd-vendor'
}
if (id.includes('echarts')) {
return 'echarts-vendor'
}
return 'vendor'
}
},
},
},
},
})
这样 react/react-dom、antd、echarts 各自独立成包。业务代码改动时,用户只需要重下业务 chunk,几个大 vendor 包继续吃缓存,平均加载时间能再省一截。
优化前后对比
| 资源 | 原始大小 | gzip 后 | 优化后 | gzip 后 |
|---|---|---|---|---|
| index (业务代码) | 2,341 KB | 780 KB | 186 KB | 61 KB |
| antd-vendor | — | — | 214 KB | 67 KB |
| echarts-vendor | 320 KB | 108 KB | 82 KB | 27 KB |
| react-vendor | 142 KB | 46 KB | 142 KB | 46 KB |
| CSS | 312 KB | 42 KB | 78 KB | 11 KB |
| 合计 | ~2,780 KB | ~976 KB | ~702 KB | ~212 KB |
总体积从接近 2.8MB 降到 700KB 出头,gzip 后 212KB,首屏从原来的 3.8s 降到 1.2s 左右(内网千兆,主要省在解析执行时间)。
✓ 小结
构建优化这件事,思路永远是先测量、再动手。visualizer 给出方向,按需引入 + 替换重型依赖做减法,最后用 manualChunks 让缓存命中率最大化。剩下 30% 的体积别死磕——配置成本和维护成本的平衡,比追求极致数字更重要。