webpack 性能优化方案
前言
原文来自 我的个人博客
webpack 作为前端目前使用最广泛的打包工具,在面试中也是经常会被问到的。
比较常见的面试题包括:
- 可以配置哪些属性来进行
webpack性能优化? - 前端有哪些常见的性能优化?(除了其他常见的,也完全可以从
webpack来回答)
webpack 的性能优化比较多,我们可以对其进行分类:
- 打包后的结果,上线时的性能优化。(比如分包处理、减小包体积、CDN服务器等)
- 优化打包速度,开发或者构建时优化打包速度。(比如
exclude、cache-loader等)
大多数情况下,我们会更加侧重于 第一种,因为这对线上的产品影响更大。
虽然在大多数情况下,webpack 都帮我们做好了该有的性能优化:
- 比如配置
mode为production或者development时,默认webpack的配置信息; - 但是我们也可以针对性的进行自己的项目优化;
本章,就让我们来学习一下 webpack 性能优化的更多细节
1. 代码分离 Code Spliting
代码分离(Code Spliting) 是 webpack 一个非常重要的特性,它主要的目的是将代码剥离到不同的 bundle 中,之后我们可以按需加载,或者并行加载这些文件。
什么意思呢?举个例子:
- 当 没有使用代码分离 时:
webpack将项目中的所有代码都打包到 一个index.js文件中(假如这个文件有10M)- 当我们在生产环境去访问页面时,浏览器必须得将这
10M的index.js文件全部下载解析执行后页面才会开始渲染。 - 假如此时的网速是
10M/s,那么光是去下载这个index.js文件会花去1s。(这1s中内页面是白屏的) - 在改动了部分代码第二次打包后,因为是全新的文件,浏览器又要重新下载一次
- 当 使用代码分离 时:
webpack将项目中的所有代码都打包到是 多个js文件中(我们假设每个文件都为1M)- 当我们在生产环境去访问页面时,此时浏览器将
1M的index.js文件下载就只需要0.1s了,至于其它的文件,可以选择需要用到它们时候加载或者和index.js文件并行的下载 - 在改动了部分代码第二次打包后,浏览器可以值下载改动过的代码文件,对于没改动过的文件可以直接从缓存中拿去。
通过以上的例子,相信大家应该能理解 代码分离 的好处了,那么在 webpack 如何能实现代码分离呢?
webpack 常用的代码分离方式有三种:
- 入口起点:使用
entry配置手动分离代码; - 防止重复:使用
EntryDependencies或者SplitChunksPlugin去重和分离代码: - 动态导入:通过模块的内联函数用来分离代码
1.1 方式一:多入口起点
这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。
先来看看如何从 main bundle 中分离 another module(另一个模块)
1.1.1 没有代码分离时
创建一个小的 demo:
- 首先我们创建一个目录,初始化
npm,然后在本地安装webpack、webpack-cli、loadsh
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli lodash --save-dev
- 创建
src/index.js:
import _ from "lodash";
console.log(_);
- 创建
src/another-module.js:
import _ from 'lodash';
console.log(_);
- 创建
webpack.config.js:
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
};
- 在
package.json中添加命令:
"scripts": {
"build": "webpack"
},
- 执行命令进行打包:
npm run build
- 生成如下构建结果:

可以看到此时生成了一个 554KB 的 main.js 文件
1.1.2 有代码分离时
接下来我们从 main bundle 中分离出 another module(另一个模块)
- 修改
webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
- entry: './src/index',
+ entry: {
+ index: './src/index',
+ another: './src/another-module.js'
+ },
output: {
path: path.resolve(__dirname, "dist"),
- filename: "main.js",
+ filename: "[name].main.js",
},
};
- 打包,生成如下构建结果:

我们发现此时已经成功打包出 another.bundle.js 和 index.bundle.js 两个文件了,但是文件的大小似乎有些问题,怎么两个都是 554KB?
正如前面提到的,这种方式存在一些隐患:
- 如果入口
chunk之间包含一些重复的模块,那些重复模块都会被引入到各个bundle中。 - 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。
以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用。在下一小节我们将移除重复的模块。
1.1.3 优化:移除重复的模块
在通过多入口分离代码的方式中,我们可以通过配置 dependOn 这个选项来解决重复模块的问题,它的原理就是从两个文件中抽出一个共享的模块,然后再让这两个模块依赖这个共享模块。
- 修改
webpack.config.js配置文件:
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: ['lodash'],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
- 打包,生成如下构建结果:

可以看到 index.mian.js 和 another.mian.js 中重复引用的部分被抽离成了 shared.main.js 文件,且 index.mian.js 和 another.mian.js 文件大小也变小了。
1.2 方式二:splitChunks 模式
另外一种分包的模式是 splitChunks,它底层是使用 SplitChunksPlugin 来实现的:
SplitChunksPlugin插件可以将公共的依赖模块提取到已有的入口chunk中,或者提取到一个新生成的chunk。
因为该插件 webpack 已经默认安装和集成,所以我们并 不需要单独安装和直接使用该插件;只需要提供 SplitChunksPlugin 相关的配置信息即可
webpack 提供了 SplitChunksPlugin 默认的配置,我们也可以手动来修改它的配置:
- 比如默认配置中,
chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all,
1.2.1 splitChunk 的配置
- 在
1.1.2的基础上修改webpack.cofig.js:
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
- 打包,生成如下构建结果:

使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。
除了 webpack 默认继承的 SplitChunksPlugin 插件,社区中也有提供一些对于代码分离很有帮助的 plugin 和 loader,比如:
mini-css-extract-plugin: 用于将 CSS 从主应用程序中分离。
1.2.2 SplitChunks 自定义配置解析
关于 optimization.splitChunks 文档上有很详细的记载,我这里讲你叫几个常用的:
1. Chunks:
- 默认值是
async - 另一个值是
initial,表示对通过的代码进行处理 all表示对同步和异步代码都进行处理
2. minSize :
- 拆分包的大小, 至少为 `minSize;
- 如果一个包拆分出来达不到
minSize,那么这个包就不会拆分;
3. maxSize:
- 将大于maxSize的包,拆分为不小于minSize的包;
4. cacheGroups:
- 用于对拆分的包就行分组,比如一个
lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包; test属性:匹配符合规则的包;name属性:拆分包的name属性;filename属性:拆分包的名称,可以自己使用placeholder属性;
- 修改
webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: {
index: "./src/index.js",
another: "./src/another-module.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
},
optimization: {
splitChunks: {
chunks: "all",
// 拆分包的最小体积
// 如果一个包拆分出来达不到 minSize,那么这个包就不会拆分(会被合并到其他包中)
minSize: 100,
// 将大于 maxSize 的包,拆分成不小于 minSize 的包
maxSize: 10000,
// 自己对需要拆包的内容进行分组
cacheGroups: {
自定义模块的name: {
test: /node_modules/,
filename: "[name]_vendors.js",
},
},
},
},
};
- 打包,生成如下构建结果:

1.3 方式三:动态导入(dynamic import)
另外一个代码拆分的方式是动态导入时,webpack 提供了两种实现动态导入的方式:
- 第一种,使用
ECMAScript中的import()语法来完成,也是目前推荐的方式; - 第二种,使用
webpack遗留的require.ensure,目前已经不推荐使用;
动态 import 使用最多的一个场景是懒加载(比如路由懒加载)
1.3.1 import 方式
接着从 1.1.2 小节代码的基础上修改:
- 修改
webpack.confg.js:
const path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
},
};
-
删除
src/another-module.js文件 -
修改
src/index.js,不再使用statically import(静态导入)lodash,而是通过dynamic import(动态导入) 来分离出一个chunk:
const logLodash = function () {
import("lodash").then(({ default: _ }) => {
console.log(_);
});
};
logLodash();
之所以需要 default,是因为 webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,而是为 CommonJS 模块创建一个 artificial namespace 对象。
- 打包,生成如下构建结果:

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。下面是如何通过 async 函数简化代码:
const logLodash = async function () {
const { default: _ } = await import("lodash");
console.log(_);
};
logLodash();
1.3.2 动态导入的文件命名
因为动态导入通常是一定会打包成独立的文件的,所以并不会再 cacheGroups 中进行配置;
它的命名我们通常会在 output 中,通过 chunkFilename 属性来命名:
- 修改
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
+ chunkFilename: "chunk_[name].js""
},
};
- 打包构建:

如果对打包后的 [name] 不满意,还可以通过 magic comments(魔法注释)来修改:
1, 修改 src/index.js:
const logLodash = async function () {
const { default: _ } = await import(/*webpackChunkName: 'lodash'*/ "lodash");
console.log(_);
};
logLodash();
- 打包构建

1.4 CDN 加速
CDN 称之为 内容分发网络(Content Delivery Network 或 Content Distribution Network,缩写:CDN)
- 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器;
- 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户;
- 来提供高性能、可扩展性及低成本的网络内容传递给用户;

在开发中,我们使用 CDN 主要是两种方式:
- 方式一:打包的所有静态资源,放到
CDN服务器,用户所有资源都是通过CDN服务器加载的; - 方式二:一些第三方资源放到
CDN服务器上;
1.4.1 配置自己的 CDN 服务器
如果所有的静态资源都想要放到 CDN 服务器上,我们需要购买自己的 CDN 服务器;
- 目前阿里、腾讯、亚马逊、
Google等都可以购买CDN服务器; - 我们可以直接修改
publicPath,在打包时添加上自己的CDN地址;
- 在
1.3.1的基础上安装HtmlWebpackPlugin插件:
npm install --save-dev html-webpack-plugin
- 修改
webpack.config.js文件:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
chunkFilename: "chunk_[name].js",
+ publicPath: "https://yejiwei.com/cdn/",
},
plugins: [new HtmlWebpackPlugin()],
};
- 打包构建

可以发现我们打包后的 script 标签自动添加了 CDN 服务器地址的前缀。
1.4.2 配置第三方库的CDN服务器
通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的 CDN 服务器上:
- 国际上使用比较多的是
unpkg、JSDelivr、cdnjs; - 国内也有一个比较好用的
CDN是bootcdn;
在项目中,我们如何去引入这些 CDN 呢?
- 第一,在打包的时候我们不再需要对类似于
lodash或者dayjs这些库进行打包; - 第二,在
html模块中,我们需要自己加入对应的CDN服务器地址;
- 创建
public/index.html模版,手动加上对应CDN服务器地址
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.core.min.js"></script>
</head>
<body></body>
</html>
- 在
1.3.1的基础上修改webpack.config.js配置,来排除一些库的打包并配置html模版:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
chunkFilename: "chunk_[name].js",
},
plugins: [
new HtmlWebpackPlugin({
+ template: "./public/index.html",
}),
],
+ externals: {
+ lodash: "_",
+ },
};
- 打包构建


1.5 补充
以下补充了解即可(一些细节)
1.5.1 解决注释的单独提取
如果将 webpack.config.js 的 mode 改为 production 也就是生产环境时,经常会看到一写 .txt 后缀的注释文件

这是因为在 production 默认情况下,webpack 再进行分包时,有对包中的注释进行单独提取。
这个包提取是由另一个插件(TerserPlugin 后面会细说) 默认配置的原因,如果想去掉可以做以下配置:

1.5.2 chunkIds 的生成方式
optimization.chunkIds 配置用于告知 webpack 模块的 id 采用什么算法生成。
有三个比较常见的值:
natural:按照数字的顺序使用id;named:development下的默认值,一个可读的(你能看的懂得)名称的id;deterministic:确定性的,在不同的编译中不变的短数字id- 在
webpack4中是没有这个值的; - 那个时候如果使用
natural,那么在一些编译发生变化时,就需要重新进行打包就会有问题;
- 在
最佳实践:
- 开发过程中,我们推荐使用
named; - 打包过程中,我们推荐使用
deterministic;
1.5.3. runtimeChunk 的配置
配置 runtime 相关的代码是否抽取到一个单独的 chunk 中:
runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;- 比如我们的
index中通过import函数相关的代码加载,就是通过runtime代码完成的; 抽离出来后,有利于浏览器缓存的策略: - 比如我们修改了业务代码(
main),那么runtime和component、bar的chunk是不需要重新加载的; - 比如我们修改了
component、bar的代码,那么main中的代码是不需要重新加载的; 设置的值: true/multiple:针对每个入口打包一个runtime文件;single:打包一个runtime文件;- 对象:
name属性决定runtimeChunk的名称;
对于每个
runtime chunk,导入的模块会被分别初始化,因此如果你在同一个页面中引用多个入口起点,请注意此行为。你或许应该将其设置为single,或者使用其他只有一个runtime实例的配置。
1.5.4. Prefetch 和 Preload
webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,来告知浏览器:
prefetch(预获取):将来某些导航下可能需要的资源preload(预加载):当前导航下可能需要资源
与 prefetch 指令相比,preload 指令有许多不同之处:
preload chunk会在父chunk加载时,以并行方式开始加载。prefetch chunk会在父chunk加载结束后开始加载。preload chunk具有中等优先级,并立即下载。prefetch chunk在浏览器闲置时下载。preload chunk会在父chunk中立即请求,用于当下时刻。prefetch chunk会用于未来的某个时刻。
推荐使用 prefetch ,因为它是在未来闲置的时候下载,有些东西是不需要立即下载的,这样做不会因为请求不重要的资源而占用网络带宽。
2. Shimming 预制依赖
shimming 是一个概念,是某一类功能的统称:
- 翻译过来我们称之为 垫片,相当于给我们的代码填充一些垫片来处理一些问题;
- 比如我们现在依赖一个第三方的库,这个第三方的库本身依赖
lodash,但是默认没有对lodash进行导入(认为全局存在lodash),那么我们就可以通过ProvidePlugin来实现shimming的效果;
注意:
webpack并不推荐随意的使用shimming。Webpack背后的整个理念是使前端开发更加模块化;也就是说,需要编写具有封闭性的、不存在隐含依赖(比如全局变量)的彼此隔离的模块;
2.1 Shimming 预支全局变量
假如一个文件中我们使用了 axios,但是没有对它进行引入,那么下面的代码是会报错的;
axios.get('XXXXX').then(res => {
console.log(res)
})
get('XXXXX').then(res => {
console.log(res)
})
我们可以通过使用 ProvidePlugin 来实现 shimming 的效果:
- 修改
webpack.config.js:
new ProvidePlugin({
axios: 'axios',
get: ['axios','get']
})
ProvidePlugin能够帮助我们在每个模块中,通过一个变量来获取一个package;- 如果
webpack看到这个模块,它将在最终的bundle中引入这个模块; - 另外
ProvidePlugin是webpack默认的一个插件,所以不需要专门导入;
这段代码的本质是告诉webpack: 如果你遇到了至少一处用到 axios 变量的模块实例,那请你将 axios package 引入进来,并将其提供给需要用到它的模块。
3. TerserPlugin 代码压缩
在了解 TerserPlugin 插件前,我们先来认识一下什么是 Terser 。
3.1 Terser 介绍
什么是 Terser 呢?
Terser是一个JavaScript的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集;- 早期我们会使用
uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的语法; Terser是从uglify-es fork过来的,并且保留它原来的大部分API以及适配uglify-es和uglify-js@3等;
也就是说,Terser 可以帮助我们压缩、丑化我们的代码,让我们的 bundle 变得更小。
我们现在就来用一下 Terser,因为 Terser 是一个独立的工具,所以它可以单独安装:
# 全局安装
npm install terser -g
# 局部安装
npm install terser -D
可以在命令行中使用 Terser:
terser [input files] [options]
# 举例说明
terser js/file1.js -o foo.min.js -c -m

我们这里来讲解几个 Compress option 和 Mangle(乱砍) option:
Compress option:
- arrows:class或者object中的函数,转换成箭头函数;
- arguments:将函数中使用 arguments[index]转成对应的形参名称;
- dead_code:移除不可达的代码(tree shaking);
Mangle option:
- toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换);
- keep_classnames:默认值是false,是否保持依赖的类名称;
- keep_fnames:默认值是false,是否保持原来的函数名称;
3.2 Terser 在 webpack 中配置(JS 的压缩)
真实开发中,我们不需要手动的通过 terser 来处理我们的代码,我们可以直接通过 webpack 来处理:
- 在
webpack中有一个minimizer属性,在production模式下,默认就是使用TerserPlugin来处理我们的代码的; - 如果我们对默认的配置不满意,也可以自己来创建
TerserPlugin的实例,并且覆盖相关的配置;
修改 webpack.config.js 配置:
const TerserPlugin = require("terser-webpack-plugin");
...
optimization: {
// 打开minimize,让其对我们的代码进行压缩(默认production模式下已经打
minimize: true,
minimizer: [
new TerserPlugin({
// extractComments:默认值为true,表示会将注释抽取到一个单独的文件中;
// 在开发中,我们不希望保留这个注释时,可以设置为false;
extractComments: false,
// parallel:使用多进程并发运行提高构建的速度,默认值是true
// 并发运行的默认数量: os.cpus().length - 1;
// 我们也可以设置自己的个数,但是使用默认值即可;
// parallel: true,
// terserOptions:设置我们的terser相关的配置
terserOptions: {
// 设置压缩相关的选项;
compress: {
unused: false,
},
// 设置丑化相关的选项,可以直接设置为true;
mangle: true,
// 顶层变量是否进行转换;
toplevel: true,
// 保留类的名称;
keep_classnames: true,
// 保留函数的名称;
keep_fnames: true,
},
}),
],
},
3.3 CSS 的压缩
上面我们讲了 JS 的代码压缩,而在我们的前端项目中另一类占大头的代码就是 CSS :
CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin;css-minimizer-webpack-plugin是使用cssnano工具来优化、压缩CSS(也可以单独使用);
- 安装
css-minimizer-webpack-plugin:
npm install css-minimizer-webpack-plugin -D
- 在
optimization.minimizer中配置:

4. Tree Shaking
什么是 Tree Shaking ?
Tree Shaking是一个术语,在计算机中表示消除死代码(dead_code);- 最早的想法起源于
LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一); - 后来
Tree Shaking也被应用于其他的语言,比如JavaScript、Dart;
JavaScript 的 Tree Shaking:
- 对
JavaScript进行Tree Shaking是源自打包工具rollup; - 这是因为
Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系); webpack2正式内置支持了ES2015模块,和检测未使用模块的能力;- 在
webpack4正式扩展了这个能力,并且通过package.json的sideEffects属性作为标记,告知webpack在编译时,哪里文件可以安全的删除掉; webpack5中,也提供了对部分CommonJS的tree shaking的支持; ✓ github.com/webpack/cha…
4.1 webpack 实现 Tree Shaking
webpack 实现 Tree Shaking 采用了两种不同的方案:
usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的;sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用;
usedExports 按 sideEffects 这两个东西的优化是不同的事情。
引用官方文档的话: The sideEffects and usedExports(more konwn as tree shaking)optimizations are two different things
下面我们分别来演示一下这两个属性的使用
4.1.1 usedExports
- 新建一个
webpack-demo。
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli lodash --save-dev
- 创建
src/math.js文件:
export const add = (num1, num2) => num1 + num2;
export const sub = (num1, num2) => num1 - num2;
在这个问价中仅是导出了两个函数方法
- 创建
src/index.js文件:、
import { add, sub } from "./math";
console.log(add(1, 2));
在 index.js 中 导入了刚刚创建的两个函数,但是只使用了 add
- 配置
webpack.config.js:
const path = require("path");
module.exports = {
mode: "development",
devtool: false,
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
optimization: {
usedExports: true,
},
};
为了可以看到 usedExports 带来的效果,我们需要设置为 development 模式。因为在 production 模式下,webpack 默认的一些优化会带来很大的影响。
- 设置
usedExports为true和false对比打包后的代码:


仔细观察上面两张图可以发现当设置 usedExports: true 时,sub 函数没有导出了,另外会多出一段注释:unused harmony export mul;这段注释的意义是会告知 Terser 在优化时,可以删除掉这段代码。
这个时候,我们将 minimize 设置 true:


usedExports设置为false时,sub函数没有被移除掉;usedExports设置为true时,sub函数有被移除掉;
所以,usedExports 实现 Tree Shaking 是结合 Terser 来完成的。
4.1.2 sideEffects
在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。
通过 package.json 的 "sideEffects" 属性,来实现这种方式。
{
"name": "your-project",
"sideEffects": false
}
如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。
"side effect(副作用)"的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个export或多个export。举例说明,例如polyfill,它影响全局作用域,并且通常不提供export。
如果你的代码确实有一些副作用,可以改为提供一个数组:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
注意,所有导入文件都会受到
tree shaking的影响。这意味着,如果在项目中使用类似css-loader并import一个CSS文件,则需要将其添加到side effect列表中,以免在生产模式中无意中将它删除:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
4.2 CSS 实现 Tree Shaking
上面将的都是关于 JavaScript 的 Tree Shaking ,对于 CSS 同样有对应的 Tree Shaking 操作。
-
在早期的时候,我们会使用
PurifyCss插件来完成CSS的tree shaking,但是目前该库已经不再维护了(最新更新也是在4年前了); -
目前我们可以使用另外一个库来完成
CSS的Tree Shaking:PurgeCSS,也是一个帮助我们删除未使用的CSS的工具;
- 安装
PurgeCss的webpack插件:
npm install purgecss-webpack-plugin -D
- 在
webpack.config.js中配置PurgeCss
new PurgeCSSPlugin({
paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }),
only: ['bundle', 'vendor']
})
paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;- 默认情况下,
Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
purgecss 也可以对 less、sass文件进行处理(它是对打包后的 css 进行 tree shaking 操作);
4.3 Scope Hoisting
Scope Hoisting 是从 webpack3 开始增加的一个新功能,它的功能是对作用域进行提升,并且让 webpack 打包后的代码更小、运行更快;
默认情况下 webpack 打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
- 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数
Scope Hoisting可以将函数合并到一个模块中来运行;(作用域提升,在主模块里直接运行它,而不是去加载一些单独的模块)
使用 Scope Hoisting 非常的简单,webpack 已经内置了对应的模块:
- 在
production模式下,默认这个模块就会启用; - 在
development模式下,我们需要自己来打开该模块;
new webpack.optimize.ModuleConcatenationPlugin()
5. webpack 对文件压缩
经过前几小节的代码压缩优化(Tree Shaking 的优化、Terser 的优化、CSS 压缩的优化),基本上已经没有什么可以通过删除一些代码再压缩文件的方法了(变量、空格、换行符、注释、没用的代码都已经处理了)
但是我们还有一种通过压缩算法从对文件压缩的方式来继续减小包的体积(就像在 winodows 将文件夹压缩成 zip 一样,只不过我们这里是对单个js文件进行压缩)
目前的压缩格式非常的多:
compress–UNIX的“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用gzip或deflate);deflate– 基于deflate算法(定义于RFC 1951)的压缩,使用zlib数据格式封装;gzip–GNU zip格式(定义于RFC 1952),是目前使用比较广泛的压缩算法;br– 一种新的开源压缩算法,专为HTTP内容的编码而设计;
在 webpack 中的配置:
- 安装
CompressionPlugin
npm install compression-webpack-plugin -D
- 配置
webpack.config.js:
new CompressionPlugin({
test: /].(css|js)$/, // 匹配哪些文件需要压缩
// threshold: 500, // 设置文件从多大开始压缩
minRatio: 0.7, // 至少的压缩比例
algorithm: "gzip, // 才用的压缩算法
// include
// exclude
})
6. HTML 文件中代码的压缩
我们之前使用了 HtmlWebpackPlugin 插件来生成 HTML 的模板,事实上它还有一些其他的配置:
- inject:设置打包的资源插入的位置
- true、 false 、body、head
- cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
- minify:默认会使用一个插件html-minifier-terser

转载自:https://juejin.cn/post/7188889051486191672