Webpack 4/5 代码分割全解:从原理到 Vue2 项目最优分包实战
引言
几乎所有 Vue2 老项目的首屏慢问题,根源都指向同一个:单 bundle 过大。
很多人只知道 "要分包",却不知道为什么要分、Webpack 打包的本质是什么、splitChunks 每个配置项背后的逻辑。本文将从底层原理讲起,带你彻底搞懂 Webpack 代码分割,最后给出一套可直接复制到 Vue2 项目的最优分包配置,解决首屏 JS 体积过大、解析慢、缓存失效三大痛点。
一、为什么必须分包?Webpack 单 bundle 的致命缺陷
1.1 Webpack 打包的本质
Webpack 是一个模块打包器,它会从入口文件(main.js)开始,递归遍历所有依赖,最终将所有模块打包成一个或多个 bundle 文件。
在没有任何分包配置的情况下,Webpack 会把整个项目的所有代码(业务代码 + 第三方依赖 + 组件 + 样式)全部打包成一个 app.js 文件。
1.2 单 bundle 的三大问题
- 加载慢:单文件体积动辄 2-3MB,弱网环境下下载需要数秒,白屏时间长
- 解析执行慢:浏览器需要一次性解析编译整个大 JS 文件,主线程阻塞时间长,产生大量长任务和丢帧
- 缓存失效严重:任何一行代码改动,整个
app.js的哈希都会变化,用户每次更新都要重新下载整个大文件
1.3 分包的核心价值
- 按需加载:只加载当前页面需要的代码,非首屏代码用到时再下载
- 并行下载:浏览器可以同时并行下载多个小文件,比下载一个大文件更快
- 缓存优化:第三方依赖和公共代码单独打包,业务代码改动不影响第三方缓存
二、Webpack 代码分割的三种核心方式
Webpack 提供了三种代码分割方式,各有适用场景:
2.1 入口点分割(Entry Points)
手动配置多个入口文件,Webpack 为每个入口生成独立的 bundle。
// webpack.config.js
module.exports = {
entry: {
app: './src/main.js',
admin: './src/admin.js'
}
}
✅ 优点:简单直观
❌ 缺点:无法提取公共代码,会有重复依赖,适合多页面应用
2.2 动态导入(Dynamic Import)
使用 ES6 的 import() 语法,Webpack 会自动将动态导入的模块打包成独立的异步 chunk。
// 路由懒加载
const Home = () => import('@/views/Home.vue')
// 组件懒加载
async openDialog() {
const { VXETable } = await import('vxe-table')
}
✅ 优点:真正的按需加载,粒度最细,是单页应用最常用的分包方式
❌ 缺点:需要修改代码,适合业务组件和第三方库
2.3 SplitChunksPlugin 分割
Webpack 内置的插件,自动提取公共代码和第三方依赖到独立的 chunk。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
✅ 优点:自动提取,无需修改业务代码,适合提取公共依赖 ❌ 缺点:配置复杂,默认配置不够合理,需要根据项目调整
三、深入 SplitChunksPlugin:每个配置项的底层逻辑
SplitChunks 是 Webpack 最复杂也最强大的配置项,网上大部分教程只给配置,不讲原理。下面我将逐个拆解每个配置项的含义和最佳实践。
3.1 核心配置项详解
splitChunks: {
// 对哪些 chunk 进行分割:initial(同步)、async(异步)、all(全部)
chunks: 'all',
// 提取的 chunk 最小体积(字节),小于这个值不分割
minSize: 20000,
// 分割后的 chunk 最大体积,超过会继续分割
maxSize: 244000,
// 模块被引用的最小次数,小于这个值不提取
minChunks: 1,
// 按需加载时并行请求的最大数量
maxAsyncRequests: 30,
// 入口点并行请求的最大数量
maxInitialRequests: 30,
// 生成 chunk 名称的分隔符
automaticNameDelimiter: '~',
// 缓存组,核心中的核心,定义具体的分割规则
cacheGroups: {
// 默认缓存组,提取被多个 chunk 引用的公共模块
default: {
minChunks: 2,
priority: -20,
// 如果模块已经被打包过,直接复用,不重新打包
reuseExistingChunk: true
},
// 第三方依赖缓存组,提取 node_modules 中的模块
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
}
}
}
3.2 关键配置的最佳实践
- chunks: 'all':必须设置为 all,才能同时分割同步和异步 chunk,这是默认配置最大的问题
- minSize: 20000:不要设置太小,否则会生成太多小文件,反而增加 HTTP 请求开销
- maxSize: 244000:设置为 244KB,刚好是浏览器 gzip 压缩的最优阈值
- priority:缓存组的优先级,数值越大优先级越高,vendors 必须高于 default
- reuseExistingChunk: true:必须开启,避免重复打包相同模块
3.3 Vue2 项目最优 SplitChunks 配置
针对 Vue2 后台项目的特点,我总结了一套经过线上验证的最优配置:
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000, // 根据自己情况尝试分包大小
minChunks: 3,
maxAsyncRequests: 5,
maxInitialRequests: 5,
automaticNameDelimiter: '~',
cacheGroups: {
// 提取 Vue 核心库
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
name: 'chunk-vue',
priority: 20,
reuseExistingChunk: true
},
// 提取 ElementUI
element: {
test: /[\\/]node_modules[\\/]element-ui[\\/]/,
name: 'chunk-element',
priority: 15,
reuseExistingChunk: true
},
// 提取其他第三方依赖
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-vendors',
priority: 10,
reuseExistingChunk: true
},
// 提取公共组件
common: {
test: /[\\/]src[\\/]components[\\/]/,
name: 'chunk-common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: 1,
reuseExistingChunk: true
}
}
}
}
}
}
✅ 效果:
- Vue 核心库单独打包,几乎不会变动,缓存命中率最高
- ElementUI 单独打包,体积大但稳定
- 其他第三方依赖打包成 chunk-vendors
- 公共组件打包成 chunk-common,多个页面复用
四、动态导入的深度解析
4.1 动态导入的原理
当 Webpack 遇到 import() 语法时,会自动将该模块及其所有依赖打包成一个独立的异步 chunk。
当代码执行到 import() 时,浏览器会动态创建一个 script 标签,下载对应的 chunk 文件,下载完成后执行代码,返回一个 Promise。
4.2 异步 chunk 命名
默认情况下,Webpack 会给异步 chunk 生成一个数字编号(如 1.js、2.js),不利于调试。我们可以使用 webpackChunkName 注释来指定 chunk 名称:
const Home = () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
4.3 预加载与预获取
Webpack 支持两种预加载方式,进一步优化用户体验:
// preload:提前下载当前页面可能用到的资源,优先级高
const LuckySheet = () => import(/* webpackPreload: true */ 'luckysheet')
// prefetch:浏览器空闲时下载未来页面可能用到的资源,优先级低
const About = () => import(/* webpackPrefetch: true */ '@/views/About.vue')
五、第三方库分包实战
5.1 全局引入的库如何分包
很多老项目会在 main.js 全局引入只在某个页面用到的库:
// main.js 错误写法:全局引入,打进首屏 bundle
import ECharts from 'echarts'
Vue.prototype.$echarts = ECharts
解决方案:使用按需引入 + 动态导入
// 新建按需引入js文件 ========= chart.js ============
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core';
// 引入柱状图图表,图表后缀都为 Chart
import { BarChart } from 'echarts/charts';
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent
} from 'echarts/components';
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers';
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
export default echarts;
// 需要使用echarts的地方 通过import 引入
// 比如 chart.vue
import echarts from '@/utils/chart.js';
export default {
methods: {
// 初始化图
initEcharts () {
}
}
}
✅ 效果:echarts 彻底从首屏 bundle 中移除,只有页面用到的时候才会加载相关js文件。
5.2 弹窗专用库分包
对于只在弹窗中使用的库(如 vxe-table、luckysheet),在弹窗打开时动态导入
async openLuckySheetDialog() {
// 并行加载 JS 和 CSS
const [vxe] = await Promise.all([
import('vxe-table'),
import('vxe-table/lib/style.css')
])
const VXETable = vxe.default;
// 注册组件
const {Table, Column} = VXETable;
this.$options.component['vxe-table'] = Table;
this.$options.component['vxe-column'] = Column;
// 开始显示相关组件
this.visible = true;
}
六、分包后的验证与优化
6.1 使用 webpack-bundle-analyzer 分析打包结果
安装插件:
npm install webpack-bundle-analyzer --save-dev
配置 vue.config.js:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin()
]
}
}
执行打包:
npm run build
浏览器会自动打开一个可视化页面,清晰展示每个 bundle 的组成和大小,帮助你发现大文件和重复依赖。
6.2 缓存策略优化
在 vue.config.js 中配置文件名哈希,确保浏览器能正确缓存:
module.exports = {
filenameHashing: true,
configureWebpack: {
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].js'
}
}
}
contenthash:只有文件内容发生变化时,哈希才会变化- 第三方依赖的哈希几乎不会变,用户只需要下载变化的业务代码
七、常见坑点与避坑指南
- 不要过度分包:生成太多小文件会增加 HTTP 请求开销,minSize 不要设置太小
- 不要把所有第三方库都打进 vendors:体积大且不常用的库(如 echarts、luckysheet)应该使用动态导入单独分包
- 注意缓存组优先级:vendors 优先级必须高于 default,否则第三方依赖会被打进 default chunk
- 动态导入不能写在循环里:Webpack 无法静态分析循环中的 import (),会导致打包失败
总结
Webpack 代码分割的核心思想是 "按需加载、分层缓存"。通过合理配置 SplitChunks 和使用动态导入,我们可以将首屏 JS 体积减少 50% 以上,大幅提升首屏加载速度。
本文给出的配置和方案已经在多个 Vue2 老项目中验证有效,你可以直接复制到你的项目中,根据实际情况微调即可。
更多推荐


所有评论(0)