性能优化:Webpack 优化构建速度
Webpack 优化构建速度
3.1、 缩小文件搜索范围
搜索过程优化包括:
::: tip
-
优化 resolve.modules 配置 -
优化 resolve.noParse 配置 -
优化 resolve.extensions 配置 -
优化 resolve.noParse 配置 -
优化 优化 Loader 配置
:::
-
优化 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.mainFields:['main'],设置尽量少的值可以减少入口文件的搜索步骤. 第三方模块为了适应不同的使用环境,会定义多个入口文件,mainFields 定义使用第三方模块的哪个入口文件,由于大多数第三方模块都使用 main 字段描述入口文件的位置,所以可以设置单独一个 main 值,减少搜索
-
优化 resolve.alias 配置
对庞大的第三方模块设置 resolve.alias, 使 webpack 直接使用库的 min 文件,避免库内解析。
alias: {
'@': resolve('src'),
},
// 通过以上的配置,引用src底下的common.js文件,就可以直接这么写
import common from '@/common.js';
这样会影响 Tree-Shaking,适合对整体性比较强的库使用,如果是像 lodash 这类工具类的比较分散的库,比较适合 Tree-Shaking,避免使用这种方式。
-
优化 resolve.extensions
合理配置 resolve.extensions,减少文件查找默认值:extensions:['.js', '.json'],当导入语句没带文件后缀时,Webpack 会根据 extensions 定义的后缀列表进行文件查找,所以:
-
列表值尽量少 -
频率高的文件类型的后缀写在前面 -
源码中的导入语句尽可能的写上文件后缀,如 require(./data)要写成 require(./data.json)
-
module.noParse
字段告诉 Webpack 不必解析哪些文件,可以用来排除对非模块化库文件的解析.
noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析和处理,这 样做的好处是能提高构建性能。原因是一些库如 jQuery、ChartJS 庞大又没有采用模块化标准,让 Webpack 去解析这些文件既耗时又没有意义。noParse 是可选的配置项,类型需要是 RegExp 、[RegExp]、function 中的一种。例如,若想要忽略 jQuery 、ChartJS ,则优化配置如下:
// 使用正则表达式
noParse: /jquerylchartjs/;
// 使用函数,从 Webpack3.0.0开始支持
noParse: content => {
// 返回true或false
return /jquery|chartjs/.test(content);
};
-
优化 loader 配置,通过 test、exclude、include 缩小搜索范围;
-
(1) 优化正则匹配 -
(2) 通过 cacheDirectory 选项开启缓存 -
(3) 通过 include、exclude 来减少被处理的文件
{
// 1、如果项目源码中只有js文件,就不要写成/\.jsx?$/,以提升正则表达式的性能
test: /\.js$/,
// 2、babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
loader: 'babel-loader?cacheDirectory',
// 3、只对项目根目录下的src 目录中的文件采用 babel-loader
include: [resolve('src')]
},
3.2、使用 DllPlugin 减少基础模块编译次数
DllPlugin 动态链接库插件,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包
,而是去 dll 中获取。**为什么会提升构建速度呢?**原因在于 dll 中大多包含的是常用的第三方模块,如 react、react-dom,所以只要这些模块版本不升级,就只需被编译一次。我认为这样做和配置 resolve.alias 和 module.noParse 的效果有异曲同工的效果。
详情 demo 请见:lucky_vue_template
-
使用 DllPlugin 配置一个 webpack.dll.config.js 来构建 dll 文件:
var path = require("path");
var webpack = require("webpack");
var CleanWebpackPlugin = require("clean-webpack-plugin");
var vendor = ["vue", "vuex", "vue-router"];
var vendordev = ["vue/dist/vue.esm.js", "vuex", "vue-router"]; //集成开发版本vue
var config = require("./config/config.js");
module.exports = {
//你想要打包的模块数组
entry: {
vendor: vendor,
vendordev: vendordev
},
output: {
path: path.join(__dirname, config.dllRoot),
filename: "[name].dll.js",
library: "[name]_library"
//vendor.dll.js 中暴露出的全局变量
//主要是给DllPlugin中的name 使用
//故这里需要和webpack.DllPlugin 中的 'name :[name]_libray 保持一致
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.join(__dirname, config.dllRoot, "[name]-manifest.json"),
name: "[name]_library",
context: __dirname
})
]
};
需要注意 DllPlugin 的参数中 name 值必须和 output.library 值保持一致,并且生成的 manifest 文件中会引用 output.library 值。
-
在主 config 文件里使用 DllReferencePlugin 插件引入 xxx-manifest.json 文件:
[
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require(config.dllPath(true, true))
}),
new HtmlWebpackTagsPlugin({ tags: ["/dll/vendor.dll.js"], append: false }),
new CopyWebpackPlugin([
{
from: path.join(__dirname, config.dllPath(true, false)),
to: path.join(__dirname, config.dllVendorTarget)
}
])
];
3.3、使用 externals 减少基础模块编译次数
我们在使用的 js 库如 vue 或者 react 等的时候,webpack 会将它们一起打包,react 和 react-dom 文件就好几百 k,全部打包成一个文件,可想而知,这个文件会很大,用户在首次打开时就往往会出现白屏等待时间过长的问题,这时,我们就需要将这类文件抽离出来。
externals: {
"react": "React",
"react-dom": "ReactDOM"
},
这里我们会用到 externals,它和 plugins 是平级。左侧 key 表示依赖,右侧 value 表示文件中使用的对象。比如在 react 开发中,我们常常会这样在文件头部写 import React from 'react',这里大家可以和上面对号入座下。
这里我们就需要对这个文件进行单独的引入使用了,在 index.html 中添加如下代码
<script src="./node_modules/react/umd/react.xxx.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.xxx.js"></script>
不过,我们在项目上线的时候不可能会带有 node_modules,所以我们就需要使用一个 copy 插件将 react 和 react-dom 文件复制出来
new CopyWebpackPlugin([ // from是要copy的文件,to是生成出来的文件
{ from: "node_modules/react/umd/react.xxx.js", to: "js/react.min.js" },
{ from: "node_modules/react-dom/umd/react-dom.xxx.js", to: "js/react-dom.min.js" }
{ from: "public/favicon.ico", to: "favicon.ico" }
])
3.4、使用 HappyPack 多进程解析和处理文件
由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得更为严重。运行在 Node.之上的 Webpack 是单线程模型的,也就是说 Webpack 需要一个一个地处理任务,不能同时处理多个任务。Happy Pack ( https://github.com/amireh/happypack )就能让 Webpack 做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进
。
配置如下
-
HappyPack 插件安装:
$ npm i -D happypack
-
webpack.base.conf.js 文件对 module.rules 进行配置
module: {
rules: [
{
test: /\.js$/,
// 将对.js 文件的处理转交给 id 为 babel 的HappyPack实例
use:['happypack/loader?id=babel'],
include: [resolve('src'), resolve('test'),
resolve('node_modules/webpack-dev-server/client')],
// 排除第三方插件
exclude:path.resolve(__dirname,'node_modules'),
},
{
test: /\.vue$/,
use: ['happypack/loader?id=vue'],
},
]
},
-
(3)webpack.prod.conf.js 文件进行配置
const HappyPack = require("happypack");
// 构造出共享进程池,在进程池中包含5个子进程
const HappyPackThreadPool = HappyPack.ThreadPool({ size: 5 });
plugins: [
new HappyPack({
// 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
id: "vue",
loaders: [
{
loader: "vue-loader",
options: vueLoaderConfig
}
],
threadPool: HappyPackThreadPool
}),
new HappyPack({
// 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
id: "babel",
// 如何处理.js文件,用法和Loader配置中一样
loaders: ["babel-loader?cacheDirectory"],
threadPool: HappyPackThreadPool
})
];
3.5、 使用 ParallelUglifyPlugin 开启多进程压缩 JS 文件
由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST ,所以导致这个过程的计算量巨大,耗时非常多。当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个一个压缩再输出,但是 ParallelUglifyPlugin 会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。所以 ParallelUglify Plugin 能更快地完成对多个文件的压缩工作。
(1)ParallelUglifyPlugin插件安装:
$ npm i -D webpack-parallel-uglify-plugin
(2)webpack.prod.conf.js 文件进行配置
const ParallelUglifyPlugin =require('webpack-parallel-uglify-plugin');
plugins: [
new ParallelUglifyPlugin({
cacheDir: '.cache/',
uglifyJs:{
compress: {
warnings: false
},
sourceMap: true
}
}),
]
3.6、使用自动刷新
借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack 将这些功能都内置了,并且提供了多种方案供我们选择。
Webpack 可以使用两种方式开启监听:
-
启动 webpack 时加上--watch 参数; -
在配置文件中设置 watch:true
Webpack 配置官方文档点击这里
module.exports = {
watch: true,
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300, //文件变动后多久发起构建,越大越好
poll: 1000 //每秒询问次数,越小越好
}
};
相关优化措施:
-
(1)配置忽略一些不监听的一些文件,如:node_modules。 -
(2)watchOptions.aggregateTirneout 的值越大性能越好,因为这能降低重新构建的频率。 -
(3)watchOptions.poll 的值越小越好,因为这能降低检查的频率。
Vue Cli 可配置如下:
devServer: {
watchOptions: {
// 不监听的文件或文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化后等300ms再去执行动作
aggregateTimeout: 300,
// 默认每秒询问1000次
poll: 1000
}
},
3.7、开启模块热替换
模块热替换不刷新整个网页而只重新编译发生变化的模块,并用新模块替换老模块,所以预览反应更快,等待时间更少,同时不刷新页面能保留当前网页的运行状态。原理也是向每一个 chunk 中注入代理客户端来连接 DevServer 和网页。开启方式:
-
webpack-dev-server --hot -
使用 HotModuleReplacementPlugin,比较麻烦
开启后如果修改子模块就可以实现局部刷新,但如果修改的是根JS文件,会整页刷新
,原因在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,然后执行回调函数。如果一层层向外抛直到最外层都没有文件接收,就会刷新整页。使用 NamedModulesPlugin 可以使控制台打印出被替换的模块的名称而非数字ID
,另外同 webpack 监听,忽略node_modules目录的文件可以提升性能
。
devServer: {
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// 显示被替换模块的名称
new webpack.NamedModulesPlugin(), // HMR shows correct file names
]