分包是什么
"分包" 就是按 "使用时机" 和 "功能" 将代码分割成多个小文件,核心是 "按需加载",解决传统单包模式下 "体积过大、加载慢" 的问题。
- 路由分包、组件分包、第三方库分包是最常用的三种方式;
- 实现上主要依赖
import()动态导入语法和打包工具(Webpack/Vite)的配置; - 最终目标是让用户 "用什么加载什么",提升页面打开速度和交互体验。
为什么分包能优化性能?
- 减少首屏加载时间:只加载必要代码,缩小初始下载体积;
- 利用浏览器缓存:第三方库、不常更新的代码被缓存,后续访问更快;
- 避免重复加载:多个页面共用的代码(如公共组件)可拆分成 "共享包",加载一次后复用。
分包后的 "加载流程":浏览器如何处理多个包?
-
首屏加载:浏览器下载主包(
app.js)和当前页面必需的分包(如首页路由的home.js); -
解析执行:主包代码先执行,初始化应用(如创建 Vue 实例、配置路由);
-
按需加载:当用户触发某个操作(如跳转路由、点击按钮),需要新的分包时:
- 浏览器通过
import()动态请求对应的分包文件(如order.js); - 下载完成后,执行分包代码并渲染新内容(过程中可显示 "加载中" 提示)。
- 浏览器通过
分包后的三个基本方向
1. 路由分包(最常用):按页面拆分,访问时才加载对应页面代码
2. 组件分包:按组件拆分,用到时才加载大型组件
3. 第三方库分包:将大型依赖单独拆分,利用缓存
1. 路由分包
路由拆分的关键是修改 router/index.js 中 "路由组件的导入方式",将静态 import 改为动态 () => import()。
(1)未拆分的静态导入(反面示例)
所有页面代码会打包到一起,不推荐:
javascript
// router/index.js(未拆分,不推荐)
import Vue from 'vue';
import Router from 'vue-router';
// 静态导入所有路由页面(会全部打包到核心 JS)
import Home from '@/views/Home';
import About from '@/views/About';
import User from '@/views/User';
Vue.use(Router);
export default new Router({
routes: [
{ path: '/', name: 'Home', component: Home },
{ path: '/about', name: 'About', component: About },
{ path: '/user', name: 'User', component: User }
]
});
(2)拆分后的动态导入(正确示例)
每个路由页面会被拆分为独立 Chunk:
javascript
// router/index.js(已拆分,推荐)
import Vue from 'vue';
import Router from 'vue-router';
// 无需静态导入页面,改为动态导入
import ElementUI from 'element-ui'; // 第三方 UI 库(会被拆到 chunk-vendors)
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(Router);
Vue.use(ElementUI);
export default new Router({
routes: [
{
path: '/',
name: 'Home',
// 动态导入:Home 页面会被拆分为独立 Chunk
component: () => import('@/views/Home')
},
{
path: '/about',
name: 'About',
// 可选:给 Chunk 自定义名称(打包后文件名更清晰)
component: () => import(/* webpackChunkName: "about-page" */ '@/views/About')
},
{
path: '/user',
name: 'User',
component: () => import('@/views/User')
}
]
});
打包前后对比:
非按需引入:

按需引入:

2. 组件分包
组件的分包拆分(即 "异步组件")是前端性能优化的关键手段之一,核心是将 "非首屏必需、体积较大或按需加载的组件" 从主页面代码中分离,单独打包成独立文件,仅在组件被使用时才加载。
一、先明确:哪些组件需要分包拆分?
不是所有组件都需要拆分,以下三类组件是 "分包重点":
- 体积大的组件:包含大量 DOM 结构、复杂逻辑(如数据可视化图表、富文本编辑器)或依赖第三方库(如 ECharts 图表组件),单组件体积超过 100KB 时建议拆分。
- 按需触发的组件:用户操作后才显示的组件(如弹窗、抽屉、下拉菜单、折叠面板),默认隐藏状态下无需加载。
- 低频率使用的组件:如 "帮助中心""关于我们""投诉反馈" 等入口对应的组件,用户很少点击,没必要随页面初始加载。
二、Vue 中组件分包的实现方式(Vue 2 和 Vue 3)
1. Vue 2 中的实现:动态导入注册组件
Vue 2 中通过 "动态 import + 组件注册" 实现分包,无需额外 API:
xml
<!-- 页面组件:ProductDetail.vue(商品详情页) -->
<template>
<div>
<!-- 主内容:立即加载 -->
<div class="product-basic">图片、标题、价格...</div>
<!-- 按需加载的组件:点击按钮才显示 -->
<el-button @click="showComment = true">查看评价</el-button>
<comment-list v-if="showComment" /> <!-- 评价列表组件(需拆分) -->
</div>
</template>
<script>
export default {
components: {
// 关键:动态导入组件,实现分包
CommentList: () => import('@/components/CommentList.vue')
},
data() {
return {
showComment: false // 控制组件显示,初始为 false(不加载)
};
}
};
</script>
- 原理:
() => import('路径')告诉 Webpack/Vite:"这个组件不是必须的,打包时单独拆成一个文件"。 - 加载时机:只有当
showComment变为true(用户点击按钮)时,浏览器才会请求CommentList对应的 JS/CSS 文件。
2. Vue 3 中的实现:defineAsyncComponent(更强大、更完善、给vue官方点👍)
Vue 3 提供了 defineAsyncComponent API,专门用于异步组件,支持加载状态、错误处理等高级配置:
xml
<!-- 页面组件:ProductDetail.vue -->
<template>
<div>
<div class="product-basic">图片、标题、价格...</div>
<el-button @click="showComment = true">查看评价</el-button>
<CommentList v-if="showComment" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
// 导入加载中、加载失败的占位组件(可选)
import Loading from '@/components/Loading.vue';
import Error from '@/components/Error.vue';
// 关键:用 defineAsyncComponent 定义异步组件,实现分包
const CommentList = defineAsyncComponent({
loader: () => import('@/components/CommentList.vue'), // 动态导入路径
loadingComponent: Loading, // 组件加载过程中显示的占位符
errorComponent: Error, // 组件加载失败时显示的内容
delay: 200, // 延迟 200ms 显示 loading(避免一闪而过)
timeout: 5000 // 5秒内未加载完成则视为失败
});
const showComment = ref(false);
</script>
- 优势:相比 Vue 2 的简单动态导入,
defineAsyncComponent能处理加载状态(避免用户看到空白)和错误情况(如网络故障),体验更友好。
三、自定义分包名称与公共组件拆分
1. 自定义分包文件名(便于调试)
默认情况下,拆分的组件文件会以哈希值命名(如 123.js),可通过 Webpack 魔法注释自定义名称:
javascript
// Vue 2 中
components: {
CommentList: () => import(/* webpackChunkName: "comment-list" */ '@/components/CommentList.vue')
}
// Vue 3 中
const CommentList = defineAsyncComponent({
loader: () => import(/* webpackChunkName: "comment-list" */ '@/components/CommentList.vue')
});
打包后会生成 comment-list.xxxx.js,更易识别。
2. 多个异步组件合并拆分(避免文件过多)
如果多个小异步组件(如弹窗 A、弹窗 B)都依赖同一个工具函数,可通过 "统一 chunk 名称" 将它们合并打包:
javascript
// 弹窗 A 组件
const PopupA = () => import(/* webpackChunkName: "popups" */ '@/components/PopupA.vue');
// 弹窗 B 组件
const PopupB = () => import(/* webpackChunkName: "popups" */ '@/components/PopupB.vue');
打包后,PopupA 和 PopupB 会合并到 popups.xxxx.js 中,避免生成过多小文件(小文件过多会增加 HTTP 请求次数)。
3. 避免过度拆分(反优化)
- 体积小于 30KB 的组件无需拆分(拆分后增加的 HTTP 请求成本可能超过体积优化收益)。
- 首屏必需的组件(如导航栏、页脚)不能拆分(拆分会导致首屏显示延迟)。
四、如何验证组件是否拆分成功?
-
打包后查看产物 :执行
npm run build,在dist/js目录中查找是否有组件对应的独立文件(如comment-list.xxxx.js)。 -
浏览器 Network 面板:
- 打开页面,初始加载时观察
Network中的 JS 文件,确认异步组件的文件未被加载。 - 触发组件显示(如点击 "查看评价"),此时会看到浏览器新请求该组件的 JS/CSS 文件,说明拆分生效。
- 打开页面,初始加载时观察
注意
组件的分包拆分是 "同一页面内的按需加载优化",与路由拆分(不同页面的按需加载)形成互补。核心逻辑是:用动态导入让非必需组件 "延迟加载",减少首屏代码体积。实现时需注意 "按需拆分"(只拆大组件、按需组件),避免过度拆分导致请求增多。
第三方库的拆分
-
Vue CLI 官方文档 - 构建优化 在 Vue CLI 官方文档的「构建优化」章节中提到,其内置的 Webpack 配置会自动拆分代码,具体包括:
- 分离第三方库(如
vue、vue-router等)和应用代码,避免第三方库被重复打包。 - 拆分公共代码(多页面应用中共享的代码),减少整体打包体积。
文档中明确说明:Vue CLI 的默认配置已针对大多数应用做了优化,包括合理的代码拆分策略。
- 分离第三方库(如
-
Vue CLI 内置 Webpack 配置解析 Vue CLI 通过
@vue/cli-service封装了 Webpack 配置,其默认的splitChunks配置逻辑可通过以下方式验证:-
执行
vue inspect --plugin splitChunks命令(在 Vue CLI 项目根目录),可查看内置的代码拆分配置。 -
输出结果中会包含类似以下的核心配置(简化版):
javascriptsplitChunks: { chunks: 'all', // 对所有类型的 chunk(初始、异步、所有)进行拆分 cacheGroups: { vendors: { name: 'chunk-vendors', // 第三方库拆分后的文件名 test: /[\/]node_modules[\/]/, // 匹配 node_modules 中的第三方库 priority: 10, // 优先级高于默认的 common 组 chunks: 'initial' // 针对初始 chunk 拆分 }, common: { name: 'chunk-common', // 公共代码拆分后的文件名 minChunks: 2, // 被至少 2 个 chunk 共享才会拆分 priority: 1, // 优先级低于 vendors 组 reuseExistingChunk: true // 复用已存在的 chunk } } }这一配置明确将
node_modules中的第三方库(如vue、axios等)拆分为chunk-vendors.js,而应用自身代码和公共组件拆分为其他 chunk,与官方描述一致。也就是所有的三方库为一个大的文件,其他的为一个文件这样的形式去打包
-
如果对三方库各自进行打包?
假设项目有两个独立业务模块:
- 数据可视化模块 :依赖
echarts、chart.js; - 文档处理模块 :依赖
xlsx、pdfjs-dist。
默认分包会把这 4 个库全部混入 chunk-vendors.js,如果用户只访问 "数据可视化模块",xlsx 和 pdfjs-dist 的代码就是 "无效加载";且只要其中一个库更新(如 echarts 升级),整个 chunk-vendors.js 的 hash 会变,导致所有依赖这个包的页面缓存失效。
手动分库解决:
按业务模块拆分第三方库,让每个模块的依赖独立打包:
javascript
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
cacheGroups: {
// 1. 数据可视化模块的第三方库
vendor-visual: {
test: /[\/]node_modules[\/](echarts|chart.js)[\/]/,
name: 'chunk-vendor-visual', // 独立包:仅包含可视化相关库
priority: 20,
chunks: chunk => chunk.name.includes('visual') // 仅对"可视化模块页面"生效
},
// 2. 文档处理模块的第三方库
vendor-doc: {
test: /[\/]node_modules[\/](xlsx|pdfjs-dist)[\/]/,
name: 'chunk-vendor-doc', // 独立包:仅包含文档相关库
priority: 20,
chunks: chunk => chunk.name.includes('doc') // 仅对"文档模块页面"生效
},
// 3. 通用核心库(vue、vue-router 等)
vendors: {
test: /[\/]node_modules[\/]/,
name: 'chunk-vendors',
priority: 10,
// 排除上述两个业务模块的依赖
exclude: /[\/]node_modules[\/](echarts|chart.js|xlsx|pdfjs-dist)[\/]/
}
}
}
}
}
};
结果:
- 用户访问 "可视化模块" 时,仅加载
chunk-vendors.js+chunk-vendor-visual.js,无无效代码; - 当
echarts升级时,仅chunk-vendor-visual.js的 hash 变化,chunk-vendor-doc.js和通用chunk-vendors.js的缓存不受影响,提升后续访问速度。
- 第三方库的打包是按需引入好还是全局引入好
先明确两种引用方式的打包差异
不管是 Vue CLI 还是 Vite,对 Vant UI 的打包处理逻辑都和 "引用范围" 强相关,先理清本质差异:
| 引用方式 | 打包结果 | 核心逻辑 |
|---|---|---|
| 全局引用 | 所有 Vant 组件(即使没用到)都打包进 chunk-vendors.js(或类似第三方库 chunk),最终只有 1 个第三方库文件 |
全局注册时,Webpack/Vite 会把整个 vant 包视为 "必需依赖",无法 Tree-Shaking 剔除未使用组件 |
| 按需引用 | 只打包你实际用到的 Vant 组件(如 Button、Dialog),每个组件(或组件组)可能拆成独立小 chunk(如 chunk-vant-button.js),最终会多几个小文件 |
按需引入时(如 import Button from 'vant/lib/button' 或用 Vant 插件),工具能精准识别 "用到的代码",未使用组件被 Tree-Shaking 剔除,同时按组件拆分 chunk |
1. 优先选 "全局引用" 的场景
- 小项目 / 工具类项目:如内部管理后台、简单的活动页,用到的 Vant 组件少(或几乎全用),且对首屏加载速度要求不高(用户多为内部人员,网络环境稳定)。
- 快速迭代 / 原型开发:需要快速出效果,不想在 "组件引入" 上花时间,优先保证开发效率。
2. 优先选 "按需引用" 的场景
- 首屏优化敏感项目:如 C 端用户产品(电商、社交 App 前端),首屏加载速度直接影响用户留存,需要极致减小首屏资源体积(LCP 指标要求 ≤2.5s)。
- 只用到少量 Vant 组件:如项目只需要 Vant 的
Button、Toast、Dialog3 个组件,按需引用能避免打包 150KB+ 的全量包,体积优势明显。 - 用 HTTP/2 部署:现代服务器基本支持 HTTP/2,多路复用能并行处理多请求,"多文件" 的请求成本几乎可以忽略,按需引用的 "体积小" 优势被放大。
分包一定好吗
Vue CLI 默认会对 "体积超过 30KB(压缩前)" 的依赖单独拆分,但有时会出现两种问题:
- 小库过多 :多个体积很小的依赖(如
lodash-es的子模块、date-fns)被拆分成多个小 chunk,导致浏览器请求数增加(HTTP/1.1 环境下会阻塞加载); - 重复依赖 :不同业务包中重复引入了同一依赖(如
lodash的debounce方法),默认未合并,导致代码冗余。
手动分库解决:
- 合并小库:将多个小体积依赖合并到一个 chunk,减少请求数;
- 提取重复依赖:将重复引入的依赖单独拆分,实现复用。
yaml
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
minSize: 10000, // 调整最小分包体积(如 10KB 以下不单独拆分)
cacheGroups: {
// 合并小体积工具库(lodash-es、date-fns 等)
vendor-utils: {
test: /[\/]node_modules[\/](lodash-es|date-fns|dayjs)[\/]/,
name: 'chunk-vendor-utils', // 合并成一个工具库包
priority: 20,
minSize: 0, // 强制合并,忽略 minSize 限制
minChunks: 2 // 被引用超过 2 次才拆分(避免单次引用的小库被合并)
}
}
}
}
}
};
手动分库打包的核心判断标准(常规情况下)
当满足以下任一条件时,就需要手动干预 Vue CLI 的第三方库分包:
- 首屏 vendor 包体积过大(如超过 1MB),导致首屏加载慢;
- 第三方库按业务模块划分明确,需要拆分以优化缓存;
- 存在非标准依赖(私有库、CDN 依赖),默认分包未覆盖;
- 默认分包粒度不合理(小库过多导致请求数增加,或重复依赖导致冗余)。
简单说:Vue CLI 的默认分包是 "通用方案",当项目有个性化的性能优化需求 或特殊依赖场景 时,就需要手动配置 splitChunks 来调整分库逻辑。