创见博客
webpack Code Splitting:把大包拆开按需加载
七崽爱吃小饼干2026/09/16阅读 0专栏 webpack构建高级

项目发展到一定程度,bundle.js 会越来越大。用户打开首页,却要下载整个应用(包括还没进的路由、点开的弹窗、用不到的第三方库),首屏自然快不起来。**Code Splitting(代码分割)**就是把一个巨大的 bundle 拆成多个小包,让浏览器按需、并行地加载。

一、为什么需要代码分割

假设整个应用被打成一个包:

bundle.js  2.3 MB

问题随之而来:

  • 首屏慢:用户只想看首页,却要下载包括「设置页」「详情页」「编辑器」在内的全部代码。
  • 缓存效率低:改任何一处代码,整个 bundle 的 hash 都会变化,用户缓存全部失效,必须重新下载 2.3 MB。
  • 加载与执行成本高:未用到的代码也要解析执行,白白占用主线程。

理想情况是:

  • 首屏只加载「现在就要用」的代码。
  • 用户点击某个路由时,再加载对应页面的代码。
  • 第三方库变化少,单独拆出来长期缓存,改业务代码时它不受影响。

这正是 Code Splitting 要达成的目标。

二、Code Splitting 是什么

Code Splitting 指的是:

把代码拆分成多个 chunk(代码块),让 webpack 分别输出成独立的文件,在运行时按需加载,而不是把所有模块合并成一个 bundle。

拆分出来的 chunk 主要有两类:

  • 初始 chunk(initial):页面一打开就需要加载的,比如入口和框架运行时。
  • 异步 chunk(async):通过动态 import() 触发的,只有执行到那一行才会去请求。

配合 splitChunks,还能把多个 chunk 共享的模块(尤其是 node_modules 里的第三方库)提取成公共包,避免重复打包。

三、怎么用

webpack 提供了三种代码分割的方式,从手动到自动。

1. 多入口(手动分割)

最原始的方式:配置多个 entry,适合多页面应用。

js
module.exports = {
  entry: {
    home: './src/home.js',
    admin: './src/admin.js',
  },
  output: {
    filename: 'js/[name].[contenthash:8].js',
  },
};

缺点是「拆多少」全靠人工,且公共依赖容易重复。适合页面之间天然独立的场景。

2. 动态 import()(按需加载)

这是最常用、也是收益最直接的方式。把 import 从静态改成动态:

js
// 静态导入:会被打进同一个 chunk
import { heavyFn } from './heavy.js';

// 动态导入:单独成一个 chunk,执行到这行才加载
button.addEventListener('click', () => {
  import('./heavy.js').then(({ heavyFn }) => {
    heavyFn();
  });
});

webpack 遇到 import() 时,会自动把目标模块及其依赖拆成一个独立 chunk,并在需要时通过 __webpack_require__.e 去请求它。因为返回的是 Promise,所以可以配合 async/await:

js
async function handleClick() {
  const { heavyFn } = await import('./heavy.js');
  heavyFn();
}

路由级分割是它的典型应用——React 用 React.lazy,Vue 用异步组件:

jsx
// React
const Detail = React.lazy(() => import('./pages/Detail.jsx'));
js
// Vue Router
const routes = [
  { path: '/detail', component: () => import('./pages/Detail.vue') },
];

这样每个路由页面的代码都会被拆出来,用户访问哪个页面才加载哪个。

3. 魔法注释:给 chunk 命名与控制加载

import() 支持通过魔法注释控制拆分行为:

js
import(
  /* webpackChunkName: "detail-page" */
  /* webpackPrefetch: true */
  './pages/Detail.jsx'
);
注释作用
webpackChunkName指定 chunk 名称,产物文件名更可读
webpackPrefetch空闲时预取,用户后续可能用到
webpackPreload与父 chunk 并行预加载,优先级更高
webpackMode控制拆分模式(lazy / eager / weak 等)
webpackInclude / webpackExclude动态导入目录时限定或排除匹配文件

其中 prefetch 与 preload 的区别值得记住:

  • prefetch:浏览器空闲时下载,适合「下一步很可能要用」的代码,不抢当前资源带宽。
  • preload:和当前 chunk 并行下载,适合「当前页面很快就要用」的代码,优先级高。

4. splitChunks:抽取公共依赖

动态导入解决「按需加载」,splitChunks 解决「避免重复、便于缓存」。它会把多个 chunk 共享的模块提取成公共包:

js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // 对同步和异步 chunk 都生效
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2, // 至少被 2 个 chunk 引用才提取
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

几个关键配置:

配置含义
chunks作用范围:'all' / 'async' / 'initial'
minSize超过该体积才拆分(默认 20000 字节)
minChunks被引用几次才提取
maxSize超过该体积尝试进一步拆分
cacheGroups分组规则,按 test / priority 决定归属
reuseExistingChunk可复用已有 chunk 时不再重复打包

最常见的收益:把 node_modules 拆成 vendors。业务代码更新时,vendors 内容不变、hash 不变,用户无需重新下载。

5. runtimeChunk:分离运行时代码

webpack 的运行时代码(模块加载、缓存等)默认打进入口文件。一旦有异步 chunk 的内容变化,运行时代码可能跟着变,导致入口 hash 变化,连锁使缓存失效。把它单独抽出来即可:

js
module.exports = {
  optimization: {
    runtimeChunk: 'single', // 所有入口共用一个 runtime chunk
  },
};

四、产物命名与目录

分割后会有多个文件,建议通过 filename / chunkFilename 规划清楚:

js
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',       // 初始 chunk
    chunkFilename: 'js/[name].[contenthash:8].chunk.js', // 异步 chunk
  },
};

配合 splitChunks 的 name,产物结构大致是:

dist/js/
├── runtime.c9d0e1f2.js
├── vendors.e5f6a7b8.js
├── main.a1b2c3d4.js
├── detail-page.11223344.chunk.js
└── 42.99887766.chunk.js

五、注意事项

  1. 不要过度分割。chunk 太碎会带来大量 HTTP 请求和调度开销,尤其 HTTP/1.1 下适得其反。用 minSize / minChunks 控制粒度。
  2. chunks: 'all' 抽公共包时留意体积和请求数。提取过度可能让首屏反而要加载更多文件。
  3. 区分 prefetch 与 preload。prefetch 可能浪费带宽(用户未必会访问),preload 会抢占关键资源,都要按场景选。
  4. 动态导入的代码会延迟执行。如果某段代码「首屏必须用」,就别拆成异步,否则会多一次请求往返。
  5. 老浏览器兼容。动态 import() 需要 polyfill,可用 @babel/plugin-syntax-dynamic-import(Babel 7 已内置语法支持)或构建时转换。

六、完整配置示例

js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    publicPath: '/',
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: -10,
        },
        common: {
          name: 'common',
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

业务代码中的动态导入:

js
// src/index.js
document.querySelector('#detail').addEventListener('click', () => {
  import(
    /* webpackChunkName: "detail-page" */
    /* webpackPrefetch: true */
    './pages/detail.js'
  ).then(({ renderDetail }) => renderDetail());
});

小结

Code Splitting 的三种手段分工明确:

方式解决的问题适用场景
多入口 entry页面级手动拆分多页面应用
动态 import()运行时代码按需加载路由、弹窗、重型功能
splitChunks提取公共依赖、优化缓存所有中大型项目
runtimeChunk稳定入口 hash配合长期缓存

一句话:用动态 import() 把「暂时用不到」的代码拆出去,用 splitChunks 把「大家都用」的代码抽出来,再用 runtimeChunk 稳住缓存。 三者配合,才能兼顾首屏体积、加载体验和缓存效率。

评论
0/100