保持最新的版本
使用最新版本,带来的优化是最显著的,开发者们会经常进行性能优化。所以第一步先升级 Webpack 以及 所有构建相关 devDependencies 的版本。
保持最新的 Node.js 也能够保证性能。除此之外,保证包管理工具 (例如 npm 或者 yarn ) 为最新也能保证性能。较新的版本能够建立更高效的模块树以及提高解析速度。
量化
必须要有一个量化指标才可以看出前后对比。
speed-measure-webpack-plugin
speed-measure-webpack-plugin
插件可以测量各个插件和 loader 所花费的时间,使用之后,构建时,会得到类似下面这样的信息:
![image](https://user-images.githubusercontent.com/4001228/80656976-02dd6980-8ab5-11ea-9797-4cd273786194.png)
对比前后的信息,来确定优化的效果。
speed-measure-webpack-plugin
的使用很简单,可以直接用来包裹 Webpack 的配置:
webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const config = {
//...webpack配置
}
module.exports = smp.wrap(config);
Webpack 自带时间统计
上面的 speed-measure-webpack-plugin
主要是测量插件以及 loader 花费的时间(主要是 loader),其自身也是耗时间的,因此在对其分析并相应的做出优化之后,应该去除 speed-measure-webpack-plugin
后根据 Webpack 每次打包给出时间作为最后的量化指标。
缩小文件搜索范围(更快命中)
优化 loader 配置
通过排除 node_modules
下的文件 从而缩小了 loader 加载搜索范围 高概率命中文件。为了尽可能少的让文件被 loader 处理,可以通过 include
去命中只有哪些文件需要被处理
优化 resolve.modules 配置
resolve.modules
用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是 ['node modules']
,含义是先去当前目录的 ./node modules
目录下去找我们想找的模块,如果没找到,就去上一级目录 ../node modules
中找,再没有就去 ../.. /node modules
中找,以此类推,这和 Node.js 的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的 ./node modules
目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。
优化后配置:
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
modules: [path.resolve(__dirname,'node_modules')]
},
注意:如果配置了上述的 resolve.moudles
,可能会出现问题,例如,依赖中还存在 node_modules
目录,那么就会出现,对应的文件明明在,但是却提示找不到。因此呢,不推荐配置这个。如果其他人不熟悉这个配置,遇到这个问题时,会摸不着头脑。
优化 resolve.alias 配置
创建 import 或 require 的路径别名,来确保模块引入变得更简单。配置项通过别名来把原导入路径映射成一个新的导入路径 此优化方法会影响使用 Tree-Shaking 去除无效代码
优化 resolve.extensions 配置
当引入模块时不带文件后缀 webpack会根据此配置自动解析确定的文件后缀。
- 后缀列表尽可能小
- 频率最高的往前放
- 导出语句尽可能带上后缀
resolve: {
extensions: ['.js']
}
优化 module.noParse 配置
用了 noParse 的模块将不会被 loaders 解析,所以当我们使用的库如果太大,并且其中不包含 import
require
define
的调用,我们就可以使用这项配置来提升性能, 让 Webpack 忽略对部分没采用模块化的文件的递归解析处理。
// 忽略对jquery lodash的进行递归解析
module: {
// noParse: /jquery|lodash/
// 从 webpack 3.0.0 开始
noParse: function(content) {
return /jquery|lodash/.test(content)
}
}
优化 resolve.mainFields 配置
在安装的第三方模块中都会有一个 package.json
文件,用于描述这个模块的属性,其中可以存在多个字段描述入口文件,原因是某些模块可以同时用于多个环境中,针对不同的运行环境需要使用不同的代码。
resolve.mainFields
的默认值和当前的 target
配置有关系,对应的关系如下。
target
为 web
或者 webworker
时,值是 ['browser','module','main']
。
target
为其他情况时,值是 ['module','main']
。
以 target
等于 we
b 为例, Webpack 会先采用第三方模块中的 browser
字段去寻找模块的入口文件,如果不存在,就采用 module
字段,以此类推。
为了减少搜索步骤,在明确第三方模块的入口文件描述字段时,可以将它设置得尽量少。由于大多数第三方模块都采用 main
字段去描述入口文件的位置,所以可以这样配置:
module.exports = {
resolve: {
//只采用 main 字段作为入口文件的描述字段,以减少搜索步骤
mainFields: ['main']
}
}
优化解析时间(开启多进程打包)
thread-loader
把 thread-loader
放置在其它 loader
之前,那么放置在这个 loader
之后的 loader
就会在一个单独的 worker
池中运行。
在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:
- 这些
loader
不能产生新的文件。
- 这些
loader
不能使用定制的 loader
API(也就是说,通过插件)。
- 这些
loader
无法获取 webpack
的选项设置。
thread-loader
使用起来也非常简单,只要把 thread-loader
放置在其他 loader 之前, 那 thread-loader
之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// 创建一个 js worker 池
use: [
'thread-loader',
'babel-loader'
]
},
{
test: /\.s?css$/,
exclude: /node_modules/,
// 创建一个 css worker 池
use: [
'style-loader',
'thread-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
importLoaders: 1
}
},
'postcss-loader'
]
}
// ...
]
// ...
}
// ...
}
官方上说每个 worker 大概都要花费 600ms ,所以官方为了防止启动 worker 时的高延迟,提供了对 worker 池的优化:预热。
// ...
const threadLoader = require('thread-loader');
const jsWorkerPool = {
// options
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
// 当 require('os').cpus() 是 undefined 时,则为 1
workers: 2,
// 闲置时定时删除 worker 进程
// 默认为 500ms
// 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000
};
const cssWorkerPool = {
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 2,
poolTimeout: 2000
};
threadLoader.warmup(jsWorkerPool, ['babel-loader']);
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: jsWorkerPool
},
'babel-loader'
]
},
{
test: /\.s?css$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'thread-loader',
options: cssWorkerPool
},
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
importLoaders: 1
}
},
'postcss-loader'
]
}
// ...
]
// ...
}
// ...
}
注意:
thread-loader
放在了 style-loader
之后,这是因为 thread-loader
后的 loader 没法存取文件也没法获取 webpack 的选项设置。
- 请仅在耗时的 loader 上使用。
HappyPack(已不维护,不推荐 Webpack4 使用)
HappyPack 是让 webpack 对 loader 的执行过程,从单一进程形式扩展为多进程模式,也就是将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而加速代码构建。与 DLL 动态链接库结合来使用更佳。
合理利用缓存(缩短连续构建时间,增加初始构建时间)
开启缓存之后,在第一构建之后,会产生缓存文件,一般默认缓存目录:node_modules/.cache
。
cache-loader
将结果缓存到磁盘里,显著提升二次构建速度。
module.exports = {
module: {
rules: [
{
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};
注意:
- 保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
thread-loader
和 cache-loader
要一起使用的話,先放 cache-loader
,接着是 thread-loader
,最后才是其它的 heavy-loader
,这样的順序才可以有最好的效能。
- 如果只打算给
babel-loader
配置 cache 的话,也可以不使用 cache-loader
,给 babel-loader
增加选项 cacheDirectory
为 true
。
HardSourceWebpackPlugin
HardSourceWebpackPlugin
为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source
。
配置 hard-source-webpack-plugin
,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。
DllPlugin
在一个动态链接库中可以包含其他模块调用的函数和数据,动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会被重新编译,而是直接使用动态链接库中的代码。
- 将web应用依赖的基础模块抽离出来,打包到单独的动态链接库中。一个链接库可以包含多个模块。
- 当需要导入的模块存在于动态链接库,模块不会再次打包,而是去动态链接库中去获取。
- 页面依赖的所有动态链接库都需要被加载。
如何使用不记录了,因为现在已经不推荐使用 DllPlugin 了,在最新的 webpack 下带来的性能提升有限。
TerserPlugin
Webpack4 默认内置使用 terser-webpack-plugin
插件压缩优化代码,而该插件使用 terser 来缩小 JavaScript。所谓 terser,官方给出的定义是用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。
使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1
。
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
}),
],
},
};
可以显著加快构建速度,因此强烈推荐开启多进程。
ParallelUglifyPlugin()
Webpack4 已废弃,不推荐 Webpack4 使用。
CSS 代码的压缩
通过 mini-css-extract-plugin
提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader
的 minimize 选项开启 cssnano
压缩 CSS。
控制包文件大小
减少编译的整体大小,以提高构建性能。尽量保持 chunks 小巧。
结合stats.json分析打包结果(bundle analyze)
提取页面公共资源
基础包分离:
- 使用
html-webpack-externals-plugin
,将基础包通过 CDN 引入,不打入 bundle 中
- 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
tree-shaking
打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)。
scope-hosting
构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
变量提升,可以减少一些变量声明。在生产环境下,默认开启。
另外,大家测试的时候注意一下,speed-measure-webpack-plugin
和 HotModuleReplacementPlugin
不能同时使用,否则会报错。
babel 配置优化
在不配置 @babel/plugin-transform-runtime
时,babel
会使用很小的辅助函数来实现类似 _createClass
等公共方法。默认情况下,它将被注入(inject
)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。
我们也并不需要在每个 js
中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime
,@babel/plugin-transform-runtime
是一个可以重复使用 Babel
注入的帮助程序,以节省代码大小的插件。
因此我们可以在 .babelrc
中增加 @babel/plugin-transform-runtime
的配置。
{
"presets": [],
"plugins": [
[
"@babel/plugin-transform-runtime"
]
]
}
图片压缩
开发环境的优化
增量编译
使用 webpack 的监听模式。不要使用其他工具来监听你的文件和调用 webpack 。在监听模式下构建会记录时间戳并将信息传递给编译让缓存失效。
在某些设置中,监听会回退到轮询模式。有许多监听文件会导致 CPU 大量负载。在这些情况下,你可以使用 watchOptions.poll
来增加轮询的间隔。
Devtool
需要注意的是不同的 devtool
的设置,会导致不同的性能差异。
"eval"
具有最好的性能,但并不能帮助你转译代码。
- 如果你能接受稍差一些的 mapping 质量,可以使用
cheap-source-map
选项来提高性能
- 使用
eval-source-map
配置进行增量编译。
=> 在大多数情况下,cheap-module-eval-source-map
是最好的选择。
避免在生产环境下才会用到的工具
某些实用工具, plugins 和 loaders 都只能在构建生产环境时才有用。例如,在开发时使用 UglifyJsPlugin
来压缩和修改代码是没有意义的。以下这些工具在开发中通常被排除在外:
UglifyJsPlugin
ExtractTextPlugin
[hash]
/ [chunkhash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
在内存中编译
以下几个实用工具通过在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:
webpack-dev-server
webpack-hot-middleware
webpack-dev-middleware
参考文档