Webpack性能优化
说明
webpack常见的性能优化笔记总结,构建性能优化,传输性能优化,运行性能优化
# 分析工具
在优化之前,需要了解一些量化分析的工具,来帮助分析需要优化的点。
使用 speed-measure-webpack-plugin 可以看到每个loader和plugin的耗时情况
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap({
entry: './src/main.js',
...
})
2
3
4
5
6
7
8
# 性能优化一图流
# 定向查找
webpack
的 resolve
配置了模块会按照什么规则如何被解析,webpack
提供合理的默认值,但是还是可能会修改一些解析的细节。修改 resolve
配置加快构建速度
resolve: {
extensions: ['.js', '.jsx', '.ts'],
// 告诉 webpack 解析模块时应该搜索的目录 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
modules: [path.resolve(__dirname, 'node_modules')]
}
2
3
4
5
# 代码分离
代码分离(Code Splitting)是webpack一个非常重要的特性:
- 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
- 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页 的加载速度;
- 代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;
Webpack中常用的代码分离有三种:
- 入口起点:使用entry配置手动分离代码;
- 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
- 动态导入:通过模块的内联函数调用来分离代码;
entry:{
index: {
import: path.join(__dirname,'./src/index1.js'),
dependOn: 'shared'
},
main: {
import: './src/main.js',
dependOn: 'shared'
},
// 共享引入
shared: ['axios']
},
output:{
path:path.join(__dirname,'dist'),
// 多入口文件名称,[]是占位符
filename: '[name]-bundle.js',
clean: true,
// 懒加载文件命名
chunkFilename: '[id]_[name]_chunk.js',
// cdn服务器地址
// publicPath: ''
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 自定义分包
optimization: {
chunkIds: 'deterministic',
// 分包配置
splitChunks: {
chunks: "async",
// 包大于maxSize时继续分包
// maxSize: 40000,
// 将包拆成不小于minSize
minSize: 1,
cacheGroups: {
utils: {
test: /utils/,
filename: '[name]_utils.js',
},
vendors: {
test: /[\\/]node_modules[\\/]/,
filename: '[name]_vendors.js'
},
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成
- 有三个比较常见的值:
- natural:按照数字的顺序使用id;
- named:development下的默认值,一个可读的名称的id;
- deterministic:确定性的,在不同的编译中不变的短数字id
- 在webpack4中是没有这个值的;
- 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题;
最佳实践:
- 开发过程中,推荐使用named;
- 打包过程中,推荐使用deterministic;
# 预加载和预获取
- prefetch预获取:在父包加载结束后,浏览器空闲时下载文件,将来某些导航下可能需要的资源
- preload预加载:同主包文件一起下载,以并行方式加载,当前导航下可能需要资源
btn1.onclick = () => {
// 魔法注释 预获取
import(
/* webpackChunkName: "about" */
/* webpackPrefetch: true */
'./route/about'
)
}
btn2.onclick = () => {
import(
/*webpackChunkName: "cate" */
'./route/cate'
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# CDN分包
对于项目中我们用的一些比较大和比较多的包,例如 react 和 react-dom,我们可以通过 cdn 的形式引入它们,然后将 react
、react-dom
从打包列表中排除,这样可以减少打包所需的时间。排除部分库的打包需要借助 html-webpack-externals-plugin
插件
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
module.exports = {
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@17.0.2/umd/react.production.min.js',
global: 'React',
},
{
module: 'react-dom',
entry:
'https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js',
global: 'ReactDOM',
},
],
}),
],
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 优化loader性能
# 限制loader的应用范围
通常来说,loader会处理符合匹配规则的所有文件。比如babel-loader,会遍历项目中用到的所有js文件,对每个文件的代码进行编译转换。而node_modules里的js文件基本上都是转译好了的,不需要再次处理,所以使用 include/exclude 来避免这种不必要的转译。
module.exports = {
module:{
rules:[
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/
//或者 include: [path.resolve(__dirname, 'src')]
}
]
},
}
2
3
4
5
6
7
8
9
10
11
12
13
# 多进程构建
运行在Node.js之上的 Webpack 是单线程的,就算有多个任务同时存在,它们也只能一个一个排队执行。当项目比较复杂时,构建就会比较慢,如今大多数CPU都是多核的,我们可以借助一些工具,充分释放 CPU 在多核并发方面的优势
thread-loader 就是把它放置在其它loader之前,如下所示。放置在这个thread-loader
之后的 loaders会运行在一个单独的worker池中。
如果是小项目,不建议开启多进程构建,因为开启进程是需要花费时间的,构建速度反而会变慢。
module.exports = {
module:{
rules:[
{
test: /\.js$/,
use: ['thread-loader','babel-loader']
}
]
},
}
2
3
4
5
6
7
8
9
10
11
# 使用缓存
利用缓存可以提升二次构建速度。使用缓存后,在node_modules中会有一个.cache
目录,用于存放缓存的内容。
在一些性能开销较大的 loader 之前添加cache-loader,以将结果缓存到磁盘中。保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用。
// npm install cache-loader -D
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader','babel-loader']
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
# 代码压缩
# 压缩js
Terser可以帮助压缩、丑化代码,让bundle变得更小,npm install terser -D
在webpack中有一个minimizer属性,在production模式下也就是生产环境下会默认开启 js 的压缩,无需单独配置。
optimization: {
minimizer: [
// js压缩
new TerserPlugin({
extractComments: false, // 默认值为true,表示会将注释抽取到一个单独的文件中
terserOptions: {
}
})
]
}
2
3
4
5
6
7
8
9
10
# 压缩CSS
CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等 npm install css-minimizer-webpack-plugin -D
在optimization.minimizer中配置:
optimization: {
minimizer: [
// css压缩
new CSSMinimizerPlugin({})
]
}
2
3
4
5
6
# 压缩HTML
使用了HtmlWebpackPlugin插件来生成HTML的模板,它还有一些其他的配置:
- cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
- minify:默认会使用一个插件html-minifier-terser
module.export = {
plugins: [
new HtmlWebpackPlugin({ // 动态生成 html 文件
template: "./index.html",
cache: true,
minify: { // 压缩HTML
removeComments: true, // 移除HTML中的注释
collapseWhitespace: true, // 删除空⽩符与换⾏符
minifyCSS: true, // 压缩内联css
},
})
] }
2
3
4
5
6
7
8
9
10
11
12
# Tree Shaking
# JS Tree Shaking
- webpack实现Tree Shaking采用了两种不同的方案:
- usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的;
- sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用;
- usedExports实现
- 将mode设置为development模式:
- 为了可以看到 usedExports带来的效果,我们需要设置为 development 模式
- 因为在 production 模式下,webpack默认的一些优化会带来很大的影响。
- 设置usedExports为true和false对比打包后的代码:
- 在usedExports设置为true时,会有一段注释:unused harmony export mul告知Terser在优化时,可以删除掉这段代码;
- 将mode设置为development模式:
optimization: {
// 导入模块时分析模块中的哪些函数有被使用,哪些函数没有被使用.
usedExports: true,
}
2
3
4
- sideEffects实现
sideEffects用于告知webpack compiler哪些模块时有副作用的:
副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义;
如果我们将sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports;
如果有一些我们希望保留,可以设置为数组;
比如我们有一个format.js、style.css文件:
- 该文件在导入时没有使用任何的变量来接受;
- 那么打包后的文件,不会保留format.js、style.css相关的任何代码
在package.json中设置sideEffects的值
// package.json
{
"sideEffects": [
"./src/util/format.js"
"*.css"
]
}
2
3
4
5
6
7
在项目中对JavaScript的代码进行TreeShaking呢(生成环境)?(项目中最佳实践)
- 在optimization中配置usedExports为true,来帮助Terser进行优化;
- 在package.json中配置sideEffects,直接对模块进行优化;
# CSS Tree Shaking
安装PurgeCss的webpack插件:npm install purgecss-webpack-plugin -D
const path = require("path");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const glob = require("glob"); // 文件匹配模式 匹配某一文件夹下的所有文件
plugins: [
new PurgecssPlugin({
// 这里我的样式在根目录下的index.html里面使用,所以配置这个路径
paths: glob.sync(`${path.resolve(__dirname, './src')}/**/*`, { nodir: true }),
safelist: function() {
return {
standard: ["body"]
}
}
}),
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 作用提升 (Scope Hoisting)
Scope Hoisting
意思是“作用域提升”。在 JavaScript
中,也有类似的概念,“变量提升”、“函数提升”,JavaScript
会把函数和变量声明提升到当前作用域的顶部,Scope Hoisting
也是类似。webpack
会把引入的 js 文件“提升”顶部。
在没有使用 Scope Hoisting
的时候,webpack
的打包文件会将各个模块分开使用 __webpack_require__
导入,在使用了 Scope Hoisting
之后,就会把需要导入的文件直接移入使用模块的顶部。这样做的好处有
- 代码中函数声明和引用语句减少,减少代码体积
- 不用多次使用
__webpack_require__
调用模块,运行速度会的得以提升。
所以,Scope Hoisting
可以让 webpack
打包出来的代码文件体积更小,运行更快。Scope Hoisting
的原理也很简单,主要是其会分析模块之间的依赖关系,将那些只被引用一次的模块进行合并,减少引用的次数。
因为 Scope Hoisting
需要分析模块之间的依赖关系,所以源码必须采用 ES6 模块化语法。也就是说如果你使用非 ES6
模块或者使用 import()
动态导入的话,则不会有 Scope Hoisting
。
Scope Hoisting
是 webpack
内置功能,只需要在plugins
里面使用即可
module.exports = {
plugins: [
// 开启 Scope Hoisting 功能
new webpack.optimize.ModuleConcatenationPlugin()
]
}
------------------------------------------------------
module.exports = {
optimization: {
concatenateModules: true // 开启 Scope Hoisting 功能
},
}
2
3
4
5
6
7
8
9
10
11
12
# HTTP压缩
HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式;
HTTP压缩的流程
- HTTP数据在服务器发送前就已经被压缩了;(可以在webpack中完成)
- 兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;
GET /encrypted-area HTTP/1.1
Host: www . example.com
Accept-Encoding: gzip,def1ate
2
3
- 服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;
HTTP/1.1 200 oK
Date: Tue,27 Feb 2018 06:03:16 GMT
Last-Modified: wed,08 Jan 2003 23:11:55 GMT
Accept-Ranges: bytes
content-Length: 438
connection: close
content-Type: text/html ; charset=UTF-8
content-Encoding: gzip
2
3
4
5
6
7
8
# 参考
webpack性能优化方案(详细) - 掘金 (juejin.cn) (opens new window)
一文搞定webpack构建优化策略 - 掘金 (juejin.cn) (opens new window)
一套骚操作下来,webpack 项目打包速度飞升🚀、体积骤减↓ - 掘金 (juejin.cn) (opens new window)
webpack 性能优化策略🦁 - 掘金 (juejin.cn) (opens new window)
webpack进阶之性能优化(webpack5最新版本) - 掘金 (juejin.cn) (opens new window)