webpack性能优化方案
webpack是一个打包工具,所以对于它的性能优化,主要是与打包有关的,我们对其的优化可以分为两类:
- 打包产物体积的优化。
- 打包速度的优化。
一般来说,我们更侧重于打包产物体积的优化,它对于准备上线的产品有很大影响。
我们可以通过配置webpack的mode属性来控制打包的优化级别,有development和production两种,分别对应不同的优化级别。它们会默认帮我们配置一些东西,但有些定制化的操作是需要我们自己进行处理的。
代码分离
webpack打包的资源默认是打包到一个文件中的,这样导致在启动的时候要一下子加载所有的资源,这样会大大降低首页的加载速度,我们希望一些不必在首页加载的资源可以按需引入,当需要的时候才去加载,提高性能。
在webpack中,代码分离的主要目的是将代码分离到不同的bundle中,我们可以按需加载,或者并行加载这些bundle。
webpack中常用的代码分离有三种:
-
多入口起点(entry):使用entry配置手动分离代码。
-
防止重复:使用
Entry Dependencies
或SplitChunksPlugin
去重和分离代码。 -
动态导入:通过模块的内联函数调用来分离代码。
多入口起点
此方案就是在 entry 中配置多个入口文件,在 output 输入的文件名等信息用占位符表示:
1 | const path = require('path'); |
打包生成的dist目录结构如下:
1 | dist |
Entry Dependencies(入口依赖)
此方案是基于多入口文件的,假设我们有两个入口文件index.js
和main.js
,它们都依赖相同的工具库,但如果我们单纯的进行入口分离,打包的两个bundle文件中都会包含工具库的代码,这样会重复引入工具库,导致bundle文件体积变大。
此方案就是指定相同引用工具单独打包成一个文件,然后两个入口文件分别引用这个工具文件,这样两个入口文件之间不会重复引入工具库,从而减少bundle文件体积。
配置实现:
1 | module.exports = { |
经过上述配置打包后的dist目录结构如下:
1 | dist |
然后在html中引入这三个文件:
1 | <script src="./dist/shared.bundle.js"></script> |
注意:一定要将shared.bundle.js
写在index.bundle.js
和main.bundle.js
之前,因为shared.bundle.js
存放着 index 和 main 中所需的公共代码,但如果,我们不引入shared.bundle.js
,浏览器也不会报错,这是由于webpack内部做了静默错误处理!而且我们在 index 和 main 中编写的普通代码也不会执行,因为负责加载、缓存和执行模块的__webpack_require__
函数和负责处理动态加载的代码块(chunk),将其集成到模块系统中webpackJsonpCallback
函数都在shared.bundle.js
中。
SplitChunks
SplitChunks 是基于 SplitChunksPlugin 实现的,此插件已经被webpack默认安装和集成,所以我们可以直接填写配置信息来使用它。
我们可以通过optimization.splitChunks
来配置它。
这里给出一些optimization.splitChunks
常见的配置项:
配置项 | 配置值 | 说明 | 默认值 |
---|---|---|---|
chunks | async/initial/all |
async表示对异步代码进行处理/initial表示对同步代码进行处理/all表示对所有代码进行处理 | async |
minSize | number |
拆分包的大小, 至少为minSize,如果一个包拆分出来达不到minSize,那么这个包就不会拆分 | 20000(bytes) |
maxSize | number |
将大于maxSize的包,拆分为不小于minSize的包 | |
cacheGroups | object |
用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包 |
常见的cacheGroups的配置项:
-
test:匹配符合规则的包。
-
name:拆分包的name属性。
-
filename:拆分包的名称,可以使用placeholder(占位符)属性。
-
priority:优先级,数字越大优先级越高。
测试一下,默认情况下的打包操作:
我们通过import函数动态导入一个模块:
1 | const a = import('./utils/asyn_test'); |
运行webpack打包命令,查看打包结果:
1 | dist |
异步操作被分离出一个单独的文件,文件名中的bundle是配置文件定义的静态输出文件名。
加入chunks,minSize,maxSize,cacheGroups属性:
1 | optimization: { |
进行上述配置,打包后的目录结构如下:
1 | dist |
我们可以看出,name属性是为[name]
占位符设置的,如果不设置的话,默认[id]
占位符的值作为[name]
的值。
注意:这里的chunks如果设置值为initial,并不意味着动态导入的异步请求不会被分离,webpack会默认对动态导入进行分离,设置这个的作用除了是能够对同步代码进行分离,还是为了能分离同步和异步代码的共同部分。
optimization.chunkIds
告知 webpack 当选择模块 id 时需要使用哪种算法。将 optimization.chunkIds 设置为 false 会告知 webpack 没有任何内置的算法会被使用,但自定义的算法会由插件提供。optimization.chunkIds 的默认值是 false:
-
如果环境是开发环境,那么 optimization.chunkIds 会被设置成 ‘named’,但当在生产环境中时,它会被设置成 ‘deterministic’。
-
如果上述的条件都不符合, optimization.chunkIds 会被默认设置为 ‘natural’。
optimization.chunkIds的
的值:
值 | 说明 |
---|---|
natural | 按使用顺序的数字 id |
named | 对调试更友好的可读的 id |
deterministic | 在不同的编译中不变的短数字 id。有益于长期缓存。在生产模式中会默认开启 |
size | 专注于让初始下载包大小更小的数字 id |
total-size | 专注于让总下载包大小更小的数字 id |
当 chunkIds 的值为 named的时候,默认在 cacheGroups 中配置的模块打包路径中的[name]和[id]
占位符值为:cacheGroups中配置的键名+引入模块所在的路径名
,但如果我们在cacheGroups
中的分包配置中添加了name属性,[name]和[id]
占位符值就为name属性的值。
natural 和 deterministic 的区别:
-
使用
'natural'
时,Webpack会按照chunk在代码中出现的自然顺序(即创建顺序)来分配ID。这种方式可能会导致ID分配不稳定,因为当你在代码中新增或删除一个chunk时,后续chunk的ID可能会发生变化。因此,它不利于长期缓存,因为模块ID的变化会导致浏览器缓存失效。 -
在
'deterministic'
模式下,Webpack会生成确定的chunk ID。它通过使用模块路径的哈希值来分配ID,这样只要模块内容不变,其chunk ID也不会变。这种方式有利于持久化缓存,因为即使添加或删除其他chunk,某个特定chunk的ID也能保持不变。在Webpack 5中,这是生产模式下的默认值。
optimization.runtimeChunk
配置runtime相关的代码是否抽取到一个单独的chunk中:
-
runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码。
其实就是我们前面提到和__webpack_require__
和webpackJsonpCallback
有关的代码。
前提,配置了runtimeChunk后,output.filename
配置就需要动态生成了,否则会报错:[webpack-cli] Error: Conflict: Multiple chunks emit assets to the same filename bundle.js
。
1 | output: { |
runtimeChunk的值:
-
true/multiple
:针对每个入口打包一个runtime文件。
配置:
1 | optimization: { |
目录结构如下:
1 | dist |
-
single
:打包一个runtime文件。
配置:
1 | optimization: { |
目录结构如下:
1 | dist |
-
对象:name属性决定runtimeChunk的名称。
配置:
1 | optimization: { |
目录结构如下:
1 | dist |
动态导入(dynamic import)
前面的的两种代码分离,在首页加载的时候,其实还是需要全部加载的,只不过抽离出来使各个JS文件的体积变小,加载更快,能够更快的进行页面渲染操作,而这个动态导入操作可以实现懒加载,即在需要的时候才去加载,从而达到按需加载的目的。
动态导入的实现前面已经提到过了,就是ES6的import()
语法。
前面说过了,对于动态导入的模块是会被打包成独立的文件的,所以我们不用在cacheGroup中配置,但这样的话,我们如果想修改它打包出来的文件名,需要在output.chunkFilename
中配置。
1 | output: { |
打包后的目录结构如下:
1 | dist |
我们发现默认情况下,[name]
和[id]
的值是相同的,我们如果想要修改[name]
的值,需要通过魔法注释(magic comment)方式:
1 | import(/* webpackChunkName: "test" */ './utils/asyn_test'); |
再次打包,目录结构如下:
1 | dist |
Prefetch和Preload
webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令(魔法注释),来告知浏览器:
-
prefetch(预获取):将来某些导航下可能需要的资源。
-
preload(预加载):当前导航下可能需要资源。
1 | import( |
与 prefetch 指令相比,preload 指令有许多不同之处:
-
preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
-
preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
-
preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
CDN
我们可以将我们的静态资源上传到 CDN,然后通过 CDN 的 URL 访问静态资源,减轻源服务器的压力,或者将一些资源的请求通过第三方服务器获取,我们自己编写的资源通过源服务器获取,这样都可以减轻我们自己的服务器的压力,使源服务器可以专注于处理动态请求(如API、数据库操作),提升整体性能。
在 Webpack 中,默认会将我们引入的模块源代码进行打包,但我们希望某些包是通过CDN引入的,从而减少打包产物的体积,提高加载速度,我们需要对externals
进行配置。
Webpack 的 externals 配置用于排除某些依赖模块被打包到最终输出文件中,转而依赖运行环境(如全局变量、CDN 引入等)提供的模块。
webpack 进行 externals 配置后的原理图(由DeepSeek生成):

externals
配置的核心原理其实就是对里面设置的项进行源码打包排除,并将对其引入的模块名进行对应的externals
配置的值的替换。所以webpack仅仅是帮助我们进行排除打包和名称替换,我们的项目还是没有源码的,所以我们需要在html模板中引入相应的第三方链接,比如lodash的CDN链接:
1 |
|
注意:externals 配置的项的值一定要是其对应的全局导出变量名,否则会找不到对应的资源。
MiniCssExtractPlugin
我们发现,在之前的打包操作中,css 文件是打包在 js 文件中的,并且是以内部样式的形式插入到HTML中的,但css是由js生成的形式,这样可能会导致首页闪烁(有些浏览器的优化策略,会先渲染出无样式的HTML结构),还有不利于缓存,每次都需要重新生成等问题。
MiniCssExtractPlugin 是一个 webpack 插件,用于将 CSS 提取到单独的文件中。
首先我们需要先安装这个插件:
1 | npm i -D mini-css-extract-plugin |
然后我们还需要配置loader和plugin:
1 | // const MiniCssExtractPlugin = require('mini-css-extract-plugin'); |
JS压缩
为了进一步减小打包产物的体积,我们可以对其进行代码压缩并进行丑化(去除没必要的空格空行,对一些常量直接进行替换或将变量名进行简(丑)化表示)。
我们可以通过Terser工具对代码进行压缩操作。
Terser
Terser是一个JavaScript的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集。
早期我们可以使用UglifyJS对代码进行压缩操作,但由于目前它已不再维护,并且不支持ES6的语法,而Terser继承了UglifyJS大部分的功能,并且支持ES6的语法,目前推荐使用Terser作为代码压缩工具。
Terser和Babel、Postcss一样,都是一个单独的工具,我们可以单独使用Terser进行一系列操作。
首先就是要对其进行安装:
1 | npm i -g terser |
Terser的使用:
1 | # terser [input files] [options] |
命令行操作可以文档:https://terser.org/docs/cli-usage/
但上述操作仅仅只是去除了空行来压缩了代码,但并没有达到上文我们所说的效果,我们还需要进行其他的配置:
https://github.com/terser/terser#compress-options:如何优化和精简代码逻辑本身的配置选项
https://github.com/terser/terser#mangle-options:如何缩短变量和属性名称来减小文件大小的配置选项
这里简单列出几个常见的Compress和Mangle的options:
-
Compress option
:- arrows:class或者object中的函数,转换成箭头函数。
- arguments:将函数中使用 arguments[index]转成对应的形参名称。
- dead_code:移除不可达的代码(tree shaking)。
-
Mangle option
:- toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换)。
- keep_classnames:默认值是false,是否保持依赖的类名称。
- keep_fnames:默认值是false,是否保持原来的函数名称。
命令行示例如下:
1 | npx terser ./utils/asyn_test.js -o foo.min.js -c arrows,arguments=true,dead_code -m toplevel=true,keep_classnames=false,keep_fnames=false |
terser-webpack-plugin
在Webpack中,我们可以通过 TerserPlugin 插件来对代码进行压缩。
在Webpack5中,已经默认集成了这个插件,我们只需要配置optimization.minimize
为true即可启用该插件,在生产环境下默认启用该配置。
1 | optimization: { |
当然,我们也可以通过optimization.minimizer
属性来定制化配置TerserPlugin,前提一定要设置minimize: true
。
1 | optimization: { |
CSS压缩
对于CSS的压缩,通常是去除无用的空格、空行等。
在Webpack中,我们通常通过css-minimizer-webpack-plugin
插件处理CSS的压缩。
此插件的内部使用的则是cssnano工具。
1 | optimization: { |
此插件也可以进行一些定制化配置,具体可以自行查阅上方文档。
Tree Shaking
Tree Shaking
是一个 JavaScript 特性,源于 rollup 打包工具,用于去除 JavaScript 中未使用的代码。
Tree Shaking
主要依赖于 ESModule 的静态语法分析,与解析 ESModule 时的构造阶段有关,相关内容可以看这篇文章:ESModule解析流程。
在Webpack中,有两种方式可以实现Tree Shaking
:
-
usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的。
-
sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用。
文档链接:https://webpack.docschina.org/guides/tree-shaking/#clarifying-tree-shaking-and-sideeffects。
usedExports
usedExports 需要与 Terser 结合使用,直接在配置文件中进行optimization.usedExports
配置。
使用前,一定要将minimize
设置为true
,前面说过了,usedExports依赖Terser。
1 | optimization: { |
但usedExports的使用是有弊端的,具体可以看上方文档:

sideEffects
sideEffects用于告知webpack compiler
哪些模块时有副作用的,它需要在package.json
中声明。
-
如果我们将sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports。
-
如果有一些我们希望保留,可以设置为数组。
1 | { |
CSS实现Tree Shaking
CSS要实现Tree Shaking
,需要使用PurgeCss插件:
1 | npm i purgecss-webpack-plugin -D |
我们同时需要借助一个文件路径匹配工具——glob,glob 提供了声明式的文件匹配能力,使 PurgeCSS 能高效、准确地定位所有可能使用CSS的源文件,这是实现CSS Tree Shaking的基础。
1 | npm i glob -D |
webpack配置:
1 | // const glob = require('glob'); |
注意:purgecss也可以对less文件进行处理(因为它是对打包后的css进行tree shaking操作)。
Scope Hoisting
Scope Hoisting从webpack3开始增加的一个新功能,功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快。
默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
-
无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数。
-
Scope Hoisting可以将函数合并到一个模块中来运行。
在生产模式下,Scope Hoisting会默认开启。
在开发环境中,我们需要手动开启:
1 | // const webpack = require('webpack'); |
HTTP压缩
HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式。
HTTP压缩的流程:
-
HTTP数据在服务器发送前就已经被压缩了。
-
兼容的浏览器在向服务器发送请求时,会通过请求头
Accept-Encoding
告知服务器自己支持哪些压缩格式。 -
服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头
Content-Encoding
中告知浏览器。
Webpack中对文件的压缩,我们可以通过compression-webpack-plugin
插件来实现:
1 | npm i -D compression-webpack-plugin |
在webpack.config.js
中添加如下配置:
1 | plugins: [ |
webpack打包后文件分析
我们可以通过webpack-bundle-analyzer
工具查看打包后的文件大小。
1 | npm i -D webpack-bundle-analyzer |
在webpack配置文件中添加如下代码:
1 | plugins: [new BundleAnalyzerPlugin()]; |
进行以上配置后,在打包webpack的时候,这个工具是帮助我们打开一个8888端口上的服务,我们可以直接的看到每个包的大小。