vlambda博客
学习文章列表

记住4点优化就可提高webpack构建速度

前言

在项目开发过程中,随着代码迭代,插件堆砌你会发现项目的构建(编译)速度越来越慢,每次热更新或打包都得等个几十秒。极大影响开发体验,效率蹭蹭蹭往下掉。那么就是时候对项目webpack配置进行一次优化,本文汇总四点来优化webpack构建速度。


记住4点优化就可提高webpack构建速度

一、缩小文件的搜索范围

这点主要在webpack的配置上做些优化,这边记录了6个优化小点:

1、优化 Loader 配置

可以通过test、include、exclude三个配置项来命中Loader要应用规则的文件。为了尽可能少地让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。

示例如下:

// 配置es6 babel-loadermodule.exports = { module: { rules: [ { // 如果项目中只有js就不用写/\.jsx?$/ test: /\.js$/, // 排除 node_modules文件夹下文件 exclude: /node_modules/,  // 只编译项目根目录src目录下js文件 include: [ path.resolve(__dirname, "../src"), ], loader: "babel-loader" } ] },};// 注意:exclude优先级比include高,通常配置include后则无需再配置exclude,除非需要排除include中的某些文件

2、优化 resolve.modules 配置

resolve modules 的默认值是['node_modules'],含义是先去当前目录的 node_modules目录下去找我们想找的模块,如果没找到就去上一级目录. /node_modules中找,再没有就去 ../../node_modules 中找,以此类推。

因此当第三方模块都放在项目根目录的./node_modules目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:

module.exports = { resolve: { // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤  modules: [path.resolve(__dirname, 'node_modules')]  }}

3、优化 resolve.mainFields 配置

在安装的第三方模块中都会有一个package.json 文件,用于描述这个模块的属性,其中的某些字段用于描述入口文件在哪里。resolve.mainFields 用于配置采用哪个字段作为入口文件的描述。

如:fetch 插件,需要支持用于浏览器和 Node.js 环境,因此在它的 package. jsoη中就有两个入口文件描述字段:

{ browser : "fetch-npm-browserify.js",  main: "fetch-npm-node.js"}

因此我们需要根据当前webpack.target配置来指明要引用哪个文件。

示例:

module.exports = { resolve: { // 只采用 main 字段作为入口文件的描述字段,以减少搜索步骤    mainFields: ['main']  }}

4、优化 resolve.alias 配置 

通过别名来将原导入路径映射成一个新的导入路径。以react包为例,在默认情况下, Webpack 会从入口文件./node_modules/react/react.js 开始递归解析和处理依赖的几十个文件,这会是一个很耗时的操作。通过配置resolve.alias可以让 Webpack 在处理React库时,直接使用单独、完整的 react.min.js 文件 ,从而跳过耗时的递归解析操作。

示例:

module.exports = { resolve: { // 使用alias将导入react的语句换成直接使用单独、完整的react.min.js文件 // 减少耗时的递归解析操作 alias: path.resolve(__dirname ,'./ node_modules/react/dist/react.min.js') }}

5、优化 resolve.extensions 配置 

在导入语句没带文件后缀时Webpack会在自动带上后缀后去尝试询问文件是否存在。默认是['.js','.json']。在项目中如果明确知道只需匹配'.js'或'.jsx'后缀的话,则可以指定匹配后缀列表以减少匹配次数。

示例:

module.exports = { resolve: { // 后缀尝试列表要尽可能小 // 频率出现最高的文件后缀要优先放在最前面 // 在源码中写导入语句时,要尽可能带上后缀     extensions: ['.js'] }}

6、优化 resolve.noParse 配置 

忽略对部分没采用模块 化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库如 jQuery。在前面讲解优化 resolve.alias 配置时讲到,单独、完整的react.min.js文件没有采用模块化,这时我们就需要通过配置 module.noParse忽略对 react.main.js 文件的递归解析处理。

示例:

module.exports = { resolve: {     // 忽略对 react.min.js文件的递归解析处理     noParse: [/react\.min\.js$/] }}
记住4点优化就可提高webpack构建速度

二、使用 DllPlugin

从插件名字可以看出这是个借助DLL思想来优化webpack构建速度。在window中DLL后缀文件表示动态链接库文件,其中包含为其他模块调用的函数和数据。

为什么为 Web 项目构建接入动态链接库的思想后会大大提升构建速度呢?原因在于,包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中 的代码。从这里我们也可以看出动态链接库抽离的代码通常是第三方库代码,如react、react-dom、vue等,只有这些库不升级那动态链接库就不需要重新编译。


Webpack 己经内置了对动态链接库的支持,需要通过以下两个内置的插件接入:

1、DllPlugin 插件:用于打包出一个个单独的动态链接库文件

2、DllReferencePlugin 插件:在主要的配置文件中引入 DllPlugin 插件打包好的动态链接库文件。


我们以打包react动态链接库为列,首先我们需要打包出react.dll.js、manifest.json。其中react.dll.js包含react、react-dom模块代码,而manifest.json则是描述react.dll.js中包含了哪些模块。在webpack构建过程中遇到react模块时首先会从manifest.json中寻找是否有该模块,如果有则直接从react.dll.js中引用该模块跳过对react的编译。生成react.dll.js、manifest.json我们需要用到DllPlugin插件,我们需要单独配置一个webpack.dll.config.js文件用来生成这个两个文件,内容如下:

const path = require('path');// webpack 内置插件,无需installconst DllPlugin = require('webpack/lib/DllPlugin');
module.exports = { // JavaScript 执行入口文件 mode: 'production', entry: { // 将 React 相关的模块放到一个单独的动态链接库中        react: ['react', 'react-dom'], }, output: { // 输出的动态链接库的文件名称,[name]代表当前动态链接库的名称, // 也就是entry中配置的 react filename: '[name].dll.js', // 将输出的文件都放到 dist 目录下 path: path.resolve(__dirname, '../public/dll'), // 存放动态链接库的全局变量名称,例如对于 react 来说就是 _dll_react // 之所以在前面加上_dll_ ,是为了防止全局变量冲突 library: '_dll_[name]', }, plugins: [ // 接入 DllPlugin new DllPlugin({ // 动态链接库的全局变量名称,需要和 output.library 中的保持一致 // 该字段的值也就是输出的 manifest.json 文件中 name 字段的值 // 例如在 react.manifest.json 中就有 "name":"_dll_react" name: '_dll_[name]', // 描述动态链接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, '../public/dll', '[name].manifest.json'), }) ]};

在package.json中配置dll命令,如下:

{ "scripts": { "dll": "webpack --config ./config/webpack.dll.config.js" }}

这样我们在命令版中执行yarn dll 就生成了react.dll.js、manifest.json这两个文件。

由于我们暴露出来的模块是通过_dll_react这个全局变量来引用其所包含的所有模块,因此我们需要在index.html模板中加载该文件,如下:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="icon" href="/favicon.ico" type="image/x-icon">    <title><%= htmlWebpackPlugin.options.title %></title> <script ></script></head><body>    <div id="app"></div></body></html>

最后我们在项目的webpack中使用这个动态链接库文件,这里就用到了DllReferencePlugin 插件,配置如下:

const path = require('path');const CopyWebpackPlugin = require('copy-webpack-plugin');const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = { // 省略其他配置项 plugins: [        // 告诉Webpack使用了哪些动态链接库 new DllReferencePlugin({            // 描述 react 动态链接库的文件内容 manifest: path.resolve(__dirname, '../public/dll/react.manifest.json'),        }),        // 资源拷贝,将生成在puplic下的dll文件拷贝到dist目录下 new CopyWebpackPlugin([ { from: path.resolve(__dirname + '/../public'), to: path.resolve(__dirname + '/../dist'), ignore: ['*.jpg'] } ])    ],};


至此,我们项目就成功引入了DllPlugin动态链接库的构建方案。


记住4点优化就可提高webpack构建速度

三、使用 HappyPack

webpack打包哪一步最耗时?可能要数loader对文件的转换操作了,使用loader将文件转换为我们需要的类型,文件数量巨大,webpack执行又是单线程的,转换的操作只能一个一个的处理,不能多件事一起做。我们需要Webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力,HappyPack 就能让 Webpack 做到这点,我们将需要通过loader处理的文件先交给happypack去处理,happypack 在收集到这些文件的处理权限后,统一分配CPU资源,从而发挥多核作用。

直接看实例:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");const HappyPack = require('happypack')//构造出一个共享进程池,在进程池中包含4个子进程const happyThreadPool = HappyPack.ThreadPool({ size: 4})module.exports = { //省略部分配置 module: { rules: [ { test: /\.js$/, //将对.js文件的处理转交给id为babel的happypack实例 use: ['happypack/loader?id=babel'] //排除node_modules下的js,合理的使用排除可以事半功倍 exclued:path.resolve(__dirname,'node_modules') }, { test: /\.css$/, //将对.css文件的处理转交给id为css的happypack实例 use: ExtractTextPlugin.extract({ fallback: "style-loader", use: ['happypack/loader?id=css'] })     }] }, plugins: [        new HappyPack({ // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: 'babel', // 如何处理 .js 文件,用法和 Loader 配置中一样 loaders: ['babel-loader?cacheDirectory=true'], //使用共享进程池中的自进程去处理任务 threadPool: happyThreadPool, //是否允许happypack输出日志,默认true verbose: true }),        new HappyPack({ id: 'css', // 如何处理 .css 文件,用法和 Loader 配置中一样 loaders: ['css-loader!postcss-loader'], threadPool: happyThreadPool }), ]}

在以上代码中有以下两项重要的修改。

  • 在Loader 置中,对所有文件的处理都交给了 happypack/loader ,使用紧跟 其后的querystring?id=babel去告诉happypack/loader 选择哪个HappPack实例处理文件。

  • 在Plugin 配置中新增了两个 HappyPack 实例,分别用于告诉 happypack /loader 如何处理.js和.css 文件。选项中的 id 属性的值和上面 querystring 中的?id=babe 对应,选项中的 loaders 属性和 Loader 配置中的一样

接入 HappyPack 后,需要为项目安装新的依赖:

npm i -D happypack

四、使用 ParallelUglifyPlugin

这个原理跟HappyPack有点类似,都是将单线程分解成多个子进程去处理文件。在发布线上代码时,通常需要对代码进行压缩,由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树, 再去应用各种规则分析和处理 AST ,所以导致这个过程的计算量巨大耗时非常多。


当Webpack 有多个 JavaScript 文件需要输出和压缩时原本会使用 UglifyJS 去一个一个压缩 再输出,但是 ParallelUglifyPlugin 会开启多个子进程,将对多个文件的压缩工作分配给多个 子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。因此可以更快完成压缩工作。


使用示例如下:

const path = require('path');const ParallelUglifyPlugin =require('webpack-parallel-uglify-plugin');
module.exports = {    // 忽略其他配置项    plugins: [ new ParallelUglifyPlugin({ // 传递给UglifyJS 的参数 uglifyJS: { output: { // 最紧凑输出 beautify: false, // 删除注释 comments: false }, compress: {                    // 删除console语句 drop_console: true, collapse_vars: true, reduce_vars: true } } })    ]};

在通过 new ParallelUglifyPlugin() 实例化时,支持以下参数。

  • test: 使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩

  • include: 使用正则去命中需要被 ParallelU glifyPlugin 压缩的文件

  • exclude :使用正则去命中不需要被 ParallelUglifyPlugin 压缩的文件

  • cacheDir :缓存压缩后的结果

  • workerCount :开启几个子进程去并发执行压缩

  • sourceMap :是否输出 Source Map ,这会导致压缩过程变慢

  • uglifyJS :用于压缩 ES5 代码时的配置

其中的 test include exclude 与配置 Loader 时的思想和用法一样




以上就是优化webpack构建速度的四点建议,如果你还在为项目构建速度缓慢而烦恼,不如试试上诉优化万一变好了呢。


参考文献

《深入浅出 Webpack》 吴浩麟


前端扫盲

扫码关注