引言

几乎所有 Vue2 老项目的首屏慢问题,根源都指向同一个:单 bundle 过大

很多人只知道 "要分包",却不知道为什么要分、Webpack 打包的本质是什么、splitChunks 每个配置项背后的逻辑。本文将从底层原理讲起,带你彻底搞懂 Webpack 代码分割,最后给出一套可直接复制到 Vue2 项目的最优分包配置,解决首屏 JS 体积过大、解析慢、缓存失效三大痛点。

一、为什么必须分包?Webpack 单 bundle 的致命缺陷

1.1 Webpack 打包的本质

Webpack 是一个模块打包器,它会从入口文件(main.js)开始,递归遍历所有依赖,最终将所有模块打包成一个或多个 bundle 文件。

在没有任何分包配置的情况下,Webpack 会把整个项目的所有代码(业务代码 + 第三方依赖 + 组件 + 样式)全部打包成一个 app.js 文件。

1.2 单 bundle 的三大问题

  1. 加载慢:单文件体积动辄 2-3MB,弱网环境下下载需要数秒,白屏时间长
  2. 解析执行慢:浏览器需要一次性解析编译整个大 JS 文件,主线程阻塞时间长,产生大量长任务和丢帧
  3. 缓存失效严重:任何一行代码改动,整个 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 关键配置的最佳实践

  1. chunks: 'all':必须设置为 all,才能同时分割同步和异步 chunk,这是默认配置最大的问题
  2. minSize: 20000:不要设置太小,否则会生成太多小文件,反而增加 HTTP 请求开销
  3. maxSize: 244000:设置为 244KB,刚好是浏览器 gzip 压缩的最优阈值
  4. priority:缓存组的优先级,数值越大优先级越高,vendors 必须高于 default
  5. 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.js2.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:只有文件内容发生变化时,哈希才会变化
  • 第三方依赖的哈希几乎不会变,用户只需要下载变化的业务代码

七、常见坑点与避坑指南

  1. 不要过度分包:生成太多小文件会增加 HTTP 请求开销,minSize 不要设置太小
  2. 不要把所有第三方库都打进 vendors:体积大且不常用的库(如 echarts、luckysheet)应该使用动态导入单独分包
  3. 注意缓存组优先级:vendors 优先级必须高于 default,否则第三方依赖会被打进 default chunk
  4. 动态导入不能写在循环里:Webpack 无法静态分析循环中的 import (),会导致打包失败

总结

Webpack 代码分割的核心思想是 "按需加载、分层缓存"。通过合理配置 SplitChunks 和使用动态导入,我们可以将首屏 JS 体积减少 50% 以上,大幅提升首屏加载速度。

本文给出的配置和方案已经在多个 Vue2 老项目中验证有效,你可以直接复制到你的项目中,根据实际情况微调即可。

更多推荐