Vite 构建优化与代码分割:从 chunk 策略到加载性能基线

系统拆解 Vite 生产构建优化:构建流程与 esbuild/Rollup 分工、Tree Shaking 原理与 sideEffects、manualChunks 代码分割策略、资源内联与预加载、gzip/brotli 压缩对比、性能基线指标,以及常见构建问题的排查。

引言

开发体验的「快」由 esbuild 与原生 ESM 带来,而生产质量的「优」则由 Rollup 构建管线兑现。很多项目「能跑」但首屏缓慢、加载过多请求、chunk 巨大——根因往往是对构建优化策略的理解不足。Tree Shaking 不是默认魔法,代码分割也不是随便写个 manualChunks 就完事。

本文从 Vite 构建流程讲起,拆解 Tree Shaking 的生效条件、manualChunks 的四种拆分策略、资源内联与预加载、gzip/brotli 压缩对比,最后给出可落地的性能基线与排查思路。读完你将能为自己的项目制定一套「有理有据」的构建优化方案。

前置:https://plumephp.com/vite-config-guide/(build 配置块)与 https://plumephp.com/vite-plugin-development/(用插件做深度定制)。


目录


1. 构建流程:esbuild 与 Rollup 的分工

1.1 双引擎分工

生产构建 (vite build):
  1. 依赖预构建(dev 已缓存,build 复用)
  2. 模块图遍历 + 代码转换(Vite 插件 + esbuild 转译 TS/JSX)
  3. Rollup 打包:tree shaking、合并、chunk 拆分
  4. 产物压缩:esbuild minify(默认)或 terser

1.2 关键点

  • esbuild 负责转译(TS/JSX → JS),Rollup 负责打包(模块合并、Tree Shaking、chunk)。
  • 转换是「按需 + 增量」,打包是「全量 + 静态分析」。
  • minify 默认为 esbuild(更快),terser(更小但慢)。

1.3 为什么分两步

转译是语法层面的(快)
打包是模块层面的(需完整依赖图)
两者解耦:转译可缓存,打包可精细控制

2. Tree Shaking:为什么不是默认魔法

2.1 什么是 Tree Shaking

构建时通过静态分析,删除未被引用的导出,避免把整库打进产物:

// 源码:只用到了 add
import { add, subtract } from './math'
// 若 subtract 未被使用,Rollup 会将其从产物中移除

2.2 生效条件

条件说明
ESM 静态 importimport { x } from 'pkg' 可分析
CJS/require()动态 require 无法静态分析,通常不摇树
package 的 sideEffects 声明sideEffects: false 允许整体移除
纯函数副作用识别有副作用的模块不会移除

2.3 库作者的 sideEffects

// 库 package.json
{
  "sideEffects": false
}
// 如果某些文件有副作用,需要精确声明
{
  "sideEffects": ["./src/styles.css", "*.css"]
}

2.4 常见「摇不掉」的原因

1. 依赖是 CJS(CommonJS)——需要 esbuild 转 ESM 后才有摇树机会
2. 模块有顶层副作用(调用全局、写文件)
3. 库未声明 sideEffects
4. 使用命名空间 import(import * as ...)限制摇树粒度

3. manualChunks:代码分割策略

3.1 默认行为

Vite/Rollup 默认按入口与动态 import 拆分 chunk,但第三方依赖常常被合并进同一个 vendor chunk,导致单文件过大。

3.2 显式 manualChunks

import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 按包名分组
          if (id.includes('node_modules')) {
            if (id.includes('react')) return 'vendor-react'
            if (id.includes('lodash')) return 'vendor-lodash'
            if (id.includes('three')) return 'vendor-three'
            return 'vendor-other'
          }
        },
      },
    },
  },
})

3.3 常见拆分策略

策略做法适用
按依赖拆分react/vue 单独 chunk缓存友好(依赖不变则 chunk 不变)
按路由拆分每页一个 chunk + 动态 import首屏只加载当前页
按体积拆分超大库(three.js)独立避免影响首屏
合并小依赖过多小 chunk 时合并减少请求数

3.4 拆分的权衡

拆太细 -> 请求多、HTTP 开销大
拆太粗 -> 单 chunk 过大、首屏慢
目标:在「请求数」与「体积」之间找平衡

4. 动态 import 与按需加载

4.1 代码里用动态 import

// 路由级懒加载(Vue Router)
const UserPage = () => import('./pages/UserPage.vue')

// 条件加载(图表等重库)
async function loadChart() {
  const { createChart } = await import('echarts')
  createChart(el, data)
}

4.2 效果

首屏只加载入口 + 必要 vendor
懒加载模块 -> 生成独立 chunk -> 触发时异步请求

4.3 配合 preload

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 关键模块生成 preload 提示
        // 浏览器会在空闲时预取这些 chunk
      },
    },
  },
})

5. 资源内联与预加载

5.1 内联小资源

export default defineConfig({
  build: {
    // 小于 4KB 的资源转 base64 内联,减少请求
    assetsInlineLimit: 4096,
  },
})

5.2 首屏关键资源预加载

<!-- 产物中的 <link rel="modulepreload"> 由 Vite 自动生成 -->
<link rel="modulepreload" href="/assets/vendor-react.abc.js" />
<link rel="modulepreload" href="/assets/index.def.js" />
  • modulepreload:让浏览器尽早开始加载关键 chunk。
  • Vite 会自动为入口 chunk 生成 modulepreload 链接。

5.3 手动 preload 的取舍

方式优点缺点
全部 preload后续点击秒开首屏带宽占用增加
仅关键 chunk平衡首屏与体验需人工分析

6. gzip 与 brotli:压缩对比

6.1 压缩级别与收益

算法压缩比解压速度体积(典型 JS)
不压缩——100 KB
gzip(level 6)约 70%快~35 KB
brotli(level 11)约 75%略慢~28 KB

6.2 在构建时预生成 .gz/.br

// vite-plugin-compression 或自定义
export default defineConfig({
  plugins: [
    compression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 10240, // 10KB 以上才压缩
    }),
  ],
})

6.3 部署层协商

CDN/Web 服务器根据 Accept-Encoding 选择:
  浏览器发 Accept-Encoding: br -> 返回 .br
  不支持 br -> 回退 gzip -> 再回退原文件
注意:预压缩文件需要服务器静态托管支持

7. 性能基线指标

7.1 需要关注的数字

指标合理基线说明
入口 JS< 150 KB(gzip)首屏主 bundle
vendor chunk可控且稳定可缓存、不常变
总请求数< 20(首屏)含静态资源
LCP< 2.5s用户可感知速度
chunk 数适中(非碎片化)5~15 合理

7.2 量化检查

# 构建产物体积
ls -lh dist/assets/*.js

# 用 vite build 的默认警告
# 超过 chunkSizeWarningLimit(默认 500KB)会提示

7.3 设置自己的警告阈值

export default defineConfig({
  build: {
    chunkSizeWarningLimit: 600, // 提示超过 600KB 的 chunk
  },
})

8. 常见构建问题排查

8.1 问题清单

现象原因解决
构建产物巨大未摇树 / 依赖过多检查 sideEffects、精简依赖
大量小 chunk过度动态 import合并或调整拆分粒度
首屏请求过多preload 全开 / 资源零内联调整 modulepreload 与 inlineLimit
无 .gz/.br 文件未配置压缩插件构建期生成压缩文件
特定依赖无法摇树CJS 依赖用 optimizeDeps 或升级库

8.2 用插件输出体积报告

// 接入可视化分析(如 vite-plugin-visualizer / rollup-plugin-visualizer)
import { visualizer } from 'rollup-plugin-visualizer'

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

8.3 定位重复依赖

# 检查依赖树
npm ls lodash

# 若出现不同版本,考虑 resolve.dedupe
export default defineConfig({
  resolve: {
    dedupe: ['react', 'lodash'],
  },
})

9. 总结:优化决策框架

9.1 一套可复用流程

1. 先量化(体积/请求数/加载时间)
2. 再拆 chunk(vendor + 路由级动态 import)
3. 再缩体积(Tree Shaking + 压缩 + 内联)
4. 最后调加载(preload + CDN + 缓存)

9.2 优化优先级

优先级动作收益
P0路由级懒加载首屏显著变快
P1vendor chunk 稳定缓存命中率提升
P2构建期 gzip/brotli传输体积下降
P3Tree Shaking 治理总包体积减小

9.3 自检清单

检查项是否掌握
能解释 Tree Shaking 生效条件☐
能配置 manualChunks 拆分☐
能落地路由级动态 import☐
能解释 gzip/brotli 与协商☐
能为项目设定性能基线☐

延伸阅读

  • https://plumephp.com/vite-config-guide/ — build 配置块详解
  • https://plumephp.com/vite-plugin-development/ — 用插件实现压缩/分析
  • https://plumephp.com/frontend-vite-deep-dive/ — 模块图与依赖预构建原理
  • Vite 构建与生产环境文档 — 官方构建指南
  • Rollup 输出选项文档 — manualChunks 完整参考

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「vite」更多文章

  1. Vite 静态资源与媒体资产处理:图片、字体、SVG 与 Worker
  2. Vite 环境变量与生产构建最佳实践:import.meta.env、构建模式与产物优化
  3. Vite 浏览器兼容与 Legacy 构建:build.target、Polyfill 与兼容插件