theme: devui-blue
highlight: vs2015
前言
原文来自 我的个人博客
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
的支持;
✓ https://github.com/webpack/changelog-v5#commonjs-tree-shaking
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