kd-cloud-web / Blog

一群人, 关于前端, 做一些有趣的事儿
13 stars 1 forks source link

关于webpack打包优化的一点实验 #34

Open mhfe123 opened 5 years ago

mhfe123 commented 5 years ago

我们都知道每次启动项目的时候重新打包都要花费很长时间,有时候电脑卡,性能不好可能时间会更久,那么提高我们打包的速度就是我们绕不开的话题。那么webpack打包为什么会慢,总结来说就是三点:

实验一:缩小文件的搜索范围

当我们在写代码的时候,引用文件的语句尽量写完整,比如后缀补全,能确定位置的文件尽量使用绝对路径,当然这种做法有点本末倒置,毕竟有了很多插件帮我们实现这些东西,而且经实践表明这方面对webpack构建的提升貌似没有太大的影响,甚至还会负影响(因为随手写的demo比较小,可能结果不是很准确,需要后续在确认)。

那么回归正题,因为我们前端经常会引用第三方包,众所周知,node_modules文件夹里的海量文件,光看着就头晕,所以我们优化大部分都是对准第三方库的。

优化loader配置

由于loader对文件的转换操作很耗时,所以我们要让尽可能少的文件被处理,可以通过test、include、exclude三个配置项来命中loader所要的文件,尽可能减少需要处理的文件,以vue为例:

rules: [
     {
       test: /\.vue$/,
       loader: 'vue-loader',
       // exclude: /node_modules/,
       include: path.resolve(__dirname, '../src')
     }
]

优化resolve.modules配置

resolve.modules 的默认值是 ['node_modules'],含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。

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

module.exports = {
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中 __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')]
  },
};

优化resolve.mainFields配置

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

可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境中,准对不同的运行环境需要使用不同的代码。有的用于浏览器环境,有的用于node.js环境,为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main 字段去描述入口文件的位置,可以这样配置 Webpack:

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

使用本方法,需要考虑所有运行时依赖的第三方模块入口文件的描述字段,有一个模块出错,都有可能造成构建的代码无法运行(个人不建议使用)。

优化resolve.alias配置

在项目中,我们使用的一些第三方包会比较大,以Vue为例,安装到node_modules目录下的Vue库包含很多个js文件,dist/vue.js用于开发环境,dist/vue.min.js 用于线上环境,默认情况下webpack会从入口文件./node_modules/vue/vue.js开始递归解析和处理依赖的几十个文件,这非常耗时,我们可以通过配置直接引用单独的、完整的vue.min.js文件,从而跳过耗时的递归与解析,配置如下:

resolve: {
    alias: {
      vue: path.resolve(__dirname, '../node_modules/vue/dist/vue.min.js')
    },
  },

但是,使用本方法以后可能会让我们代码中含有很多可能永远不会被执行的代码,来自第三方库的,会影响到Tree-Sharking去删除无效代码的优化,所以不建议在线上使用。

优化resolve.extensions配置

用于配置在尝试过程中用到的后缀列表,默认是:

extensions: ['.js', '.json']

也就是说当遇到 require('./data') 这样的导入语句时,Webpack 会先去寻找 ./data.js 文件,如果该文件不存在就去寻找 ./data.json 文件,如果还是找不到就报错。

如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions 的配置也会影响到构建的性能。 在配置 resolve.extensions 时你需要遵守以下几点,以做到尽可能的优化构建性能:

相关 Webpack 配置如下:

module.exports = {
  resolve: {
    // 尽可能的减少后缀尝试的可能性
    extensions: ['js'],
  },
};

可能是demo原因,测试用例比较少,得出结果并没有什么卵用,甚至还有负作用,需要后续在实验。

优化 module.noParse 配置

module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。

module: {
    noParse: [/vue\.min\.js$/]
}

注意被忽略的文件里不要包含importrequiredefine等模块化语句。

实验二:使用DllPlugin

在介绍 DllPlugin 前先给大家介绍下 DLL。 用过 Windows 系统的人应该会经常看到以 .dll 为后缀的文件,这些文件称为动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据。

要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:

为什么给 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会在重新编译,而是直接使用动态链接库中的代码。 由于动态链接库中大多数包含的是常用的第三方模块,例如 react、react-dom,只要不升级这些模块的版本,动态链接库就不用重新编译。

实验三: 使用HappyPack

我们都知道webpack的打包跟计算机性能有很大关系,但是我们通常使用webpack的时候就跟我们js执行一样,都是单线程的,所以如果可以重新利用计算机多核的设计,也可以提高我们的打包速度。

使用 HappyPack

分解任务和管理线程的事情 HappyPack 都会帮你做好,你所需要做的只是接入 HappyPack。 接入 HappyPack 的相关代码如下:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
        use: ['happypack/loader?id=babel'],
        // 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
        }),
      },
    ]
  },
  plugins: [
    new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
      // ... 其它配置项
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中一样
      loaders: ['css-loader'],
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};

以上代码有两点重要的修改:

在实例化 HappyPack 插件的时候,除了可以传入 idloaders 两个参数外,HappyPack 还支持如下参数:

module.exports = { plugins: [ new HappyPack({ // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: 'babel', // 如何处理 .js 文件,用法和 Loader 配置中一样 loaders: ['babel-loader?cacheDirectory'], // 使用共享进程池中的子进程去处理任务 threadPool: happyThreadPool, }), new HappyPack({ id: 'css', // 如何处理 .css 文件,用法和 Loader 配置中一样 loaders: ['css-loader'], // 使用共享进程池中的子进程去处理任务 threadPool: happyThreadPool, }), new ExtractTextPlugin({ filename: [name].css, }), ], };


接入 HappyPack 后,你需要给项目安装新的依赖:`npm i -D happypack`

安装成功后重新执行构建你就会看到以下由 HappyPack 输出的日志:

```javascript
Happy[babel]: Version: 4.0.0-beta.5. Threads: 3
Happy[babel]: All set; signaling webpack to proceed.
Happy[css]: Version: 4.0.0-beta.5. Threads: 3
Happy[css]: All set; signaling webpack to proceed.

说明你的 HappyPack 配置生效了,并且可以得知 HappyPack 分别启动了3个子进程去并行的处理任务。

HappyPack 原理

在整个 Webpack 构建流程中,最耗时的流程可能就是 Loader 对文件的转换操作了,因为要转换的文件数据巨多,而且这些转换操作都只能一个个挨着处理。 HappyPack 的核心原理就是把这部分任务分解到多个进程去并行处理,从而减少了总的构建时间。

从前面的使用中可以看出所有需要通过 Loader 处理的文件都先交给了 happypack/loader 去处理,收集到了这些文件的处理权后 HappyPack 就好统一分配了。

每通过 new HappyPack() 实例化一个 HappyPack 其实就是告诉 HappyPack 核心调度器如何通过一系列 Loader 去转换一类文件,并且可以指定如何给这类转换操作分配子进程。

核心调度器的逻辑代码在主进程中,也就是运行着 Webpack 的进程中,核心调度器会把一个个任务分配给当前空闲的子进程,子进程处理完毕后把结果发送给核心调度器,它们之间的数据交换是通过进程间通信 API 实现的。

核心调度器收到来自子进程处理完毕的结果后会通知 Webpack 该文件处理完毕。

同样可能因为demo原因,效果不明显,还有负作用,需要后续继续实验。