WinddSnow

Webpack-Performance-Optimization-Code-Splitting-Lazy-Loading-SplitChunks-Cach...

字数统计: 4.4k阅读时长: 17 min
2026/07/31

第82课:Webpack 性能优化——Code Splitting、懒加载、SplitChunksPlugin、缓存策略

Webpack 的核心使命之一是将应用打包为浏览器可高效加载的静态资源。默认情况下,所有模块会被打包为一个(或少数几个)巨大的 bundle,导致首屏加载缓慢缓存失效频繁(任何一处代码变动都会使整个 bundle 的哈希变化)。Webpack 提供了多层优化手段来解决这些问题:Code Splitting 将代码拆分为多个较小的 chunk,懒加载 按需加载非首屏模块,**SplitChunksPlugin** 精细控制公共依赖的提取和合并,缓存策略 利用内容哈希和运行时分离实现长效缓存。本节课将逐一拆解这些优化技术的原理、配置和最佳实践。


1. Code Splitting:代码分割的三种方式

代码分割(Code Splitting)是指将打包产物拆分为多个文件(chunk),而非单一 bundle.js。它的核心价值在于:

  • 按需加载:首屏只加载必要的代码,其他代码在需要时再加载,显著降低首屏 JS 体积。
  • 并行下载:浏览器可同时下载多个较小的文件,而非等待一个巨大文件下载完成。
  • 缓存优化:第三方库(如 React、Lodash)与业务代码分离后,库代码可长期缓存(因为版本变更频率远低于业务代码)。

Webpack 支持三种代码分割方式:

1.1 入口分割(Entry Points)

webpack.config.js 中配置多个入口,每个入口生成独立的 chunk。这是最原始、最显式的分割方式。

1
2
3
4
5
6
7
8
9
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {
filename: '[name].[contenthash:8].js',
},
};

缺点

  • 手动维护入口列表繁琐。
  • 如果多个入口引用了相同的模块(如 React),该模块会被重复打包进每个入口的 chunk 中,造成代码冗余。这是入口分割无法自动解决的问题——需要配合 SplitChunksPlugin 提取公共依赖。

1.2 动态导入(Dynamic Import)

使用 import() 语法在代码中动态加载模块。Webpack 遇到 import() 时会自动将导入的模块及其依赖提取到一个独立的异步 chunk 中,并在运行时按需请求。

1
2
3
4
5
6
7
8
// 静态导入(整个模块会打包进主 bundle)
import { heavyFunction } from './heavy-module';

// 动态导入(Webpack 自动拆分为独立 chunk)
button.addEventListener('click', async () => {
const { heavyFunction } = await import('./heavy-module');
heavyFunction();
});

动态导入的 Webpack 特殊注释

1
2
3
4
5
6
// 自定义 chunk 名称
const module = await import(/* webpackChunkName: "my-chunk" */ './my-module');

// 预加载/预获取
import(/* webpackPrefetch: true */ './future-module'); // 空闲时预获取
import(/* webpackPreload: true */ './critical-module'); // 与父 chunk 并行加载

这些魔术注释(Magic Comments)为 Webpack 提供了关于 chunk 的额外信息:

  • webpackChunkName:指定 chunk 的名称(而非自动生成的数字 ID),便于调试和缓存分析。
  • webpackPrefetch: true:浏览器在空闲时预先获取该模块,用于未来可能需要的资源(如用户可能点击的下一页)。
  • webpackPreload: true:与父 chunk 并行加载,用于当前页面必需但动态导入的资源。

框架中的懒加载集成

  • ReactReact.lazy 配合 <Suspense> 实现组件级懒加载。
  • Vue 3defineAsyncComponent 异步组件。
  • Angular:路由配置中的 loadChildren 惰性加载模块。

这些框架的底层原理都是基于 import(),由打包工具(Webpack/Vite)将异步组件拆分为独立的 chunk。

1
2
3
4
5
6
7
8
9
10
11
12
// React 懒加载组件
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
return (
<Suspense fallback={<div>加载中...</div>}>
<HeavyChart />
</Suspense>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Vue 3 异步组件 -->
<script setup>
import { defineAsyncComponent } from 'vue';

const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'));
</script>

<template>
<Suspense>
<HeavyChart />
<template #fallback>加载中...</template>
</Suspense>
</template>

1.3 SplitChunksPlugin:智能提取公共依赖

SplitChunksPlugin(Webpack 内置,通过 optimization.splitChunks 配置)是代码分割的核心自动机制。它会分析所有 chunk 的依赖关系,将被多个 chunk 共享的模块提取为独立的公共 chunk,避免重复打包。

基础配置(足以覆盖大多数场景):

1
2
3
4
5
6
7
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 对所有类型的 chunk 生效(async + initial)
},
},
};

仅设置 chunks: 'all',Webpack 就会自动提取公共依赖。但为了精细控制缓存和加载性能,需要深入理解 cacheGroups


2. SplitChunksPlugin 配置详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
optimization: {
splitChunks: {
chunks: 'all', // async: 仅分割异步 chunk;initial: 仅分割入口 chunk;all: 全部
minSize: 20000, // 模块最小体积(字节),小于此值不分割(默认 20KB)
minChunks: 1, // 模块至少被引用的次数,少于此次数不分割
maxAsyncRequests: 30, // 按需加载时的最大并行请求数
maxInitialRequests: 30, // 入口点处的最大并行请求数
cacheGroups: { // 缓存组:自定义分割规则
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors',
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
}
}
}
}

2.1 chunks:应用范围

说明 适用场景
'async' 仅对异步 chunkimport() 动态导入产生)进行分割。 保守策略,不影响首屏请求数。
'initial' 仅对入口 chunkentry 配置中声明的入口)进行分割。 控制首屏 bundle 的组成。
'all' 所有 chunk 进行分割。推荐配置。 最大化提取公共代码,减少总体积。
函数 (chunk) => boolean,自定义逻辑。 高级场景(如根据 chunk 名称决定)。

2.2 cacheGroups:缓存组

cacheGroupsSplitChunksPlugin 的核心——它定义了一组分割规则,每个规则可以指定哪些模块应该被提取、合并到哪个 chunk 中。

关键属性

属性 说明 默认值
test 匹配模块路径的正则或函数。
priority 优先级。数值越大优先级越高,模块优先匹配高优先级的规则。 0
name 提取出的 chunk 名称。同名 cacheGroup 的模块会被合并到同一个 chunk 中。
filename 输出文件名(可覆盖 output.filename)。
minSize 模块最小体积。 继承全局
minChunks 最少引用次数。 继承全局
reuseExistingChunk 如果当前 chunk 已包含该模块,不再重复分割(避免多余请求)。 true
enforce 忽略 minSizeminChunks 等全局限制,强制提取。 false

2.3 实战配置:典型的前端项目 cacheGroups

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
cacheGroups: {
// 提取 Vue/React 框架核心
framework: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia|react|react-dom|react-router|react-router-dom)[\\/]/,
name: 'framework',
priority: 40, // 最高优先级
},
// 提取大型 UI 库
uiLib: {
test: /[\\/]node_modules[\\/](element-plus|antd|@mui)[\\/]/,
name: 'ui-lib',
priority: 30,
},
// 提取其他第三方工具库
lib: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
minChunks: 1,
},
// 提取业务代码中的公共模块(至少被 2 个 chunk 引用)
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
name: 'common',
},
},
},
},

设计原则

  • 框架核心vue/react)单独拆包并赋予最高 priority(因为它们被所有页面引用,且极少更新)。
  • UI 库 体积巨大,独立拆包便于浏览器缓存。
  • 业务公共模块 提取条件较严格(minChunks: 2),避免过细碎的文件生成。

3. 缓存策略:长效缓存

浏览器缓存是性能优化的最后一道防线——理想情况下,用户访问网站时,未变更的资源无需重新下载。长效缓存的目标是:当一个模块的内容不变时,它的文件名(哈希)也不变,从而最大化缓存命中率。

3.1 contenthash:基于内容的哈希

output.filename 中使用 [contenthash],哈希值基于文件内容生成。内容不变,哈希不变。

1
2
3
4
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
}

**对比 [hash][chunkhash]**:

占位符 生成依据 问题
[hash] 整个构建 的哈希。 任何文件变化,所有文件哈希都变——缓存完全失效。
[chunkhash] 每个 chunk 内容 的哈希。 如果 chunk 中引入的 CSS 变了,该 chunk 的 JS 哈希也变(因为它是同一 chunk)。
[contenthash] 单个文件内容 的哈希。 精准反映文件变化,实现最优缓存。生产环境标准配置

3.2 runtimeChunk:分离运行时与清单

Webpack 在生成的每个 chunk 中都包含一小段运行时代码(用于管理模块加载和 chunk 间的依赖关系)和 manifest(模块 ID 到路径的映射记录)。这些代码在每次构建时都可能变化(即使业务代码未变),导致所有 chunk 的哈希值均被破坏。

解决方案:通过 optimization.runtimeChunk: 'single' 将运行时和 manifest 提取到一个独立的 chunk 中。这个 runtime chunk 体积很小(通常 < 10KB),可单独缓存,而其他业务 chunk 的哈希在业务代码不变时保持稳定。

1
2
3
optimization: {
runtimeChunk: 'single',
}
runtimeChunk 行为
false(默认) 运行时内联在每个 chunk 中。
'single' 所有 chunk 共享一个运行时文件。推荐值
'multiple' 每个入口 chunk 拥有独立的运行时文件(极少使用)。

3.3 moduleIds:稳定的模块 ID

Webpack 内部为每个模块分配一个 ID(数字或字符串)。默认情况下,ID 基于模块在依赖图中的顺序分配,因此添加或删除模块可能改变其他模块的 ID,导致 contenthash 变化。

通过 optimization.moduleIds: 'deterministic' 将模块 ID 固定为基于路径的短哈希。这确保模块 ID 仅在其内容变化时才变化。

1
2
3
4
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic', // 同样推荐
}
说明
'natural' 按模块使用顺序递增的数字 ID(不稳定,不应在生产环境使用)。
'named' 基于文件路径的可读 ID(开发环境推荐,便于调试)。
'deterministic' 基于内容的短哈希,稳定且短。生产环境推荐
'size' 基于模块大小的数字 ID(较少使用)。

4. 其他优化手段

4.1 Tree Shaking

Tree Shaking(摇树优化)指的是移除 JavaScript 中未引用的代码(dead code)。它是“代码分割”的互补技术——代码分割是“把代码拆开”,Tree Shaking 是“把没用的代码扔掉”。

启用条件

  • 使用 ES Modules(import/export),因为 ESM 的静态结构允许打包工具在编译时分析哪些导出被使用。
  • Webpack 的 mode: 'production' 会自动启用 Tree Shaking(通过 TerserPlugin 移除死代码)。
  • package.json 中使用 "sideEffects": false 或指定有副作用的文件,帮助 Webpack 安全地移除未使用的模块。
1
2
3
4
5
6
7
8
// package.json
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}

"sideEffects": false 告诉 Webpack:“这个包中的所有模块都没有副作用,可以安全地移除未使用的导出。”对于 CSS 文件需要显式声明它们有副作用(因为导入 CSS 即使没被导出也需要保留)。

4.2 Scope Hoisting

Scope Hoisting(作用域提升)将多个模块合并到一个函数作用域中,减少 Webpack 默认的模块包裹函数开销。它在 mode: 'production' 下自动启用。

4.3 资源预加载:prefetchpreload

使用 Webpack 的魔术注释 /* webpackPrefetch: true *//* webpackPreload: true */ 控制资源的加载优先级。

1
2
3
4
5
// 预获取:空闲时下载,用于下一页可能需要的资源
const AboutPage = () => import(/* webpackPrefetch: true */ './AboutPage');

// 预加载:立即并行下载,用于当前页面肯定需要的资源
const CriticalModule = () => import(/* webpackPreload: true */ './CriticalModule');
类型 下载时机 适用场景
prefetch 浏览器空闲时。 用户下一步可能访问的页面或功能。
preload 立即,与父 chunk 并行。 当前页面肯定需要的资源,但通过动态导入分割。

5. 综合实战:一个完整的优化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// webpack.config.js
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',
clean: true,
publicPath: '/',
},

optimization: {
// 分离运行时
runtimeChunk: 'single',

// 稳定模块 ID
moduleIds: 'deterministic',
chunkIds: 'deterministic',

// 代码分割
splitChunks: {
chunks: 'all',
minSize: 20000,
maxAsyncRequests: 20,
maxInitialRequests: 10,
cacheGroups: {
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
name: 'framework',
priority: 40,
},
uiLib: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'ui-lib',
priority: 30,
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
name: 'common',
},
},
},

// 压缩
minimizer: [
new TerserPlugin({
terserOptions: {
compress: { drop_console: true }, // 移除 console.log
},
}),
new CssMinimizerPlugin(),
],
},
};

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 在 Webpack 中,import() 动态导入会自动触发哪种优化?
    A. Tree Shaking
    B. Code Splitting
    C. Scope Hoisting
    D. Module Federation

  2. (单选) SplitChunksPluginchunks: 'all' 选项表示什么?
    A. 仅对异步 chunk 进行分割。
    B. 对所有 chunk(异步和同步入口)进行分割。
    C. 仅对入口 chunk 进行分割。
    D. 禁用代码分割。

  3. (填空) 为了实现长效缓存,应使用 ______ 占位符生成基于文件内容哈希的文件名。

  4. (多选) 以下哪些措施有助于 Webpack 生产构建的长效缓存?
    A. 使用 [contenthash] 占位符
    B. 设置 optimization.runtimeChunk: 'single'
    C. 设置 optimization.moduleIds: 'deterministic'
    D. 使用 [hash] 占位符

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为一个中大型 Webpack 5 项目配置生产优化。要求如下:

  • 启用 splitChunks,将 react/react-dom/react-router 拆分为 framework chunk,将 antd 拆分为 ui-lib chunk,其他 node_modules 拆分为 vendors chunk,业务公共代码(至少被 2 个 chunk 引用)拆分为 common chunk。
  • 启用 runtimeChunk: 'single'moduleIds: 'deterministic' 以支持长效缓存。
  • 输出文件名使用 [contenthash:8]
  • 生产模式移除 console.log(通过 TerserPlugin 的 drop_console)。
  • 配置一个完整的 optimization 部分。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 webpack.config.js 配置片段。提示词中需明确指定 cacheGroups 的优先级配置和长效缓存相关设置。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请为一个 React + Webpack 5 项目创建生产优化配置。需要生成以下文件:

  1. webpack.prod.js
    • mode: 'production'
    • output: filename: 'js/[name].[contenthash:8].js', chunkFilename: 'js/[name].[contenthash:8].chunk.js', clean: true
    • optimization: runtimeChunk: 'single', moduleIds: 'deterministic', chunkIds: 'deterministic'
    • splitChunks: { chunks: 'all', cacheGroups: { framework: { test: /node_modules[\\/](react|react-dom|react-router)/, name: 'framework', priority: 40 }, uiLib: { test: /node_modules[\\/]antd/, name: 'ui-lib', priority: 30 }, vendors: { test: /node_modules/, name: 'vendors', priority: 10 }, common: { minChunks: 2, priority: 5, name: 'common', reuseExistingChunk: true } } }
    • minimizer: new TerserPlugin({ terserOptions: { compress: { drop_console: true } } }), new CssMinimizerPlugin()
    • 添加必要的注释解释每个 cacheGroup 的用途和优先级设置。
  2. package.json:确认包含 terser-webpack-plugincss-minimizer-webpack-plugin 依赖(若不使用 webpack 5 内置版本)。
    完成后列出所有文件内容。

四、面试真题与参考答案

题目(滴滴前端面试题):

请详细解释 Webpack 的 SplitChunksPluginchunks 选项的三个值(asyncinitialall)的区别,以及在什么场景下选择 all。为什么需要将 runtimeChunk 提取为单独的文件?moduleIds: 'deterministic' 解决了什么问题?

参考答案

  • async 仅分割异步导入的模块(import()),对首屏入口代码不做分割,适合保守优化;initial 仅分割入口 chunk 的公共代码;all 则对异步和同步 chunk 全面分割,最大化提取公共模块,推荐用于大多数项目,尤其是当入口 chunk 共享大量依赖时。
  • runtimeChunk 提取为单独文件是因为 Webpack 的运行时和模块清单在每次构建时都可能变化(即使业务代码未变),如果不分离,这些微小变化会渗透到所有 chunk 的哈希中,破坏缓存。单独提取后,其他业务 chunk 的哈希仅在自身内容变化时才变化,实现真正的长效缓存。
  • moduleIds: 'deterministic' 将模块 ID 固定为基于路径的短哈希,解决了默认自然数字 ID 因模块增删导致顺序变化、进而造成所有 chunk 哈希变化的问题。它确保模块 ID 仅在模块内容变化时才变化。

课后练习答案

一、概念自测答案

  1. B

    • 解析:import() 动态导入触发 Webpack 的 Code Splitting,自动将导入的模块拆分为独立 chunk。
  2. B

    • 解析:chunks: 'all' 对所有 chunk(异步和同步入口)进行公共代码分割。
  3. [contenthash]

    • 解析:[contenthash] 基于文件内容生成哈希,是实现长效缓存的标准占位符。
  4. A、B、C

    • 解析:D 错误,[hash] 是整个构建的哈希,任何文件变化都会导致所有文件名改变。A、B、C 均有助于稳定的长效缓存。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请生成 Webpack 5 的生产优化配置。要求:

  • 输出文件使用 [contenthash:8]
  • splitChunks.chunks: 'all'。cacheGroups 定义四个组:framework(react/react-dom/react-router,priority: 40)、uiLib(antd,priority: 30)、vendors(所有 node_modules,priority: 10)、common(minChunks: 2,priority: 5,reuseExistingChunk: true)。
  • runtimeChunk: 'single'moduleIds: 'deterministic'
  • TerserPlugin 移除 console.log。
  • 直接输出 optimization 部分的完整配置代码,添加注释。”
CATALOG
  1. 1. 第82课:Webpack 性能优化——Code Splitting、懒加载、SplitChunksPlugin、缓存策略
    1. 1.1. 1. Code Splitting:代码分割的三种方式
      1. 1.1.1. 1.1 入口分割(Entry Points)
      2. 1.1.2. 1.2 动态导入(Dynamic Import)
      3. 1.1.3. 1.3 SplitChunksPlugin:智能提取公共依赖
    2. 1.2. 2. SplitChunksPlugin 配置详解
      1. 1.2.1. 2.1 chunks:应用范围
      2. 1.2.2. 2.2 cacheGroups:缓存组
      3. 1.2.3. 2.3 实战配置:典型的前端项目 cacheGroups
    3. 1.3. 3. 缓存策略:长效缓存
      1. 1.3.1. 3.1 contenthash:基于内容的哈希
      2. 1.3.2. 3.2 runtimeChunk:分离运行时与清单
      3. 1.3.3. 3.3 moduleIds:稳定的模块 ID
    4. 1.4. 4. 其他优化手段
      1. 1.4.1. 4.1 Tree Shaking
      2. 1.4.2. 4.2 Scope Hoisting
      3. 1.4.3. 4.3 资源预加载:prefetch 与 preload
    5. 1.5. 5. 综合实战:一个完整的优化配置
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)