vlambda博客
学习文章列表

性能优化:Webpack 优化构建速度

Webpack 优化构建速度

3.1、 缩小文件搜索范围

搜索过程优化包括:

::: tip

  • 优化 resolve.modules 配置
  • 优化 resolve.noParse 配置
  • 优化 resolve.extensions 配置
  • 优化 resolve.noParse 配置
  • 优化 优化 Loader 配置

:::

  1. 优化 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 值,减少搜索

  1. 优化 resolve.alias 配置

对庞大的第三方模块设置 resolve.alias, 使 webpack 直接使用库的 min 文件,避免库内解析。

 alias: {
  '@': resolve('src'),
},
// 通过以上的配置,引用src底下的common.js文件,就可以直接这么写
import common from '@/common.js';

这样会影响 Tree-Shaking,适合对整体性比较强的库使用,如果是像 lodash 这类工具类的比较分散的库,比较适合 Tree-Shaking,避免使用这种方式。

  1. 优化 resolve.extensions

合理配置 resolve.extensions,减少文件查找默认值:extensions:['.js', '.json'],当导入语句没带文件后缀时,Webpack 会根据 extensions 定义的后缀列表进行文件查找,所以:

  • 列表值尽量少
  • 频率高的文件类型的后缀写在前面
  • 源码中的导入语句尽可能的写上文件后缀,如 require(./data)要写成 require(./data.json)
  1. 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);
};
  1. 优化 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

  1. 使用 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 值。

  1. 在主 config 文件里使用 DllReferencePlugin 插件引入 xxx-manifest.json 文件:
[
  new webpack.DllReferencePlugin({
    context: __dirname,
    manifestrequire(config.dllPath(truetrue))
  }),

  new HtmlWebpackTagsPlugin({ tags: ["/dll/vendor.dll.js"], appendfalse }),

  new CopyWebpackPlugin([
    {
      from: path.join(__dirname, config.dllPath(truefalse)),
      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({ size5 });
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: {
          warningsfalse
        },
        sourceMaptrue
      }
     }),
    ]


3.6、使用自动刷新

借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack 将这些功能都内置了,并且提供了多种方案供我们选择。

Webpack 可以使用两种方式开启监听:

  1. 启动 webpack 时加上--watch 参数;
  2. 在配置文件中设置 watch:true

Webpack 配置官方文档点击这里

module.exports = {
  watchtrue,
  watchOptions: {
    ignored/node_modules/,
    aggregateTimeout300//文件变动后多久发起构建,越大越好
    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: {
  hottrue,
},
plugins: [
  new webpack.HotModuleReplacementPlugin(),
// 显示被替换模块的名称
  new webpack.NamedModulesPlugin(), // HMR shows correct file names
]