前面的话
Webpack升级4之后,小柒踩过的很多坑,这篇文章总结Webpack4的一些新特性,以及常见的优化方式。
Webpack4 新特性
-
不再强制需要
webpack.config.js
配置文件。默认入口为./src/index.js
,默认输出./dist目录
,输出文件main.js
-
Webpack不再能单独使用,4.x版本的很多命令都移动到了
webpack-cli
中。所以必须安装webpack
与webpack-cli
。 -
开箱即用 WebAssembly,webpack4提供了wasm的支持,现在可以引入和导出任何一个 Webassembly 的模块,也可以写一个loader来引入C++、C和Rust。(注:WebAssembly 模块只能在异步chunks中使用)
-
增加模式区分,在配置文件中使用
mode
选项来配置相应的模式:
development
:开发模式。打包默认不压缩代码,默认开启代码调试
production
:生产模式,线上使用。打包压缩代码,不开启代码调试 -
一些插件由
optimeztion
配置代替。
比如:CommonsChunkPlugin
废弃,由optimization.splitChunks
和optimization.runtimeChunk
代替,前者拆分代码,后者提取runtime代码- optimize.UglifyJsPlugin 废弃,由 optimization.minimize 替代,生产环境默认开启。
-
升级到 webpack4 后,mini-css-extract-plugin 替代 extract-text-webpack-plugin 成为css分离首选。
量化
speed-measure-webpack-paligin
插件可以测量各个插件和loader所花费的时间。
使用如下:
const SpeedMeasurePlugin = require('speed-measure-webpack-palugin');
const smp = new SpeedMeasurePlugin();
const config = {
// ...webpack配置
}
module.exports = smp.wrap(config);
从打包时间上优化
从哪几个方面优化
- 减少搜索依赖的时间
- 减少loader解析时间
- 减少js文件压缩时间: 将所有解析完的代码,打包到一个文件中,为了减小白屏时间,webpack为对其进行优化。js压缩是发布编译的最后阶段,通常webpack需要卡好一会,因为压缩js代码要将代码解析成AST,再根据复杂的规则去分析和处理AST,最后将AST还原为js。
- 减少二次打包时间:当我们更改项目中的文件是,就要重新打包。有些文件其实不需要再次打包,所以要减少二次打包时间。
1、减少搜索依赖的时间
- 优化loader配置
由于loader转换文件比较耗时,所以让尽可能少的文件被loader处理。使用test
、include
、exclude
三个配置项来命中loader要应用的文件。 - 优化resolve.modules配置
resolve.modules用于配置Webpack去哪些目录下寻找第三方模块。resolve.modules的默认值是['node_modules']
,意思是先从当前目录下的./node_modules目录下去找我们想要的模块,如果没有就去上级目录…/node_modules,再没有就去…/…/node_modules中找,以此类推。 - 优化resolve.mainFields配置
用于配置第三方模板使用哪个入口文件。 - 优化resolve.alias配置
通过别名来把原来的导入路径映射成为一个新的路径,减少解析时间 - 优化reslove.extensions配置
在导入语句没有文件后缀时,Webpack会根据resolve.extension自动带上后缀后去尝试询问文件是否存在。 - 优化module.noParse配置
可以让Webpack忽略对部分模块没有采用模块化的文件的递归解析处理。比如:jQuery、ChartJS它们庞大有没有采用模块化标准,让Webpack去解析这些文件耗时又没有意义。
详细配置:根据你的项目去选择
// 编译代码的基础配置
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`),
}
},
};
2、 减少loader的解析时间(开启多进程)
webpack是单线程模式,只能一个一个文件去处理,当打包文件比较大时,打包时间就会比较长。
-
HappyPack(Webpack 3)
原理:将loader的解析交给多个进程并行去处理,发挥CPU多核的能力,从而减少构建时间。
安装依赖:npm install happypack -D
使用如下:// 比如对js文件的处理: // 引入happypack const HappyPack = require('happypack'); // 引入系统操作模块 const os = require('os'); // 构造共享进程池,根据系统内核数量,指定进程池的个数,也可以是其他数量 const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); const createHappyPlugin = (id, loaders) => new HappyPack({ id: id, // 唯一标识 loaders: loaders, // 使用的loader // 其他选项 threadPool: happyThreadPool, //使用共享进程池中的子进程去处理任务 verbose: true // 是否允许HappyPack输出日志,默认为true }) module.exports = { // ... 其他选项 module: { rules: [ // ... 其他文件loader配置 { test: /\.js$/, // 对js文件的处理交给`id`为babel的HappyPack实例 use: ['happypack/loader?id=babel'], // node_modules目录下的文件都是es5语法,不用通过babel转换 exclude: /node_modules/, }, ] }, plugins: [ // ...其他插件 createHappyPlugin('babel',[{ loader: 'babel-loader', options: { babelrc: true, cacheDirectory: true // 启用缓存 } }]) ] }
注意:只在解析时间长的loader上使用,项目小也不必使用。并且happypack不推荐使用,现在已经不再维护它
-
thread-loader(Webpack4 推荐)
使用比较简单,将这个thread-loader放在其他loader之前就可以了。
原理: 也是开启多进程来并行loader的解析,被放置了thread-loader的loader就会在单独的worker池中运行,一个worker就是一个node进程。每个单独的进程处理时间限制在600ms。
安装依赖:
npm install thread-loader -D
使用如下:
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 } }, ] } // ... ] // ...
3、减少js压缩时间
发布到到线上的代码,一般都会压缩js代码。
-
UglifyJsPlugin (Webpack 3)
UglifyJsPlugin是webpack3内置的的插件,使用时引入就好,是单进程的。
原理: 配置在压缩过程中使用的规则。
使用如下,给出最优化的代码配置:
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); module.exports = { // ... plugins: [ new UglifyJsPlugin({ compress: { warnings: false,// 在UglifyJsPlugin删除没有用到的代码时,不是输出警告 drop_console: true,// 删除所有的'console'语句 collapse_var: true,// 内嵌已定义,但是只用了一次的变量 reduce_var: true,// 提取出现了多次但没有定义成变量去引用的静态值。 }, output:{ // 最紧凑的输出 beautify: false, // 删除所有的注释 comments: false } }) ]
此外,Webpack提供了更简单的方法来接入UglifyJsPlugin,直接在启动Webpack时,带上
--optimize-minimize
参数。即webpack --optimize-minimize
。 -
webpack-parallel-uglify-plugin (Webpack 3)
看名字都知道,并行的UglifyPlugin嘛,就是利用了多进程。
原理:开启多个子进程,对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UgilifyJS去压缩代码,但是并行执行。子进程处理完后再把结果发送给主进程。
安装依赖:
npm install webpack-parallel-uglify-plugin -D
使用如下:
const ParallelUglifyJsPlugin = require('webpack-parallel-uglify-plugin'); module.exports = { // ... plugins: [ new ParallelUglifyJsPlugin({ cacheDir: './cache' // 缓存压缩后的结果 // 传递给UglifyJS的参数 uglifyJS: { compress: { warnings: false,// 在UglifyJsPlugin删除没有用到的代码时,不是输出警告 drop_console: true,// 删除所有的'console'语句 collapse_var: true,// 内嵌已定义,但是只用了一次的变量 reduce_var: true,// 提取出现了多次但没有定义成变量去引用的静态值。 }, output:{ // 最紧凑的输出 beautify: false, // 删除所有的注释 comments: false } } }) ]
上面的
ugilifyJS
参数是用于压缩ES5代码是时的配置,也可以是ugilifyES
:用于压缩ES6代码。 -
terser-webpack-plugin(Webpack 4)
terser:简要的。
官方定义:用于ES6+的js解析器、压缩工具。为何选择terser:不再维护UglifyES,并且UgilifyJS不支持ES6+。terser是UglifyES的一个分支,主要保留了与UglifyJS和UglifyES的API和CLI兼容性。
原理:开启多进程
安装依赖:
npm install terser-webpack-plugin -D
使用如下:module.exports = { optimization: { minimizer: [ new TerserPlugin({ parallel: true, }), ], }, };
4、 减少二次打包时间
合理利用缓存,来减少二次打包时间。比如cache-loader
HardSourceWebpackPlugin
、 babel-loader的cacheDirectory标志
。但所有的缓存方法都有启动开销。二次打包节约时间,初次打包很慢
-
cache-loader
cache-loader的配置很简单,只要放在其他loader之前即可。如果只是下
babel-loader
配置cache,使用babel-loader
的cacheDirectory
安装依赖:
npm install cache-loader -D
module.exports = { module: { rules: [ { test: /\.ext$/, use: ['cache-loader', ...loaders], include: path.resolve('src'), }, ], }, };
-
hard-source-webpack-plugin
为模块提供中间缓存。缓存默认的存放路径是:
node_modules/.cache/hard-source
.使用如下:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); // ... plugins: [ new HardSourceWebpackPlugin() ],
非首次使用
HardSourceWebpackPlugin
打包的时间,明显比没有使用要快:
从打包体积上优化
-
Tree Shaking (依赖需要采用ES6模块化语法的代码)
可以去除无用代码。
要求:必须使用ES6模块化语法,使用babel时,要关闭其模块转换功能,修改.babelrc文件或者在 webpack.config.js 配置文件中将modules设置为false。
// .babelrc { "presets": [ ["env", { "modules": false // 保留es6的模块化语法 } ] ] }
Webpack4下的配置:
- 首先,你必须处于生产模式。Webpack 只有在压缩代码的时候会 tree-shaking。
- 其次,开启优化选项 “usedExports” 。这样Webpack就看可以提示你哪些是用不上的代码
- 最后,你需要使用一个支持删除死代码的压缩器。上面提到过terser-webpack-plugin
注意:生产环境下默认开启 。
const config = { mode: 'production', // 生产模式 optimization: { usedExports: true, // minimizer: [ new TerserPlugin({...}) ] } };
还需注意的一点:
在
package.json
文件中,有一个特殊的属性sideEffects
,它有三个可能值:-
true
: 是默认值,意味着所有的文件都不能进行tree-shaking
-
false
: 表示所有的文件都可以进行tree-shaking
-
[...]
: 表示在数组中的文件,是不可以进行tree-shaking
的,其他文件都可以.// 所有文件都有副作用,全都不可 tree-shaking { "sideEffects": true } // 没有文件有副作用,全都可以 tree-shaking { "sideEffects": false } // 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件 { "sideEffects": [ "./src/file1.js", "./src/file2.js" ] }
-
Scope Hoisting (依赖采用ES6模块化语法的代码)
可以让Webpack 打包出来的代码更小。
原理:分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。
使用如下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); module.exports = { // ... resolve: { // 针对Npm中的第三方模块优先采用jsnext:main中指向ES6模块化语法的文件 mainFields:['jsnext:main', 'browser', 'main'] }, plugins: [ // 开启Scope Hoisting new ModuleConcatenationPlugin() ], // ...
注意:生产环境下默认开启。
最后
还有使用DllPlugin
与DllReferencePlugin
分离基础模块(vue-router、vuex等)、使用optimization.splitChunks
与optimization.runtimeChunk
分离公共代码。这两部分内容放到下篇总结。
参考文章: