ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vite构建优化实战:从40秒到10秒的产物体积与速度调优

Vite构建优化实战:从40秒到10秒的产物体积与速度调优 做前端这几年Vite基本已经是我搭建Vue3项目的默认选择了。开发服务器启动快、HMR反馈几乎无感体验过一次之后确实很难再回webpack那一套。但有一个问题很容易被低估项目做大了以后vite build的出包速度和产物体积是需要单独花精力去调的不然上线前你会在构建机上等到怀疑人生。我手里有几个Vue3后台管理系统页面数量过百全局引用的库又有element-plus、echarts这类重家伙。最初一次vite build要40多秒产物4MB以上首屏加载被同事吐槽了好几次。这篇文章就是把最近几轮优化的思路、具体配置和踩过的坑整理一遍适合刚把Vue3项目迁到Vite、或者正在被构建时间和包体积折磨的同学参考。后面有新进展我会持续更新。1. 动手优化前先量化现状与定位瓶颈1.1 基准数据要记牢刚接手一个Vite构建项目别急着改配置。第一步先把现状量化出来构建总耗时、产物总体积、最大chunk体积、首屏实际加载的资源数这四个数据记下来后面每一步优化做完都要回来对照才知道改动到底有没有效果。构建总耗时和产物体积最直观看终端里vite build跑完输出的那行即可。$ npm run build vite build vite v6.0.3 building for production... transforming... ✓ 642 modules transformed. dist/index.html 0.45 kB │ gzip: 0.29 kB dist/assets/index-D1x2c3e4.js 1382.50 kB │ gzip: 412.35 kB dist/assets/vendor-9f8a7b6c.js 2064.10 kB │ gzip: 601.22 kB ✓ built in 38.62s看到这样的输出心里基本就有数了单个vendor chunck超过2MBgzip后601KB首屏加载绝对会卡。于是我还额外用浏览器DevTools的Network面板看了一遍线上页面确认首屏需要等待的request数量以及最大的几个JS文件的下载时间把这些问题记进优化清单里。这一步不要偷懒。很多时候我们凭感觉觉得“包很大”但到底大在哪不做记录后面排查会很痛苦。1.2 用构建日志定位耗时大头如果构建时间太长建议给Vite挂一个临时插件把构建各阶段的耗时量化出来。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [ vue(), { name: build-timer, buildStart() { console.time(build-time) }, closeBundle() { console.timeEnd(build-time) } } ] })比起肉眼盯着终端发呆这种打点方式能准确告诉你哪一部分最耗时间。我实测下来Vite生产构建的主要耗时通常集中在三块依赖预构建与分析如果项目里第三方依赖特别多初次构建时对依赖的扫描和转换要占不少时间。源码转换与打包业务代码量越大这一阶段越慢尤其是使用了大量SFC和TS文件时。压缩混淆默认用esbuild压缩如果切换成terser压缩阶段的耗时可能翻倍甚至更多。建议先跑一次npx vite --debug build看看输出--debug模式会打印很多底层细节。也可以临时加一个耗时插件来定位。定位完瓶颈之后再针对性地调整比盲目套网上的优化配置靠谱得多。2. 依赖预构建与构建提速的核心配置2.1 预构建缓存与 optimizeDeps 的取舍Vite的依赖预构建是它的一个标志性机制开发模式下用esbuild对node_modules里的依赖做预打包把CommonJS/UMD格式转成ESM并缓存到node_modules/.vite目录。这带来的好处是开发服务器启动时不需要像webpack那样全量编译浏览器请求依赖时直接走缓存速度自然快。生产构建时同样会对依赖进行预扫描和处理所以optimizeDeps的配置对构建速度也有影响。项目初期我不建议手动维护一长串include和exclude多数情况下默认配置已经够用。真正需要介入的场景我遇到过两种一是某个依赖在开发阶段反复触发“重新加载”或者生产构建报“Failed to resolve import”这时需要在include里手动指定依赖名强制Vite对它的依赖关系做完整预构建。二是某个库本身已经是规范的原生ESM并且体积很小可以尝试exclude掉减少一次预构建耗时。但要提醒一句exclude操作要谨慎。如果这个库内部依赖关系复杂排除预构建之后反而会导致开发服务器加载大量零散文件性能直接下降。如果你修改了optimizeDeps配置但构建结果还是老样子多半是缓存没失效。清理方式有两种删掉node_modules/.vite目录或者执行npx vite --force强制重建缓存。这算是我踩过最频繁的一个坑尤其是重复切换分支时总要记得先清理一次。2.2 压缩器选 esbuild 还是 terserVite 5和Vite 6默认生产压缩器都是esbuild但对老项目或从webpack迁移过来的项目很多人会习惯性地去配置terser因为webpack生态里terser-webpack-plugin太普遍了。这里就涉及一个取舍esbuild压缩速度快产物体积略微偏大terser压缩速度慢不少但产物能再小一点。我个人的经验是除非上线要求压缩到极致否则用默认esbuild不要折腾terser。原因很简单esbuild的压缩速度是terser的好几倍对构建时间的收益非常明显体积差距通常在1%到3%之间。对绝大多数业务系统来说这点体积差远不如构建速度重要。如果确实要用terser配置也很简单// vite.config.js export default defineConfig({ build: { minify: terser, terserOptions: { compress: { drop_console: true, drop_debugger: true } } } })注意drop_console在生产环境去掉console.log也是常用优化之一。但不建议把所有console全部删掉如果线上需要临时排查问题一点日志都没有也很被动。比较稳妥的方案是保留warn和error级别的输出只去掉log和debug。2.3 chunkFileNames 与 output 路径规范构建输出文件名的命名规则很多人会忽略但它和浏览器缓存策略直接相关。Vite默认生成的产物文件名自带hash比如index-D1x2c3e4.js这是为了内容更新后保证浏览器能拿到新文件。不过分包之后如果不对chunk文件做统一命名可能上线后排查问题时分不清哪个chunk对应哪个模块。我在项目里的配置一般是这样的build: { rollupOptions: { output: { entryFileNames: assets/js/[name]-[hash].js, chunkFileNames: assets/js/[name]-[hash].js, assetFileNames: assets/[ext]/[name]-[hash][extname], manualChunks: {} } } }entryFileNames管入口文件chunkFileNames管路由懒加载拆分出来的chunkassetFileNames管图片、字体等静态资源。把JS和CSS、图片分目录存放配合CDN部署时写缓存规则会清晰很多。3. 产物体积优化懒加载、按需引入与手动分包3.1 路由懒加载和组件按需加载后台管理系统最常见的体积问题是所有页面代码全部打到一个chunk里首屏加载时把整个系统的代码都下载一遍。解决的第一个动作就是路由懒加载让用户访问哪个路由才去加载对应页面的代码。// router/index.js import { createRouter, createWebHashHistory } from vue-router const routes [ { path: /, component: () import(/views/HomeView.vue) }, { path: /report, component: () import(/views/ReportView.vue) } ] export default createRouter({ history: createWebHashHistory(), routes })对于Vue3项目一二级路由都建议写成动态import()形式这样每个页面会被单独打包成chunk。页面里如果有比较大的弹窗组件、抽屉组件也不要全量注册进父组件可以等打开弹窗时再用defineAsyncComponent异步加载。除了路由级别的拆分还有一个容易忽略的点一些大型图表组件、富文本编辑器如果只在某个页面里用不要写在入口文件里全局注册。我曾经维护过一个可视化大屏项目最开始在main.js里全局注册了ECharts结果每个页面都背着ECharts跑首屏白白多出几百KB。改成按需引用之后体积下降非常明显。3.2 第三方库按需引入的三个案例element-plus按需引入很多后台管理系统都在用element-plus如果直接在main.js里app.use(ElementPlus)整包引入光这一个库的体积就非常可观。官方推荐的方式是用unplugin-vue-components和unplugin-auto-import做自动按需引入。// vite.config.js import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })配置完成后组件模板里直接写el-button样式和逻辑会自动按需引入不再需要全局引入element-plus/dist/index.css。这里有个小坑如果你同时在某个文件里手动import了ElMessage别忘了它对应的样式也要按需引入否则会出现“有逻辑没样式”的诡异问题。echarts按模块引入ECharts整包引入的体积很大但通常一个项目只会用到柱状图、折线图、饼图、散点图里的几种。更好的做法是按模块注册// utils/echarts.js import * as echarts from echarts/core import { BarChart, LineChart, PieChart } from echarts/charts import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent } from echarts/components import { CanvasRenderer } from echarts/renderers echarts.use([ TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, BarChart, LineChart, PieChart, CanvasRenderer ]) export default echarts这样打包时会自动tree-shaking掉不需要的部分。注意echarts/core和echarts/charts这些子路径必须是ESM格式才能被正确摇树Vite下默认就是支持的。lodash等工具库如果用lodash整包引入可以改成lodash-es配合Vite的tree-shaking只保留实际使用的方法。例如import { debounce, cloneDeep } from lodash-es3.3 manualChunks 手动分包的两种写法按需引入做完之后剩余的还是有一批公共依赖。如果全部打到一个vendor里可能这个vendor还是很大如果完全交给Vite自动拆分又可能出现chunk数量过多、加载碎片化的问题。因此手动分包是优化产物体积和缓存命中率的关键一步。常见的配置有两种数组式直接指定包名适合依赖数量少的项目。build: { rollupOptions: { output: { manualChunks: { vue: [vue, vue-router, pinia], element: [element-plus], charts: [echarts, zrender] } } } }函数式更灵活可以按模块路径动态归类。build: { rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(echarts) || id.includes(zrender)) { return charts } if (id.includes(element-plus) || id.includes(element-plus)) { return element-plus } if (id.includes(vue) || id.includes(pinia) || id.includes(vue-router)) { return vue-vendor } return vendor } } } } }我在实际项目中用函数式更多。但这里有一个很关键的注意点如果函数式写法里最后写了return vendor就等于把所有未匹配到的node_modules依赖都塞进vendor这个vendor很容易变成2MB以上的巨型chunk。所以如果不是特别有必要可以让部分小依赖跟着业务代码走不要一股脑全塞进vendor。分包数量也要控制。拆得太细浏览器并行请求数会增加HTTP/1.1下请求并发有限会造成阻塞。我一般会控制在5到8个chunk以内既保证缓存粒度又不至于把loading瀑布流拉得太长。3.4 gzip压缩与静态资源处理产物压缩是性价比相当高的优化手段。Vite官方不内置gzip但社区插件vite-plugin-compression可以直接接入。// vite.config.js import viteCompression from vite-plugin-compression export default defineConfig({ plugins: [ vue(), viteCompression({ algorithm: gzip, threshold: 10240, // 只压缩10KB以上文件 deleteOriginFile: false }) ] })threshold: 10240表示只有超过10KB的文件才生成gzip版本避免让小文件也多做一次压缩反而拖慢构建。deleteOriginFile不建议设成true否则服务器如果不做透明解压原文件也没了线上会直接白屏。如果服务端已经开了gzip或者brotli前端静态生成.gz文件就不是必须的两种方案二选一即可不要重复做。另外图片、字体这类资源gzip收益有限真正大头是JS和CSS。建议再用vite-plugin-compression的algorithm: brotli生成一份br文件配合CDN部署时体积还能再小不少。静态资源方面Vite默认assetsInlineLimit是4096字节小于4KB的图片会转成base64内联到代码里减少请求数。如果项目里有大量小图标可以把阈值适当调高到8KB或10KB减少小图片的HTTP请求。大图片、PDF这类资源还是建议放到public目录或直接走对象存储/CDN不要把大文件打进产物。4. 构建内存与多环境模式的细节调优4.1 内存溢出与 max-old-space-size 的正确姿势项目大到一定程度构建时经常报“JavaScript heap out of memory”。这不是配置写错了是Node默认堆内存上限不够用。最直接的解决办法是调大Node的内存上限。在package.json里把build脚本改成{ scripts: { build: node --max-old-space-size4096 node_modules/vite/bin/vite.js build } }这里4096单位是MB可以根据服务器内存调整一般4GB到8GB足够。另一种方式是用环境变量export NODE_OPTIONS--max-old-space-size4096 vite buildWindows的CMD里写成set NODE_OPTIONS--max-old-space-size4096 vite buildPowerShell里写法不同$env:NODE_OPTIONS--max-old-space-size4096; vite build搜索引擎里经常看到有人搜$ node_options--max-old-space-size4096 vite然后Windows下直接报“node_options不是内部或外部命令”这就是把类Unix shell的语法拿到Windows CMD里执行了。在Windows下只要用上面的set写法就能避免这个错误。4.2 sourcemap到底该不该开生产构建默认不开sourcemap这对减小产物体积、保护源码编译结果都有好处。但如果你接入了错误监控系统又需要通过sourcemap还原线上压缩前的代码该怎么办我建议的方案是生产环境用sourcemap: hidden把map文件单独保存到监控平台不在浏览器暴露。build: { sourcemap: hidden }hidden模式和普通模式一样会生成.map文件区别在于生成的JS文件里不会带有sourceMappingURL注释浏览器就不会主动去下载map文件也就变相降低了源码暴露风险。监控平台需要分析错误栈时再手动上传对应的map文件。如果公司压根没有错误监控体系那生产环境直接sourcemap: false省体积、省构建时间最简单。4.3 多环境构建vite build --mode test很多团队除了生产和开发环境还会有测试环境、预发布环境。Vite的环境模式通过--mode参数控制不同模式会加载对应的.env文件。项目根目录下可以创建.env # 所有环境通用 .env.development # 开发环境 .env.test # 测试环境 .env.production # 生产环境然后构建脚本里指定模式{ scripts: { build:test: vite build --mode test, build:prod: vite build --mode production } }在每个环境文件里定义变量# .env.test VITE_APP_TITLE测试环境 VITE_API_BASEhttps://test-api.example.com代码里通过import.meta.env.VITE_APP_TITLE读取。注意只有以VITE_开头的变量才会暴露给前端代码其它变量不会被打包进去。这里有个容易踩的坑如果vite build --mode test之后发现打出来的包还是走的production配置记住--mode只控制环境文件的加载构建本身还是会走生产模式逻辑包括压缩、tree-shaking等。所以不用担心用--mode test会打出开发模式包。5. 实战中常见的构建问题与排查技巧5.1 首屏加载慢的定位路径首屏慢先看浏览器Network面板按资源大小排序找出下载耗时最长的几个文件。如果是我们自己的chunk就用可视化工具看体积构成。这里推荐rollup-plugin-visualizer// vite.config.js import { visualizer } from rollup-plugin-visualizer export default defineConfig({ plugins: [ vue(), visualizer({ open: true, gzipSize: true }) ] })构建完成后插件会自动打开一个交互式的依赖体积分析页面能直接看到每个模块占的字节数。这个工具是我做体积优化的第一选择比猜靠谱得多。定位到大模块之后分别处理如果是第三方库考虑按需引入或单独分包如果是自己业务代码考虑路由懒加载或组件异步化。5.2 懒加载和分包失效的排查方向有时配置了路由懒加载但构建产物里发现所有页面代码还是堆在一起大概率是动态import()的路径没写对。Rollup要求动态导入必须使用静态可解析的字面量路径不能是纯变量拼接否则无法有效拆分chunk。手动分包配置了但觉得没生效可以从这几个方向排查manualChunks函数里有没有逻辑漏掉的依赖导致所有模块都被归到默认chunk里。是否有其它插件覆盖了output配置。修改配置后没有清缓存运行一次rm -rf node_modules/.vite再重新构建。5.3 Windows 下 node_options 报错还原很多刚在Windows上配置Vite的同学会遇到下面的报错node_options 不是内部或外部命令也不是可运行的程序或批处理文件。这不是Vite本身的问题而是命令写法不对。$ node_options...这种写法来自Linux/macOS的shellWindows的CMD并不支持。解决方案在4.1小节已经给过了Windows下用set NODE_OPTIONS--max-old-space-size4096 vite build或者直接把--max-old-space-size参数写进package.json的build脚本里这样团队所有成员统一命令不用各自记环境变量。5.4 常见问题速查表现象可能原因解决方式构建报“JavaScript heap out of memory”Node堆内存不足调大--max-old-space-size或调整构建脚本产物存在超大vendor chunk所有依赖被集中打包使用manualChunks按库拆分控制单个chunk体积首屏加载许多小chunk分包过细合并小chunk减少HTTP请求数配置了vite build --mode test却读到生产变量同名变量在不同.env中被覆盖检查.env.test与.env.production中变量名是否冲突代码里用了process.env却报错Vite默认不注入Node变量使用import.meta.env或通过define注入某些依赖在构建时解析失败依赖是CJS/UMD且预构建未处理完善在optimizeDeps.include中补充依赖名6. 持续迭代的优化清单与下一步规划6.1 把优化固化进CI优化做完之后最怕两件事一是后续开发中无意间引入了大依赖导致体积悄悄反弹二是团队成员各自在本地构建没人维护构建时长。因此我建议把体积和构建时间检查纳入CI流程给团队定一个性能预算。一个简单做法是在CI脚本里加一个体积检查脚本例如// scripts/check-size.mjs import { readdirSync, statSync } from node:fs import { join } from node:path const assetsDir dist/assets let totalSize 0 for (const file of readdirSync(assetsDir)) { const size statSync(join(assetsDir, file)).size totalSize size } const budget 800 * 1024 // 800KB if (totalSize budget) { console.error(产物总大小 ${(totalSize / 1024).toFixed(2)}KB超过预算 ${budget / 1024}KB) process.exit(1) } console.log(产物体积 ${(totalSize / 1024).toFixed(2)}KB在预算范围内)再配合定时任务监控CI构建日志中的built in耗时出现明显增长时可以及时回溯是哪次提交引入的。6.2 后续想验证的方向Vite生态更新速度很快最近我在关注的方向有两个。一是Vite官方正在推进的rolldown打包器它在底层用Rust实现理论上打包速度会比现在的Rollup方案更快后续如果稳定我打算在非核心项目上试水。二是随着项目规模继续增长可以考虑在架构层面把后台系统拆分成微前端不同业务模块独立构建、独立部署进一步缩短单次构建时间。优化本身没有终点。构建工具的版本、业务代码的规模、团队的交付节奏都在变所以这篇文章叫“持续更新”。目前这套配置在我的几个后台项目里已经稳定跑了几个月构建时间从40多秒压到了10秒左右产物总体积也降了将近一半。以后有新坑、新进展我会继续补充。最后分享一个体会优化项目时最容易犯的错就是看到网上的优化技巧就往上堆结果配置越来越复杂问题反而增多。我的习惯是每加一个优化项都记下改动前后的基准数据收益不明显就回滚。打包优化不是炫技能用最少的配置换来可感知的速度提升才是真正有价值的优化。
返回列表