文章 / 前端

Vite 冷启动从 40 秒到 3 秒,我只动了这几处配置

接手一个后台项目,npm run dev 要等 40 秒,改一行代码热更新又要三四秒。开发效率低到同事开始用 --force 赌运气。

花了一个下午梳理,问题分布在四个地方,都不算深奥,但叠在一起就很致命。

先别猜,加个分析器

优化最忌讳凭感觉。先让构建产物自己说话:

npm i -D rollup-plugin-visualizer
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({ open: true, gzipSize: true, brotliSize: true }),
  ],
});

跑一次 npm run build,会弹出一张依赖体积图。我们的图上,echarts 和 lodash 两个块加起来占了 62%,而这些包几乎全量进来了,实际用到的不足两成。

第一处:依赖预构建没吃上

Vite 会把 CommonJS 依赖预打包成 ESM,但如果依赖列表写得不全,它会在运行时现场探测,每次启动都重新做一遍。

export default defineConfig({
  optimizeDeps: {
    include: [
      'echarts/core',
      'echarts/charts',
      'echarts/components',
      'echarts/renderers',
      'dayjs',
      'axios',
    ],
    exclude: ['@app/local-lib'],   // 本地包不要预构建,否则改了不生效
  },
});

另外确认 node_modules 没被挂进监控范围。Windows 或 Docker 环境下可以显式关掉:

export default defineConfig({
  server: {
    watch: {
      ignored: ['**/node_modules/**', '**/.git/**'],
    },
  },
});

这一步单独就砍掉了 12 秒。

第二处:barrel 文件把整棵树拖进来了

这是最隐蔽的一个。项目里有个 components/index.ts,把上百个组件统一导出:

// ❌ components/index.ts —— 看起来整洁,实际是性能黑洞
export { default as Table } from './Table.vue';
export { default as Chart } from './Chart.vue';
// …还有一百多行

而业务代码里写 import { Table } from '@/components'。开发阶段浏览器要逐个请求这一百多个文件,页面加载慢;生产构建时 Tree Shaking 也会被这条"桶"拦住,因为副作用无法可靠判断。

改法很朴素——直接指向具体文件:

// ✅ 只加载真正用到的那个模块
import Table from '@/components/Table.vue';

如果调用点太多不好全改,至少给 package.json 标上无副作用,让打包器敢做裁剪:

{
  "sideEffects": ["*.css", "*.scss"]
}

第三处:图表库按需引入

echarts 全量引入约 1MB,按需之后只剩 300KB 出头:

// src/charts/setup.ts
import * as echarts from 'echarts/core';
import { LineChart, BarChart } from 'echarts/charts';
import {
  GridComponent,
  TooltipComponent,
  LegendComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
  LineChart,
  BarChart,
  GridComponent,
  TooltipComponent,
  LegendComponent,
  CanvasRenderer,
]);

export default echarts;

组件库同理。element-plus 之类一定要配合 unplugin-vue-components 做自动按需注册,不要为了省事全局 app.use()。

第四处:分包策略

默认分包按文件顺序切,结果每次发版用户都要重下整个 vendor。按运行时更新频率分层,缓存命中率会高很多:

export default defineConfig({
  build: {
    target: 'es2020',
    sourcemap: false,           // 上传到监控平台,别跟着发布包走
    chunkSizeWarningLimit: 800,
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (!id.includes('node_modules')) return;

          // 框架层:几乎不变,长期缓存
          if (/[\\/](vue|vue-router|pinia|@vue)[\\/]/.test(id)) return 'vendor-core';

          // 图表单独一块:体积大但改动少
          if (id.includes('echarts') || id.includes('zrender')) return 'vendor-chart';

          // 其余三方依赖
          return 'vendor';
        },
        chunkFileNames: 'assets/[name]-[hash].js',
        entryFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash][extname]',
      },
    },
  },
});

这里的关键是分层依据更新频率,而不是体积。vendor-core 可能半年不动一次,用户就能一直吃缓存;业务代码天天变,单独切出来,发布后只有它会失效。

顺手做掉的小事

export default defineConfig({
  build: {
    cssCodeSplit: true,        // 按页面拆 CSS,首屏少下几百 KB
    reportCompressedSize: false, // 大项目关掉,构建快一截
    minify: 'esbuild',         // 比 terser 快得多,压缩率差距很小
  },
});

还有一条不在配置里但很值钱:检查 package.json 里的 @types/* 是否被误装成了 dependencies。类型包进生产依赖除了拖慢安装没有任何好处。

结果

指标优化前优化后
dev 冷启动40.2s3.1s
热更新3.4s0.4s
首屏 JS(gzip)1.9MB480KB
构建耗时96s38s

复盘

这四步的性价比排序非常明确:先解决 barrel 文件,再补依赖预构建,然后按需引入,最后才是分包。前两个改动量最小、收益最大,而且不需要理解打包器的内部机制。

另外,别把 --force 当成日常工具。它每次都会清掉预构建缓存,用得越多越慢。