构建产物优化
🎯 引言
前面几篇我们聊了加载、渲染和缓存的优化,这一篇回到源头:构建产物本身。用户下载的每一个 JS、CSS 文件,都是构建工具打包出来的。产物里混进了多少用不到的代码?哪个依赖占了大头?这些问题不弄清楚,优化就只能凭感觉。
学完本篇,你将能够:
- 用 rollup-plugin-visualizer 给 Vite 项目生成一份可视化的体积分析报告。
- 看懂报告:哪块代码占体积、哪个依赖被打进了包里。
- 掌握四种常用瘦身手段:Tree Shaking、压缩混淆、按需引入、CDN 外链。
- 建立「改一步、量一步」的优化习惯,每一步都用数据验证效果。
Webpack 的 loader、plugin 配置细节在 Webpack 课程里已经讲过,本篇只讲优化思路,示例以 Vite 项目为主。
🧱 先测量再瘦身:产物体积分析
优化界有句老话:没有测量,就没有优化。盲目地拆包、删依赖,可能忙了半天只省了几 KB,真正的「体积大户」却纹丝不动。所以第一步永远是:先搞清楚产物里到底有什么。
Vite 底层用 Rollup 打包,我们可以借助 rollup-plugin-visualizer 这个插件,在构建时生成一份 HTML 报告,把每个文件的体积画成方块图,一眼看出谁占地方。
本课程在 Node.js v22 + Vite 6 + rollup-plugin-visualizer v6 环境下验证通过,先安装插件:
# 本地 Node.js 版本:v22,rollup-plugin-visualizer 版本:v6
pnpm add -D rollup-plugin-visualizer
然后在 vite.config.js 里注册它:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
vue(),
// 构建时生成体积分析报告
visualizer({
filename: 'dist/report.html', // 报告输出位置
open: true, // 构建完成后自动在浏览器打开
}),
],
});
配置就这一处。Vite 配置的基础写法在 Vite 课程里有完整讲解,这里不重复。执行 pnpm run build,构建结束后浏览器会自动打开 report.html:
- 报告里每个方块代表一个模块,方块面积越大,说明它在产物中占的体积越大。
- 方块按目录层级嵌套,你可以顺着
node_modules一路点进去,看清楚每个第三方依赖的实际大小。 - 报告提供 gzip、brotli 两种压缩后体积的切换视图,评估线上传输体积时看 gzip 那一档,更接近真实情况。
拿到报告后,重点回答三个问题:哪个方块大得出乎意料?某个依赖是不是整个被打进来了?同一份代码是不是出现在了多个包里? 这三个问题的答案,就是你接下来瘦身的方向。
dist/report.html 一起部署到线上。✨ 瘦身手段一:Tree Shaking
Tree Shaking(摇树)是一种很基础也很省心的瘦身方式:构建时自动把没有被使用到的代码从产物里剔除掉,就像摇一棵树,枯枝败叶落一地,留下真正有用的部分。
它能生效,靠的是 ES Module(ESM)的静态结构。import / export 在代码运行之前就是确定的,构建工具可以在不执行代码的情况下,静态分析出「哪些导出被谁用了」,没用到的就可以安全删掉。而 CommonJS 的 require 是运行时动态执行的,没法这样分析,这也是现代构建工具都围绕 ESM 设计的原因之一。
想让自己写的代码享受到 Tree Shaking,记住两个习惯:
习惯一:尽量用具名导出,少用默认导出大包。
// 推荐:每个函数单独导出,用不到的不进产物
export function formatDate(date) {
return date.toLocaleDateString();
}
export function formatPrice(num) {
return num.toFixed(2);
}
// 业务代码里只 import 用到的,formatPrice 会被摇掉
import { formatDate } from './utils';
习惯二:避免在模块顶层写「副作用代码」。 副作用指的是模块一被导入就执行的动作,比如修改全局变量、给原型挂方法、发请求。构建工具无法判断这些代码能不能删,为了保险只能保留:
// 顶层直接执行:哪怕没人用,构建工具也不敢删
console.log('模块被加载了');
window.myLib = { version: '1.0.0' };
export function doSomething() {}
把这类初始化逻辑收进函数里,让使用方主动调用,模块就重新变得「可摇」了。
pnpm run build 的产物,别拿 dev server 的体积说事。⚡ 瘦身手段二:压缩混淆与按需引入
压缩混淆简单说两句:生产构建时,构建工具会自动删除空格注释、把变量名缩短成 a、b、合并重复代码。Vite、Webpack 的默认配置已经开启了这一步,你基本不用操心,知道它在默默干活就行。
真正值得你动手的是 按需引入:只用库的一部分,就只打包那一部分。
组件库和工具库是体积大户。以工具库 lodash 为例,它提供了上百个函数,但你一个项目里常用的可能就几个。全量引入和按需引入的写法差别很小,产物体积差别却很大:
// 全量引入:整个 lodash 库都进了产物
import _ from 'lodash';
_.debounce(fn, 300);
// 按需引入:只打包 debounce 这一个函数(lodash-es 是 ESM 版本,支持 Tree Shaking)
import { debounce } from 'lodash-es';
debounce(fn, 300);
组件库同理。Element Plus 这类主流组件库都提供了自动按需引入的方案(借助 unplugin-vue-components 等插件),模板里用到哪个组件,构建时才打包哪个组件的代码和样式,而不是整库照单全收。具体接入步骤官方文档写得很清楚,这里记住思路即可。
-es 后缀或标明 ESM 的版本(如 lodash-es 而非 lodash),ESM 格式才能配合 Tree Shaking 把没用到的部分摇掉。🪤 瘦身手段三:CDN 外链
还有一种思路更「激进」:某些依赖干脆不打进产物,构建时把它排除掉,页面上改用 <script> 标签从公共 CDN 加载。
这种做法叫 externals(外部依赖)。以 Vite 为例,配置 Rollup 的 external 选项:
export default defineConfig({
build: {
rollupOptions: {
// 构建时不打包 lodash-es,代码里的 import 会原样保留
external: ['lodash-es'],
},
},
});
同时在 HTML 里用 script 标签从 CDN 引入对应的全局变量版本。这样产物里就完全没有这个库了,体积直接减掉一大块。
CDN 外链适合什么样的依赖?体积大、版本稳定、更新不频繁的库,比如某些图表库、富文本编辑器。好处有两个:一是你的产物立刻变小;二是公共 CDN 的节点离用户更近,如果用户访问过用同一个 CDN 的其他网站,文件可能已经在浏览器缓存里了,加载近乎零成本。
代价也要想清楚:
- 可用性风险:你的站点从此依赖第三方 CDN 的稳定性,对方挂了,你的功能也跟着挂。
- 失去 Tree Shaking:外链的全局变量版本通常是整库,用不到的部分也一起加载了。
- 版本管理变复杂:npm 包和 CDN 文件的版本要手动对齐,升级时容易顾此失彼。
所以 CDN 外链不是常规手段,而是针对个别「又大又稳」的依赖做的取舍。业务代码、频繁升级的框架,还是老老实实交给构建工具打包。
💡 优化闭环:改一步,量一步
上面讲了四种手段,但比手段更重要的是工作方式:每做一次优化,就重新构建、重新看报告,确认体积真的变小了。
原因很简单:优化的效果常常和直觉不一致。你以为删掉一个依赖能省很多,报告告诉你它其实只占零头;你以为按需引入生效了,报告却发现配置写错、整库还是进来了。没有验证,这些问题你根本发现不了。
推荐的流程是这样的:
- 基线测量:优化前先跑一次构建,把报告里几个大文件的体积记下来,作为对比基准。
- 单步改动:一次只做一项优化(比如先接按需引入),不要几项一起上,否则体积变化对不上号,你分不清是哪一步起的作用。
- 验证对比:重新构建,对照报告确认目标方块确实变小了,再记一笔。
- 回归检查:确认功能没有被改坏(比如 CDN 外链后页面还能正常跑),再进入下一轮。
把这个流程跑上几轮,你对项目的体积构成就心里有数了,这也是性能优化这项工作的基本节奏:测量 → 优化 → 再测量。
🧾 小节总结
- 优化前先测量:用 rollup-plugin-visualizer 生成可视化报告,方块越大占体积越多,评估线上传输看 gzip 体积。
- 看报告回答三个问题:哪个方块大得出乎意料?哪个依赖被整个打进来了?同一份代码是否出现在多个包里?
- Tree Shaking 基于 ESM 静态分析,自动删除未使用代码;配合它的两个习惯是具名导出、避免模块顶层副作用,且只在生产构建时生效。
- 压缩混淆由构建工具默认完成;按需引入(如
lodash-es的具名导入、组件库自动按需方案)才是需要你动手的部分。 - CDN 外链(externals)适合体积大、版本稳定的依赖,代价是可用性风险、失去 Tree Shaking、版本要对齐。
- 养成「改一步、量一步」的习惯:先记基线,单步改动,用报告验证体积变化,再回归功能。
❓ 知识问答
Q1:报告里看到的体积就是用户实际下载的体积吗?
A:不完全是。报告默认显示的是打包后的原始体积,用户实际下载的是经过 gzip 或 brotli 压缩后的体积,两者差距可能很大。评估加载性能时,切到报告的 gzip 视图看更接近真实情况。
Q2:我用了 import _ from 'lodash',为什么 Tree Shaking 没帮我把没用到的函数删掉?
A:两个原因叠加:默认导出的对象(_)上挂了所有函数,构建工具没法判断你用了哪些;而且 lodash 是 CommonJS 格式,不支持静态分析。换成 ESM 版本的 lodash-es 加具名导入就能解决了。
Q3:Tree Shaking 会不会误删我需要的代码?
A:对纯函数式的模块很安全。有风险的是带副作用的模块(比如导入即注册全局样式的 CSS、顶层执行初始化的 JS),构建工具可能误判。遇到这类代码,npm 包通常会在 package.json 的 sideEffects 字段里声明,自己写的代码则尽量把副作用收进函数里。
Q4:CDN 外链和按需引入能不能一起用?
A:思路上是二选一的关系:走 CDN 的库已经不参与打包了,按需引入就无从谈起。所以取舍点是:这个库你只用一点点,按需引入更划算;你用了它大部分功能且它又大又稳定,才考虑 CDN 外链。
Q5:这些手段在 Webpack 项目里一样适用吗?
A:思路完全一样。Tree Shaking、压缩、externals 都是构建工具的通用概念,只是配置写法不同(比如 Webpack 的 externals 是顶层配置项)。报告工具换成 webpack-bundle-analyzer,用法和本篇的 visualizer 类似。
🧪 小练习
给你手头的一个 Vite 项目(没有的话用 pnpm create vite 新建一个)做一次完整的「测量 → 优化 → 验证」:
练习一:安装 rollup-plugin-visualizer 并配置好,跑一次 pnpm run build,打开报告,找出体积占比较大的三个模块,记录下来。
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// 请在这里编写代码:引入 rollup-plugin-visualizer 的 visualizer
export default defineConfig({
plugins: [
vue(),
// 请在这里编写代码:注册 visualizer,设置 open: true
],
});
练习二:在项目里安装 lodash-es(Node.js v22 环境,包版本 4.x),分别用全量引入和具名引入两种方式使用 debounce,各构建一次,对比报告中 lodash 方块的体积变化,体会按需引入的效果。
// 请在这里编写代码:用具名导入的方式引入 debounce,并调用一次
const onInput = () => console.log('input');
🎉 恭喜你已经掌握构建产物优化技能啦!现在你有了分析工具这把「秤」,也学会了四种常用的瘦身手段。下一篇我们将学习性能监控指标,用真实数据衡量你优化后的成果。
