
最近不少人在业务项目中遇到一个很典型的问题Vue 3 项目里明明只用了组件库的 3 个组件但打包后的产物却多了 1.2MB。而且项目本身逻辑并不复杂这多出来的体积到底是哪里来的不是已经用了 Vite 吗Vite 不是默认支持 tree-shaking 吗为什么还有这么多死代码被打进产物里这篇文章会把问题完整拆开先讲清楚组件库体积和死代码的来源再通过一个最小可复现工程演示从全量引入到按需引入的完整改造过程最后给出常见问题的排查思路和工程化建议。无论你是刚接触 Vue 3 的开发者还是已经在业务项目中维护前端工程的老手都能从中找到可以参考的配置方案。1. 问题背景只用了 3 个组件产物却多了 1.2MB1.1 你遇到的现象可能比标题更隐蔽先描述一下这类问题最典型的现场。业务项目技术栈是 Vue 3 Vite TypeScript。页面里只使用了组件库中的按钮、输入框、弹窗这三个组件。最开始为了省事直接在main.ts里全量注册组件库import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue createApp(App).use(ElementPlus).mount(#app)模板中也确实只写了三个组件template div el-button typeprimary clickvisible true打开弹窗/el-button el-input v-modelkeyword placeholder请输入关键词 / el-dialog v-modelvisible title提示 p业务弹窗内容/p /el-dialog /div /template script setup langts import { ref } from vue const visible ref(false) const keyword ref() /script看起来代码量很小但执行npm run build后dist目录体积直接达到 1.2MB 以上。更离谱的是如果打开浏览器控制台会发现根本没用到组件库里的DatePicker、Table、Form等组件但它们对应的代码依然被打进了包里。这种情况并不是个例。很多团队在项目初期为了“开发快一点”习惯性地全量引入组件库。等到业务迭代到中后期产物体积越来越大才开始回头排查体积问题。这时候往往已经需要重构大量入口代码了。1.2 什么是死代码为什么它会进入产物“死代码”这个词听起来很抽象但放到这个场景里其实很直观被打进最终产物、但在运行时永远不会被执行的 JavaScript 代码都可以算作死代码。死代码通常来自两种途径开发者手动引入了某个模块但代码里根本没有使用它。模块虽然被引用但打包器无法静态分析出“这个模块可以被安全删除”于是保守地把代码保留下来。第一种情况相对容易发现第二种情况才是组件库体积问题的重灾区。一个组件库的完整入口文件会把所有组件的导出都集中在一个或多个模块里。如果你通过import ElementPlus from element-plus这种全量方式引入代码实际上引用了入口模块里的全部组件导出。即使业务代码里只用了 3 个组件的标签打包器看到的是“入口模块被完整加载了”。死代码进入产物后带来的影响不只是“文件体积变大”这么简单。浏览器需要下载更大的 JS 文件移动端低网速场景会明显增加白屏时间同时 JavaScript 模块在执行前还需要经过解析、编译即使这些代码永远不会执行也要占掉一部分解析成本。这也是为什么很多团队在做性能优化时会特别关注构建产物中“未使用模块”的占比。1.3 组件库的体积是怎么膨胀起来的主流 Vue 3 组件库例如 Element Plus、Naive UI、Ant Design Vue、Vant都是组件数量非常庞大的工程。Element Plus 有几十个基础组件加上指令、工具函数、常量、图标整体代码量非常可观。假设一个组件库完整的编译后代码在 1MB 到 2MB 之间那全量引入时这 1MB 多就会全部进入你的业务产物。而真正被业务用到的部分可能只有几十 KB 到一两百 KB。所以标题里说的“1.2MB 死代码”本质上不是组件库真的给你注入了 1.2MB 垃圾代码而是你的引入方式没有让 tree-shaking 发挥作用导致组件库把完整的代码包全部交给了打包器。这里要区分一个概念不是所有多出来的体积都能叫“死代码”。Vue 运行时本身、业务依赖的第三方库都属于必要体积。但组件库中未被使用的组件确实是典型的死代码来源。2. 环境准备与复现一个最小工程2.1 项目基础信息为了把问题讲得更具体这里建议用一个最小工程来复现。版本信息以当前主流环境为例实际项目可以按自己的版本调整名称版本示例说明Node.js18.x 或 20.xVite 5 需要 Node 18Vite5.x构建工具Vue3.4.x核心框架Element Plus2.x示例组件库TypeScript5.x可选但推荐启用这里用 Element Plus 作为示例组件库因为它的体积问题和按需引入方案都比较有代表性。其他组件库的处理思路是相通的。2.2 初始化 Vue 3 Vite 工程先创建一个新项目npm create vitelatest vue3-components-demo -- --template vue-ts进入项目并安装依赖cd vue3-components-demo npm install安装 Element Plusnpm install element-plus项目结构大概如下vue3-components-demo/ ├── src/ │ ├── components/ │ ├── App.vue │ ├── main.ts │ └── vite-env.d.ts ├── index.html ├── package.json ├── tsconfig.json └── vite.config.ts注意这里不额外安装路由、状态管理等依赖以便更清晰观察组件库对产物体积的影响。2.3 用全量导入复现体积问题先按最常规的方式在src/main.ts中全量引入组件库import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue const app createApp(App) app.use(ElementPlus) app.mount(#app)然后修改src/App.vue写一个只用到三个组件的页面template div stylepadding: 24px el-button typeprimary clickvisible true打开弹窗/el-button el-input v-modelkeyword placeholder请输入搜索关键词 / el-dialog v-modelvisible title提示 p这是全量引入时被打包的组件/p /el-dialog /div /template script setup langts import { ref } from vue const visible ref(false) const keyword ref() /script执行构建命令npm run build构建结束后终端会输出类似下面的信息dist/index.html 0.48 kB dist/assets/index-xxx.js 1.28 MB / gzip: 320.xx kB dist/assets/index-xxx.css 320.xx kB这么大的 JS 产物对于一个只用了 3 个组件的业务页面来说是非常不正常的。你可以把dist目录里的 JS 文件复制到文本编辑器里搜索ElDatePicker、ElTable等组件名大概率能搜到相关字符串或钩子函数。这说明整个组件库几乎都被打包进去了。3. 拆解为什么用了 Vite 还是打包出大量死代码3.1 tree-shaking 是如何工作的要理解问题必须先理解 tree-shaking。tree-shaking 最早由 Rollup 提出并普及后来 Webpack 也支持了类似能力。Vite 在生产构建时使用的就是 Rollup因此也继承了 tree-shaking。tree-shaking 的核心逻辑是在模块的静态分析阶段找到“被引入但从未被使用”的导出然后在生成产物时把它们的代码删除。听起来很简单但实际有几个前提代码必须使用 ES Moduleimport/export语法。CommonJS 的require是动态的打包器很难静态分析。模块必须没有“副作用”。如果某个模块在被 import 的时候会执行副作用代码打包器就不能轻易删除它。入口引用不能太“黑盒”。如果主入口文件把所有导出集中在一个对象里那么即使你没用某个导出只要这个对象被整体引用了打包器就会认为它们都被使用。3.2 app.use(ElementPlus) 对 tree-shaking 的破坏那为什么上面的全量引入会让 tree-shaking 失效关键就在app.use(ElementPlus)这段代码。组件库的入口在内部其实长得很像这样以下是结构示意并非源码// element-plus 入口结构示意 import Button from ./button import Input from ./input import Dialog from ./dialog // ... 其他几十个组件 const components [ Button, Input, Dialog, // ... ] const install (app) { components.forEach((component) { app.component(component.name, component) }) } export { Button, Input, Dialog, // ... } export default { install, // ... }当你写下app.use(ElementPlus)时打包器会认为install函数被执行了。install内部遍历了components数组并逐个调用app.component。这导致打包器必须把components数组里的每一个组件都保留下来因为它无法判断“如果不注册某个组件业务会不会报错”。换句话说全量注册操作是在强制打包器保留所有组件代码。即使你的模板里只写了el-button其他几十个组件依然会被打包进产物。同样地import ElementPlus from element-plus会加载组件库的默认导出而默认导出对象上挂载了大量属性和组件也可能阻断 tree-shaking 的进一步优化。3.3 sideEffects 与“副作用”标记除了入口结构还有一个重要概念是sideEffects。在库的package.json中可以声明sideEffects字段。它的作用非常简单告诉打包器这个包里哪些文件是纯模块哪些文件包含副作用。如果某个库配置了{ sideEffects: false }就相当于对打包器说“我这个包里的所有模块都是纯的没有被引用就可以放心删除。”如果配置成数组{ sideEffects: [ **/*.css, **/dist/* ] }意思是除了 CSS 等特殊文件之外其余 JS 模块都可以参与 tree-shaking。问题在于不同组件库、不同版本的package.json对sideEffects的声明并不完全一致。有些老版本组件库没有正确声明或者入口文件结构复杂导致打包器无法安全地删除未使用模块。这也是为什么有时候你以为“按需导入了”体积却还是不太理想。所以在改造体积时除了看业务代码的引入方式也需要检查组件库本身的package.json排查配置是否合理。4. 完整实操从 1.2MB 死代码到按需打包这一节给出三种由浅入深的改造方式。4.1 方式一手动按需导入组件和样式最直接的方式是放弃全量注册改为只从组件库中导入本次用到的组件。修改src/main.tsimport { createApp } from vue import App from ./App.vue createApp(App).mount(#app)修改src/App.vuetemplate div stylepadding: 24px el-button typeprimary clickvisible true打开弹窗/el-button el-input v-modelkeyword placeholder请输入搜索关键词 / el-dialog v-modelvisible title提示 p手动按需导入 Demo/p /el-dialog /div /template script setup langts import { ref } from vue import { ElButton, ElInput, ElDialog } from element-plus import element-plus/es/components/button/style/css import element-plus/es/components/input/style/css import element-plus/es/components/dialog/style/css const visible ref(false) const keyword ref() /script在这个写法中script setup会自动把导入的组件注册到当前页面的局部作用域。模板里的el-button会被解析为ElButton。注意这里显式导入了组件对应的样式文件import element-plus/es/components/button/style/css这是因为 Element Plus 的组件逻辑和样式是分离的。如果不导入样式组件虽然能渲染但会出现无样式的问题。修改完成后重新构建npm run build产物 JS 体积通常能降到几百 KBCSS 体积也会明显下降。具体数字取决于组件库版本和项目依赖但至少不会再出现 1.2MB 这种情况。不过手动按需有一个明显缺点每个页面用到新组件时都要手动写两条 import。开发体验不够好而且容易漏掉样式。4.2 方式二unplugin-vue-components 自动按需导入在实际工程中更推荐用unplugin-vue-components搭配unplugin-auto-import实现自动按需引入。先安装依赖npm install -D unplugin-auto-import unplugin-vue-components修改vite.config.tsimport { defineConfig } from vite import vue from vitejs/plugin-vue 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()], dts: src/auto-imports.d.ts, }), Components({ resolvers: [ElementPlusResolver()], dts: src/components.d.ts, }), ], })这是 Vite 项目中最常见的自动导入配置。AutoImport负责自动导入组件库暴露出来的 API例如ElMessage、ElNotification这类方法。Components负责自动导入模板中使用的组件标签。配置完成后App.vue可以恢复成非常干净的写法template div stylepadding: 24px el-button typeprimary clickvisible true打开弹窗/el-button el-input v-modelkeyword placeholder请输入搜索关键词 / el-dialog v-modelvisible title提示 punplugin 自动按需导入 Demo/p /el-dialog /div /template script setup langts import { ref } from vue const visible ref(false) const keyword ref() /script模板里的el-button、el-input、el-dialog会被插件自动解析并生成对应导入语句。如果开发环境开启了 TypeScriptdts配置会自动生成类型声明文件保证编辑器不报红。需要注意的是unplugin-vue-components默认的ElementPlusResolver会自动处理样式按需导入所以不需要再手动写样式导入语句。重新执行构建体积和方式一基本一致但开发体验好很多。4.3 方式三用构建体积分析器定位剩余体积按需引入改造完成后推荐用可视化分析工具确认体积变化并检查还有没有其他大体积依赖。安装rollup-plugin-visualizernpm install -D rollup-plugin-visualizer在vite.config.ts中加入插件import { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers import { visualizer } from rollup-plugin-visualizer export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()], dts: src/auto-imports.d.ts, }), Components({ resolvers: [ElementPlusResolver()], dts: src/components.d.ts, }), visualizer({ open: true, gzipSize: true, brotliSize: true, }), ], })再次执行npm run build构建完成后会自动打开一个stats.html页面里面展示了每个 chunk 的大小和引用关系。通过这个页面可以清楚看到哪些代码来自业务模块。哪些依赖被打了进来。组件库实际被打包的部分有多大。在我见过的实际项目中按需引入改造后组件库相关代码通常会降到几十 KB 到一两百 KB 的级别。如果再配合分包和懒加载整体产物会再次明显下降。4.4 三种方式对比方式体积优化效果开发体验推荐度全量引入 app.use差1MB 死代码好什么都不用管不推荐手动按需导入好只保留使用组件一般每页都要写导入和样式适合极简单页面自动按需导入好和手动按需效果一致最好标签直接使用强烈推荐5. 常见问题与排查思路5.1 自动导入不生效组件还是未定义现象配置好unplugin-vue-components后模板中使用el-date-picker等组件构建不报错但运行时控制台提示组件未注册。排查思路检查插件是否放在了vite.config.ts的plugins数组中并且vue()插件之后。检查组件名是否匹配。自动导入依赖模板中的标签名比如el-table对应ElTable如果你写的是自定义标签my-table插件无法自动匹配。检查dts文件是否正常生成。如果找不到生成的components.d.ts说明插件没有正确工作。确认组件库解析器名称正确。Element Plus 用ElementPlusResolverVant 用VantResolver不要混用。5.2 组件正常但样式丢失现象按需引入后组件能渲染但完全没有样式。排查思路检查是否还保留了全量样式import element-plus/dist/index.css。如果保留会覆盖按需样式。确认ElementPlusResolver的默认importStyle行为。绝大多数情况下不需要手动配置但如果你手动指定了importStyle: sass需要保证项目里安装了对应的 Sass 依赖。如果是手动按需导入检查样式路径是否正确。Element Plus 的样式路径通常是element-plus/es/components/xxx/style/css。5.3 改成按需引入后体积仍然很大现象已经改成按需引入但产物还是有好几 MB。排查思路使用rollup-plugin-visualizer分析体积分布先找出最大的 chunk。如果组件库体积依然很大检查是否在使用ElMessage等功能性 API 时又引用了完整组件库入口。查看项目里是否还有其他大型依赖比如图表库、日期处理库、Excel 处理库它们可能才是体积大头。检查package.json中依赖的版本。如果一个组件库发布时没有正确处理sideEffects即使按需导入体积也可能不理想。可以尝试升级到较新版本。5.4 Webpack 项目怎么处理如果业务项目还在使用 Webpack处理思路类似。Webpack 4 支持 tree-shaking但需要确保项目配置了正确的模块解析方式。对于按需引入可以借助babel-plugin-import实现组件和样式按需加载// babel.config.js module.exports { plugins: [ [ import, { libraryName: element-plus, customStyleName: (name) { return element-plus/es/components/${name}/style/css }, }, ], ], }不过更推荐在 Webpack 项目中也使用unplugin-vue-components的 Webpack 版本// webpack.config.js const AutoImport require(unplugin-auto-import/webpack) const Components require(unplugin-vue-components/webpack) const { ElementPlusResolver } require(unplugin-vue-components/resolvers) module.exports { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], }这些插件都同时支持 Vite 和 Webpack一套配置逻辑可以迁移使用。6. 组件库按需引入的工程化最佳实践6.1 从项目初始化阶段确定引入规范组件库体积问题之所以经常到项目中期才爆发是因为很多项目在初始化时选择全量引入想着“后面再优化”。结果后面根本没有人去优化。建议从第一天起就确定引入规范新项目默认使用自动按需导入方案。不在main.ts中写app.use(ElementPlus)这类全量注册代码。组件库版本锁定避免团队中某个成员升级后产生体积回归。把规范写进团队文档或 Code Review 检查清单里。6.2 全局注册 vs 局部注册的选择有些人会问能不能保留全局注册但只注册几个组件从技术上可以import { ElButton, ElInput, ElDialog } from element-plus const app createApp(App) app.component(ElButton.name, ElButton) app.component(ElInput.name, ElInput) app.component(ElDialog.name, ElDialog)这样做的优点是模板中可以继续使用el-button且不需要每个页面手动引入。缺点也很明显组件多了以后main.ts会变得很臃肿而且你依然需要手工维护组件清单。更推荐的做法还是依赖unplugin-vue-components自动生成局部导入。它生成的组件是每个页面局部的不会造成全局命名空间污染代码分割时也更有利。6.3 图标、指令、API 的按需引入很多组件库还包含图标库和自定义指令。这些同样需要注意体积。以 Element Plus 图标为例如果是全量引入import * as ElementPlusIconsVue from element-plus/icons-vue那么所有图标都会被打包。正确做法是只导入用到的图标script setup langts import { Search } from element-plus/icons-vue /script template el-input :prefix-iconSearch / /template对于ElMessage这类方法最好也通过unplugin-auto-import自动导入或者手动按需导入避免写成import ElementPlus from element-plus后直接使用ElementPlus.ElMessage后者会把入口模块整个拉进来。6.4 给产物体积加 CI 校验体积优化不是一次性工作。团队协作中某个人随手加了一个全量引入可能就让产物体积反弹。建议在 CI 中增加产物体积监控。常见方案有两种使用rollup-plugin-visualizer生成统计报告人工在发布前检查。接入size-limit或 GitHub Action 中的 bundle size 自动化检测设置体积阈值超过阈值即构建失败。如果你不想引入额外服务也可以写一个简单脚本在构建后读取dist/assets下 JS 文件的总大小并与上个版本对比超过阈值就自动通知。下面是 Node 脚本思路import { readdirSync, statSync } from fs import { join } from path const assetsDir dist/assets function getFilesSize(dir) { let total 0 for (const name of readdirSync(dir)) { const filePath join(dir, name) const stat statSync(filePath) if (stat.isDirectory()) { total getFilesSize(filePath) } else { total stat.size } } return total } const bytes getFilesSize(assetsDir) const limit 500 * 1024 // 500KB if (bytes limit) { console.error(产物体积 ${bytes} 字节超过阈值 ${limit} 字节) process.exit(1) } console.log(产物体积 ${bytes} 字节未超过阈值)这只是最简版本实际可以按.js和.css区分统计。核心思路是把体积控制变成自动化的关卡而不是靠人肉提醒。7. 总结与自查清单回到最开始的问题Vue 3 组件库业务项目只用了 3 个组件打包却多出 1.2MB 死代码。原因不是 Vite 不支持 tree-shaking而是全量引入和app.use(ElementPlus)这种注册方式让打包器无法安全删除未使用的组件代码。解决思路也很清晰把全量引入改成按需导入优先使用unplugin-auto-import和unplugin-vue-components让组件、样式、API 都以最小粒度进入产物同时用构建体积分析器确认结果。如果你在项目里也遇到产物体积异常偏大的情况可以按下面三步快速排查打开src/main.ts看是否还有全量导入组件库并调用app.use的代码。检查vite.config.ts中是否配置了自动按需导入插件。构建后运行rollup-plugin-visualizer找到体积最大的 chunk对比是否来自组件库。这三步走完绝大多数组件库体积问题都能定位到原因。组件库体积优化的核心说起来很简单不要全量注册让打包器能看清你用了什么剩下的交给 tree-shaking 去处理。把这个习惯固化到项目规范和 CI 流程里产物体积才能真正保持稳定可控。