更快-提高打包速度
我们可以通过speed-measure-webpack-plugin来观察webpack的打包速度
- 分析整个打包总耗时;
- 每个插件和 loader 的耗时情况;
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports =smp.wrap({
mode:'development',
...
})
先回顾下webpack的流程:
1.开始打包,我们需要获取所有的依赖模块 搜索所有的依赖项,这需要占用一定的时间,即搜索时间,那么我们就确定了:我们需要优化的第一个时间就是搜索时间。
2. 解析所有的依赖模块(解析成浏览器可运行的代码)webpack 根据我们配置的 loader 解析相应的文件。日常开发中我们需要使用 loader 对 js ,css ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于 js 单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。我们需要优化的第二个时间就是解析时间。
3. 将所有的依赖模块打包到一个文件。将所有解析完成的代码,打包到一个文件中,为了使浏览器加载的包更新(减小白屏时间),所以 webpack 会对代码进行优化。JS 压缩是发布编译的最后阶段,通常 webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。我们需要优化的第三个时间就是压缩时间。
4. 二次打包 当更改项目中一个小小的文件时,我们需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。我们需要优化的第四个时间就是二次打包时间。**
优化解析时间 - 开启多线程打包
运行在 Node.js 之上的 webpack 是单线程模式的,也就是说,webpack 打包只能逐个文件处理,当 webpack 需要打包大量文件时,打包时间就会比较漫长。
thread-loader(webpack4 官方推荐)
把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker【worker pool】 池里运行,一个worker 就是一个nodeJS 进程【node.js proces】,每个单独进程处理时间上限为600ms,各个进程的数据交换也会限制在这个时间内。
thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
上面图中可以看到babel-loader耗时比较久,2.5secs。
注意:项目比较小的时候,解析时间可能会大。
合理利用缓存(缩短连续构建时间,增加初始构建时间)
HardSourceWebpackPlugin
- 第一次构建将花费正常的时间
- 第二次构建将显着加快(大概提升90%的构建速度) 2.63s => 1.32s
plugins: [
new HardSourceWebpackPlugin({
// cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
// 'node_modules/.cache/hard-source/[confighash]'
cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'),
// configHash在启动webpack实例时转换webpack配置,
// 并用于cacheDirectory为不同的webpack配置构建不同的缓存
configHash: function(webpackConfig) {
// node-object-hash on npm can be used to build this.
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
// 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
// hard-source需要替换缓存以确保输出正确。
// environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock'],
},
// An object. 控制来源
info: {
// 'none' or 'test'.
mode: 'none',
// 'debug', 'log', 'info', 'warn', or 'error'.
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 50 * 1024 * 1024
},
}),
new HardSourceWebpackPlugin.ExcludeModulePlugin([
{
test: /.*\.DS_Store/
}
]),
]
优化压缩时间
webpack4 中 webpack.optimize.UglifyJsPlugin
已被废弃。
也不推荐使用 ParallelUglifyPlugin,项目基本处于没人维护的阶段,issue 没人处理,pr没人合并。
webpack4 默认内置使用 terser-webpack-plugin
插件压缩优化代码,而该插件使用 terser
来缩小 JavaScript
。
terser 是什么?
所谓 terser,官方给出的定义是:
用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。
为什么 webpack 选择 terser?
不再维护 uglify-es ,并且 uglify-js 不支持 ES6 +。 terser 是 uglify-es 的一个分支,主要保留了与 uglify-es 和 uglify-js@3 的 API 和 CLI 兼容性。
terser 启动多线程
使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1
。
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
}),
],
},
};
可以显著加快构建速度,因此强烈推荐开启多线程
2.63s => 2.49s
优化搜索时间- 缩小文件搜索范围 减小不必要的编译工作
webpack 打包时,会从配置的 entry
触发,解析入口文件的导入语句,再递归的解析,在遇到导入语句时 webpack 会做两件事情:
- 根据导入语句去寻找对应的要导入的文件。例如
require('react')
导入语句对应的文件是./node_modules/react/react.js
,require('./util')
对应的文件是./util.js
。 - 根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。
以上两件事情虽然对于处理一个文件非常快,但是当项目大了以后文件量会变的非常多,这时候构建速度慢的问题就会暴露出来。 虽然以上两件事情无法避免,但需要尽量减少以上两件事情的发生,以提高速度。
接下来一一介绍可以优化它们的途径。
1. 优化 loader 配置
使用 Loader 时可以通过 test
、 include
、 exclude
三个配置项来命中 Loader 要应用规则的文件
2. 优化 resolve.module 配置
resolve.modules
用于配置 webpack 去哪些目录下寻找第三方模块,resolve.modules
的默认值是 ['node_modules']
,含义是先去当前目录下的 ./node_modules
目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules
中找,再没有就去 ../../node_modules
中找,以此类推。
3. 优化 resolve.alias 配置
resolve.alias
配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作。
4. 优化 resolve.extensions 配置
在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions
应尽可能注意以下几点:
resolve.extensions
列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。- 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。
5. 优化 resolve.mainFields 配置
有一些第三方模块会针对不同环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在
package.json
文件里,如下:{
"jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
"main": "lib/index.js" // 采用 ES5 语法的代码入口文件
}
webpack 会根据
mainFields
的配置去决定优先采用那份代码,mainFields
默认如下:mainFields: ['browser', 'main']
webpack 会按照数组里的顺序去
package.json
文件里寻找,只会使用找到的第一个。
假如你想优先采用 ES6 的那份代码,可以这样配置:mainFields: ['jsnext:main', 'browser', 'main']
6. 优化 module.noParse 配置
module.noParse
配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。7. 详细配置
// 编译代码的基础配置
module.exports = {
// ...
module: {
// 项目中使用的 jquery 并没有采用模块化标准,webpack 忽略它
noParse: /jquery/,
rules: [
{
// 这里编译 js、jsx
// 注意:如果项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提升正则表达式性能
test: /\.(js|jsx)$/,
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
use: ['babel-loader?cacheDirectory'],
// 排除 node_modules 目录下的文件
// node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: /node_modules/,
},
]
},
resolve: {
// 设置模块导入规则,import/require时会直接在这些目录找文件
// 可以指明存放第三方模块的绝对路径,以减少寻找
modules: [
path.resolve(`${project}/client/components`),
path.resolve('h5_commonr/components'),
'node_modules'
],
// import导入时省略后缀
// 注意:尽可能的减少后缀尝试的可能性
extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
// import导入时别名,减少耗时的递归解析操作
alias: {
'@compontents': path.resolve(`${project}/compontents`),
}
},
};
更小-减小打包体积
压缩JS
由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多
webpack4 开启mode:”production”,会压缩JS代码压缩CSS
extract-text-webpack-plugin: webpack3 及之前版本
webpack4 或之后,该插件将 CSS 提取到单独的文件中。 它为每个包含 CSS 的 JS 文件创建一个 CSS 文件。 它支持 CSS 和 SourceMap 的按需加载。提取CSS
- 压缩CSS ``` const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’); const devMode = process.env.NODE_ENV !== ‘production’;
module.exports = { module: { rules: [ { test: /.css$/, use: [ devMode ? ‘style-loader’ ? MiniCssExtractPlugin.loader, ‘css-loader’ ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: devMode ? ‘[name].css’ : ‘[name].[hash:8].css’, chunkFilename: devMode ? ‘[id].css’ : ‘[id].[hash:8].css’, disable: false, allChunks: true, }), ], };
<a name="m5rVQ"></a>
#### optimize-css-assets-webpack-plugin
在 webpack 构建过程中搜索 CSS ,并优化、最小化 CSS。
var OptimizeCssAssetsPlugin = require(‘optimize-css-assets-webpack-plugin’);
module.exports = { plugins: [ new OptimizeCssAssetsPlugin({ // ExtractTextPlugin 或 MiniCssExtractPlugin导出的文件名 assetNameRegExp: /.css$/g, // 用于优化/最小化CSS的CSS处理器,默认为cssnano cssProcessor: require(‘cssnano’), cssProcessorPluginOptions: { preset: [‘default’, { discardComments: { removeAll: true } }], }, // 控制插件是否可以将消息打印到控制台,默认为 true canPrint: true }) ] }; ```