Vue 3项目死代码排查:从打包分析到按需引入的完整实践
2026/9/23 11:55:33 网站建设 项目流程

一个 Vue 3 业务项目只用了 3 个组件,打包产物却多出 1.2MB 死代码。这个问题通常在开发阶段看不出来,真正难受的是上线前:首包变大、并行请求变多、性能预算突然超支。很多人第一反应是“组件库太大”,但在大多数情况下,大不是重点,重点是无效代码被打进了产物。

我按实际排错路径拆一遍。先讲清死代码为什么会存在,再说怎么用打包分析报告定位,接着给两种按需引入方案,最后聊 tree shaking 生效需要满足哪些条件,以及上线前怎么验收体积变化。这套流程对 Vue 3 组件库基本通用,不管用的哪种组件库,思路一致。

1. 先定位 1.2MB 死代码到底藏在哪些位置

死代码不会凭空出现。1.2MB 的增量通常来自四个位置:组件全量注册、样式全量引入、图标库全量打包、公共依赖被连带保留。这四个原因可以同时存在,也可以只中一个。先逐个确认,再动手改,比急着调打包配置更有效。

1.1 全量注册组件库:最大号的死代码来源

很多 Vue 3 项目沿用 Vue 2 时代的习惯,在入口处直接挂整个组件库:

import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp(App) app.use(ElementPlus)

这段代码会把整个组件库注册到 Vue 实例上。即使业务模板里只写了 3 个组件,构建时组件库内部的所有组件、指令、插件模块,都会进入产物。

为什么构建工具不能把“注册了但没被模板使用”的组件剪掉?因为app.use(ElementPlus)是一个函数调用,插件内部会对组件、指令、服务方法做批量注册,构建工具只能看到参数对象,无法追踪注册过程中到底使用了哪些导出。没有静态线索,就宁可多保留,也不能剪错,否则运行时一调用就报错。

如果你在项目里同时有多个页面文件,情况会更复杂。组件库自身的内部共享逻辑,比如弹层、popper、日期计算、虚拟滚动,会被很多内部组件引用。构建工具发现这些公共模块有多个引用点时,通常会把它们打包成一个公共 chunk。即使业务里只用按钮,按钮内部引用的公共基础模块也会跟着进来,这部分不算完全的死代码,但体积占比很大。

所以“只用了 3 个组件”是业务层面的使用情况,构建层面的模块依赖可能已经牵动了大半个组件库。这是第一个要确认的点:入口是不是全量app.use

1.2 样式全量引入:体积往往比组件还大

组件代码按需了,样式却容易漏。很多组件库的文档会给出两种选择:全量主题 CSS,或者按组件拆分样式。一些团队图省事,在main.js里写:

import 'element-plus/dist/index.css'

或者用自动按需插件之后,又在某个全局样式里保留了一行全量主题文件。这样组件逻辑可能只打进 100KB,样式却把整个库的 CSS 都带进来了。

CSS 文件的特点是不能像 JS 那样做精确的 tree shaking。普通 CSS 里没有模块作用域,构建工具很难判定哪些类名永远不会出现在 HTML 里。虽然一些现代构建工具能对 CSS 做 unused 删除,但组件库的 CSS 通常和主题变量、工具类、动画状态混在一起,风险高,优化效果有限。

所以样式这一步必须显式按需。不同组件库的按需路径不同,有些叫theme-chalk,有些在es/components/*/style下。最好以组件库官方文档为准,不要自己猜路径。

1.3 图标库和内置依赖被一起打包

还有一个很大的坑:图标全量注册。以 Vue 3 组件库常用的图标包为例,有人会在入口写:

import * as Icons from '@element-plus/icons-vue' for (const [name, component] of Object.entries(Icons)) { app.component(name, component) }

这段代码会把几百个图标全部注册成全局组件。图标每个都很小,但合在一起可能比三个业务组件大得多。更重要的是,图标组件会引用统一的 Svg 基础组件、属性解析逻辑,这些公共依赖也会被一起包进去。

一些高级组件内部还会带独立的第三方库。比如日期选择器内部可能依赖 date-fns 或 dayjs,表格组件可能带有自己的虚拟滚动逻辑。如果这个组件本身没有被使用,按需应该能把它们排除;但如果组件库入口存在export * from './components'之类的聚合导出,构建工具不一定能安全地把未使用组件删干净。结果就是,一个没用到的表格组件,连带它的日期依赖、递归菜单依赖,全都出现在产物里。

1.4 组件库自身的副作用标记与模块格式问题

死代码还和模块格式、副作用标记有关。ES 模块的 import 是静态的,构建工具可以看到引用了哪些导出,这是 tree shaking 的前提。但如果组件库走的是 CommonJS 入口,或者业务侧被某个插件转成了 CommonJS,构建工具就只能做整体引用,无法细粒度删除。

另一个关键是sideEffects字段。这个字段告诉构建工具:哪些文件是有副作用的,不能随便删。组件库通常需要把 CSS 文件标记为副作用,否则样式会被误删。但如果一个组件库没有把组件入口标记成可安全清理,或者在 publish 时链路里混进了非 ESM 的目录,tree shaking 就会失败。

排查时可以打开组件库的package.jsonsideEffects字段,再确认安装包里eslib目录是否存在。不要只依赖文档判断,因为多数情况下问题出在依赖树里某个中间包把模块格式改掉了。

2. 用打包分析报告代替猜测

遇到死代码问题,最怕按感觉改。一会把插件去掉,一会加参数,改到最后可能只是把问题从一个 chunk 挪到另一个 chunk。我更建议先做一次分析报告,把死代码分布看清楚,再决定怎么改。

2.1 哪种报告更容易看出死代码分布

Vite 项目可以用rollup-plugin-visualizer,Webpack 项目可以用webpack-bundle-analyzer。这两个工具的产出都是 HTML 文件,打开后能看到每个 chunk 里各模块的占比。

以 Vite 项目为例,先安装依赖:

npm i -D rollup-plugin-visualizer

然后在vite.config.ts里临时加一段配置:

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', gzipSize: true, brotliSize: true }) ] })

重新执行npm run build,打开dist/report.html,你会看到每个 chunk 的树状模块占比。重点看:

  • 组件库 chunk 里有没有出现你没用到的高级组件。
  • 图标相关模块是否集中在一处。
  • CSS 文件是否和组件库同名且体积很大。
  • 有没有“公共依赖”模块包含日期处理、工具函数、防抖节流这类基础逻辑。

Webpack 项目,尤其是 Vue CLI 创建的项目,可以直接用:

vue-cli-service build --report

之后在dist目录下生成 report.html。它基于 webpack-bundle-analyzer,效果类似。

2.2 快速查看产物构成:三个最容易忽视的数据

分析报告信息很多,一眼看过去容易迷失。我一般先只盯三个数字。

第一个是总包未压缩大小和 gzip 后大小。很多人把未压缩大小当成线上传输大小,会高估问题;反过来只看 gzip,又会低估 CSS 的影响。两个都记下来,再看中间的差距。

第二个是组件库在产物中的体积占比。如果组件库 chunk 占比超过 20%,并且业务代码很小,那大概率有全量引入或聚合导出问题。

第三个是 CSS 文件数量与大小。正常情况下按需样式会拆成多个小 CSS 文件,或者合并成一个体积可控的 CSS。如果 CSS 里出现了大量未使用组件类名,说明样式入口仍有全量引入。

为了方便对比,可以建一张小表格:

指标记录值判断标准
总包未压缩大小例如 2480KB趋势下降说明有效
gzip 后总大小例如 680KB业务包压缩率更高
组件库 chunk例如 1200KB应远小于全量引入
CSS 总大小例如 900KB按需后应明显下降
异步 chunk 数量例如 12 个死代码常藏在异步包里

2.3 改配置前的基线记录

不要凭记忆对比。在改动前先执行一次完整构建,记下几个关键数据,再开始改。后续每次重新构建,都回到同一个报告里对比。

如果报告显示组件库 chunk 很大,但业务确实只用几个组件,先检查入口写法和是否有全局图标注册。如果报告显示 CSS 很大,再点开 CSS 对应模块,看类名前缀。

一个容易被忽略的点:某些组件库会把所有组件统一打进lib/index.js,即使你从es目录导入,最终模块也可能是聚合导出。这时无论业务侧怎么按需,依然会有大块死代码。遇到这种情况,要么继续使用官方提供的自动按需插件,要么把组件库升级到更规范维护 ESM 子路径导出的版本。

注意:改配置前一定要留下基线数据,否则体积变大变小都说不清原因。

3. 按需引入的正确落地方式

定位到原因之后,再动手改引入方式。按需引入有两种路线:手动按需和自动按需。少量组件项目用手动,组件数量多且需要长期扩展的用自动。

3.1 手动按需:适合只使用少量组件且不想加插件

手动按需的核心是,从组件库的es目录单独导入对应组件,再单独导入它的样式。

以 Vue 3 组件库为例,在组件文件中局部注册:

import { ElButton, ElInput } from 'element-plus' import 'element-plus/es/components/button/style/css' import 'element-plus/es/components/input/style/css' export default { components: { ElButton, ElInput } }

如果多个页面都用到,可以在main.js里全局注册。注意,这里的全局注册只包含用到的组件,而不是整个库:

import { createApp } from 'vue' import { ElButton, ElInput } from 'element-plus' import 'element-plus/es/components/button/style/css' import 'element-plus/es/components/input/style/css' const app = createApp(App) app.component(ElButton.name, ElButton) app.component(ElInput.name, ElInput) app.mount('#app')

手动按需的最大优势是直观,不会引入额外的构建插件,报错时容易定位。缺点是组件一多,每个文件都要重复写导入,而且样式路径可能因为组件库版本变化而失效。还有一个容易踩的坑:某些组件有依赖样式,比如日期选择器依赖 popper,组件库内部已经处理了大部分,但如果你发现样式缺失,要按文档把对应依赖组件的样式也补上。

如果只使用 3 个组件,我建议先试手动按需。它能直接验证“是不是全量引入导致体积大”这个判断。

3.2 自动按需:用 unplugin-vue-components 处理组件扫描和导入

当项目里组件数量增长,或团队不想维护手动导入列表时,常用方案是unplugin-vue-components。这个插件会在编译阶段扫描模板中使用到的组件名,自动生成导入语句和样式导入。

Vite 项目配置:

// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ vue(), Components({ resolvers: [ElementPlusResolver()] }) ] })

Webpack 项目配置:

// vue.config.js const Components = require('unplugin-vue-components/webpack') const { ElementPlusResolver } = require('unplugin-vue-components/resolvers') module.exports = { configureWebpack: { plugins: [ Components({ resolvers: [ElementPlusResolver()] }) ] } }

配置完成后,模板里直接写<el-button><Button>,插件会自动把它转换成对应组件导入,并按需引样式。

为什么能省体积?因为这个插件生成的是具名导入语句,比如import { ElButton } from 'element-plus',同时自动加样式路径。相比全量app.use,构建工具能顺着具名导入做 tree shaking,把没用到的组件排除掉。

需要注意,这个插件只处理模板中出现过的组件。像ElMessageElNotification这类通过 JS 调用的组件,模板中不会出现,所以还需要配合unplugin-auto-import

import AutoImport from 'unplugin-auto-import/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }) ] })

3.3 样式也要按需,不能只按需组件

自动导入插件通常会通过 resolver 自动引样式,但前提是组件库的 resolver 配置正确。如果发现样式还是全量,最常见的原因是:main.js里还留着import 'dist/index.css';或者你在全局样式中引用了组件库主题变量,而主题变量又连带引用了全量样式入口。

改成按需后,样式会被拆成多个小文件,构建工具最终合并成一个或多个 CSS chunk。体积下降是一方面,另一方面也能减少全局类名冲突。

有一种情况要单独处理:自定义主题。如果业务需要修改组件库的主题色,而组件库的样式入口是 scss 变量,按需引入后会发现主题变量没有被正确注入。此时需要配置额外的样式预处理器参数,或者在业务侧覆盖对应的 CSS 变量。不同组件库做法不同,建议查官方主题定制文档,不要只靠全量 CSS 覆盖。

3.4 全局组件、指令和函数式调用的特殊处理

自动按需并不等于完全无脑。模板里扫描不到的地方,仍然要手动处理。

第一类是全局指令。比如v-loadingv-permission,它们不会作为组件出现在模板里,自动导入插件一般不会帮你注册。需要手动从组件库导入指令,并在app.directive注册,同时保留对应指令的样式。

第二类是动态组件。如果写成<component :is="currentComponent" />,而currentComponent的值来自字符串,构建工具无法在编译阶段知道它对应哪个组件。自动导入插件扫描不到,运行时就会出现组件未注册或样式丢失。稳妥做法是把动态组件映射成一个显式对象,或者手动注册那些可能出现的组件。

第三类是函数式调用。像消息提示、对话框确认这类 API,用auto-import可以自动引,但如果项目里只用一两个,也可以手动 import 并单独引样式。关键是不要忽略它们的样式入口,否则按需之后按钮正常,弹窗却没有背景色和动画。

注意:按需引入不是只按

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询