编写插件,将 Taro 编译打包耗时缩短至三分之一

1. 背景

随着项目越来越大,编译的耗时也在默默的不断增加。无论是开发阶段还是生产集成,编译耗时成为了一个不容小觑的痛点。

我们的项目由微信原生迁移至 Taro,先后经历了约 5 年的持续开发迭代,项目编译后代码接近 12M。在日常开发阶段执行构建命令,只是编译打包开发相关的部分文件时,耗时近 1 分钟。在生产环境下执行构建命令,编译打包项目中所有文件,长达 10 分钟。

此外,随着基建部分、单个复杂页面功能会越来越多,代码量也越来越大,会导致主包或者一些分包的大小会超过 2M,这将会使得微信开发者工具的二维码预览功能无法使用,开发体验非常糟糕。

针对上述问题,尝试优化 Taro 编译打包工作,本文分为以下三个部分。

了解 Taro 内置的 Webpack 配置,使用 webpack-chain 提供的方法链式修改配置。
编写 Taro 插件,将 Taro 编译打包耗时缩短至三分之一。
编写 Taro 插件,解决分包过大无法进行二维码预览的问题。

2. Taro 内置的 webpack 配置

我们知道 Taro 编译打包的工作是由 webpack 来完成的,既然想要优化打包速度,首先要知道 Taro 是如何调用 webpack 进行打包的,同时也要了解其内置的 webpack 配置是怎样的。

通过阅读 Taro 源码后可以知道,Taro 是在@tarojs/mini-runner/dis/index.js 文件中,调用了 webpack 进行打包。我们可以着重关注该文件中的 build 函数,代码如下,该函数接受两个参数,appPath 和 config,appPath 是当前项目的目录,参数 config 就是我们编写的 Taro 配置。在调用 webpack 前,Taro 会处理 webpackConfig,包括将 Taro 内置的 webpack 配置进去,以及将用户在 Taro 配置文件中的 webpackChain 配置进去。

exportdefaultasyncfunction build (appPath: string, config: IBuildConfig): Promise<webpack.Stats> {
  const mode = config.mode

  /** process config.sass options */
  const newConfig = await makeConfig(config)

  /** initialized chain */
  const webpackChain = buildConf(appPath, mode, newConfig)

  /** customized chain */
  await customizeChain(webpackChain, newConfig.modifyWebpackChain, newConfig.webpackChain)

  if (typeof newConfig.onWebpackChainReady === 'function') {
    newConfig.onWebpackChainReady(webpackChain)
  }

  /** webpack config */
  const webpackConfig: webpack.Configuration = webpackChain.toConfig()

  returnnewPromise<webpack.Stats>((resolve, reject) => {
    const compiler = webpack(webpackConfig) //调用webpack
    const onBuildFinish = newConfig.onBuildFinish
    let prerender: Prerender

    const onFinish = function (error, stats: webpack.Stats | null) {
      ...
    }

    const callback = async (err: Error, stats: webpack.Stats) => {
      ...
    }

    if (newConfig.isWatch) {
      bindDevLogger(compiler)
      compiler.watch({
        aggregateTimeout: 300,
        poll: undefined
      }, callback)
    } else {
      bindProdLogger(compiler)
      compiler.run(callback)
    }
  })
}

定位到了 webpack 位置,那么让我们来看看 Taro 最终生成的 webpack 配置是怎样的呢,需要注意的是在开发和生产环境下,内置的 webpack 配置是有差别的,比如在生产环境下,才会调用 terser-webpack-plugin 进行文件压缩处理。我们用的是 vscode 代码编辑器,在调用 webpack 位置前,debugger 打断点,同时使用 console 命令输出变量 webpackConfig,即最终生成的 webpack 配置。在 vscode 自带的命令行工具 DEBUG CONSOLE,可以非常方便的点击展开对象属性,查看 Taro 生成的 webpack 配置。这里展示下,在 development 环境下,Taro 内置的 webpack 配置,如下图。

Taro 内置的 webpack 配置
这些都是常见的 webpack 配置,我们主要关注两部分的内容,一是 module 中配置的 rules,配置各种 loader 来处理匹配的对应的文件,例如常见的处理 scss 文件和 jsx 文件。二是 plugins 中配置的 TaroMiniPlugin 插件,该插件是 Taro 内置的,主要负责了将代码编译打包成小程序代码的工作。

现在了解了 Taro 中的 webpack 配置,接下来该考虑的是如何去修改该配置,来帮助我们优化编译打包。这里 Taro 提供了 webpack-chain 机制,webpack 配置本质是一个对象,创建修改比较麻烦。webpack-chain 就是提供链式的 API 来创建和修改 webpack 配置。具体用法可以看官方 github,上面提供了大量的案例用于学习。

https://github.com/Yatoo2018/webpack-chain/tree/zh-cmn-Hans

3. 速度优化,耗时缩短至三分之一

我们已经了解了 Taro 生成的 webpack 配置,也掌握了修改这些配置的方法,接下来就是考虑修改 webpack 配置,来优化编译打包速度。我这里是引入了 speed-measure-webpack-plugin。该插件可以统计出编译打包过程中,plugin 和 loader 的耗时情况,可以帮助我们明确优化方向。

引入 speed-measure-webpack-plugin
将 speed-measure-webpack-plugin 配置好后,执行构建命令,输出结果如下图。

图中数据显示在 plugins 中,TaroMiniPlugin 耗时严重,这个是 Taro 内置的 webpack 插件,Taro 的绝大多数编译打包工作都是配置在这里的进行的,例如获取配置内容、处理分包和 tabbar、读取小程序配置的页面添加 dependencies 数组中进行后续处理、生成小程序相关文件等。次之耗时严重的就是 TerserPlugin,该插件主要进行压缩文件工作。

而在 loaders 耗时统计中,babel-loader 耗时两分半,sass-loader 耗时两分钟,这两者耗时最为严重。这两者也是导致 TaroMiniPlugin 耗时如此严重的主要原因。因为该插件,会将小程序页面、组件等文件,通过 webpack 的 compilation.addEntry 添加到入口文件中,后续会执行 webpack 中一个完整的 compliation 阶段,在这个过程中会调用配置好的 loader 进行处理。当然也会调用 babel-loader 和 scss-loader 进行处理 js 文件或者 scss 文件,这就严重拖慢了 TaroMiniPlugin 速度,导致统计出来该插件耗时严重。

因此优化这两 loader,也就相当于优化了 TaroMiniPlugin。这里主要使用了两种优化策略,多核和缓存。

3.1 多核

这里是采用了官方推荐的 thread-loader,可以将非常消耗资源的 loaders 转存到 worker pool。根据上述耗时统计,可以知道 babel-loader 是最耗时的 loader,因此将 thread-loader 放置在 babel-loader 之前,这样 babel-loader 就会在一个单独的 worker pool 中运行,从而提高编译效率。

清楚了优化方法,就该考虑如何配置到 webpack 中。简单来说,就是利用 Taro 插件化机制提供的 modifyWebpackChain 钩子,采用 webpack-chain 提供的方法,链式修改 webpack 配置。

具体做法是,首先想办法删除 Taro 中内置的 babel-loader,我们可以回头查看 Taro 内置的 webpack 配置,发现处理 babel-loader 的那条具名规则为'script',如下图,然后使用 webpack-chain 语法规则删除该条具名规则即可。

名为'script'的规则
最后,通过 webpack-chain 提供的 merge 方法,重新配置处理 js 文件的 babel-loader,同时在 babel-loader 之前引入 thread-loader。这样就完成了。

ctx.modifyWebpackChain(args => {
  const chain = args.chain
  chain.module.rules.delete('script') // 删除Taro中配置的babel-loader
  chain.merge({ // 重新配置babel-loader
    module: {
      rule: {
        script: {
          test: /\.[tj]sx?$/i,
          use: {
            threadLoader: {
              loader: 'thread-loader', // 多核构建
            },
            babelLoader: {
              loader: 'babel-loader',
              options: {
                cacheDirectory: true, // 开启babel-loader缓存
              },
            },
          },
        },
      },
    }
  })
})

目前引入的 thread-loader 只处理 babel-loader,我尝试过去用其处理 css-loader,但是失败了。因为 thread-loader 的限制,可见 issue,目前仍未解决。
Cannot read property 'outputOptions' of undefined #66

3.2 缓存

缓存优化策略也是针对这两部分进行,一是使用 cache-loader 缓存用于处理 scss 文件的 loaders,二是 babel-loader,设置参数 cacheDirectory 为 true,开启 babel-loader 缓存。

在使用 cache-loader 缓存时,额外注意的是,需要将 cache-loader 放置在 css-loader 之前,mini-css-extract-plugin 之后。实践中发现,放置在 mini-css-extract-plugin/loader 之前,是无法有效缓存生成的文件。

具体做法类似上面,主要是查看 Taro 内置的 webpack 配置,然后使用 webpack-chain 语法,定位到对应的位置,最后调用 before 方法,插入到 css-loader 之前。

// 通过webpack-chain方法,将cache-loader放置在css-loader之前,mini-css-extract-plugin之后
chain.module.rule('scss').oneOf('0').use('cacheLoader').loader('cache-loader').before('1')
chain.module.rule('scss').oneOf('1').use('cacheLoader').loader('cache-loader').before('1')
注意: 缓存默认是保存在 node_moduls/.cache 中,如下图。因此在使用执行编译打包命令时,需要注意当前的打包环境是否能够将缓存保留下来,否则缓存配置无法带来速度优化效果。

值得一提的是,看上图我们可以发现,terser-webpack-plugin 也是开启了缓存的。我们再回头看下,下图是 Taro 中配置的参数。我们可以发现 cache 和 parallel 都为 true,分别是开启了缓存以及并行编译。

3.3 小结

我已经将上述优化方案,写成了 Taro 插件,已经放在了 npm 上,大家可以很方便的使用。总的来说,本插件是利用了 Taro 插件化机制暴露出来的 modifyWebpackChain 钩子,采用 webpack-chain 方法,链式修改 webpack 配置。将多核和缓存优化策略配置到 Taro 的 webpack 中,来提升编译打包速度。本案例中,优化前 3m9s,优化后 56.8s,可以将编译打包耗时缩短至三分之一左右。

最后看看优化后的耗时统计,可以发现总耗时已经缩短至 56.9s,TaroMiniPlugin、babel-loader 还有 css-loader 耗时有着明显的缩短,而配置了缓存的 TerserPlugin 也从 22.8s 缩短至 13.9s。优化效果还是很显著的。

3.4 使用

npm install --save-dev thread-loader cache-loader taro-plugin-compiler-optimization
// 将其配置到taro config.js中的plugins中
// 根目录/config/index.js
plugins: ['taro-plugin-compiler-optimization']
安装好了 npm 包后,将 Taro 插件写入到 Taro 配置中即可。

GitHub: https://github.com/CANntyield/taro-plugin-compiler-optimization
Npm: https://www.npmjs.com/package/taro-plugin-compiler-optimization

4. 压缩项目文件

微信开发者工具中,如果想要在真机上调试小程序,通常是需要进行二维码预览的。由于微信限制,打包出来的文件,主包、分包文件不能超过 2M,否则进行二维码预览无法成功。但是随着项目越来越大,主包文件超过 2M 是没办法的事情,尤其是通过 babel-loader 处理后的文件,更是会包含了非常多的注释、过长的变量名等,导致文件过大。比较简单能够想到的办法是,将跟目前调试目标无关的主包代码手动进行删除,留下入口用于调试。当然这样做也有一些问题,一是每次手动删除会比较麻烦,调试完之后需要自己手动恢复,每次预览都需要重启项目。二是微信限制 tabbar 最少 2 个、最多 5 个 ,这就导致存在可能单个 tabbar 超过 2M 的情况,这样更是麻烦。

也还有一种解决办法,那就是执行 build 构建命令,这样就可以启用 terser-webpack-plugin 压缩文件,这样就可以将主包文件缩小至 2M 以下。问题也是很明显的,那就是每次都需要花费大量的时间用于构建打包工作,效率实在是太低了。而且,这种情况下,不会监听文件变化,进行模块热替换工作,这种工作效率更是低到令人发指。

思路比较简单,就是在开发环境下,配置 webpack,调用 terser-webpack-plugin 进行压缩。同时配置插件参数,压缩指定文件。

打开微信开发者工具,点开代码依赖分析,如下图。可以看到主包文件很明显已经超过了 2M。可以发现 common.js、taro.js、vendors.js、app.js 这几个文件比较大,并且都是通过根据我们的代码,Taro 编译打包后必然生成的。还有 pages 文件夹更是高达 1.41M,但这是我们配置的 tarBar 文件,通常这些页面都是我们自己编写的。除此之外,其他文件都比较小,可以暂时不考虑进行处理。

我们的目标就是压缩这几个比较大的文件,用起来很简单,首先执行以下命令安装 terser-webpack-plugin。

npm install -D terser-webpack-plugin@3.0.5
需要注意的是,terser-webpack-plugin 最新版本已经是 v5 了,这个版本是根据 webpack5 进行优化的,但是不支持 webpack4,因此需要自己额外指定版本,才能使用。这里我选择的是 3.0.5,跟 Taro 中使用的 terser-webpack-plugin 是同一个版本。其中,传入的参数配置也是跟 Taro 一样,我们要做的是,将需要进行压缩的文件路径添加到 test 数组中即可,其中已经默认配置了 common.js、taro.js、vendors.js、app.js、pages/homoe/index.js 文件。

注意:文件路径是,Taro 编译打包后最终生成的文件路径,不是项目中的文件路径。

同样的,我们需要在 Taro 配置文件 plugins 中引入该 Taro 插件,建议在 config/dev.js 配置文件中引入,只会在开发环境下才会使用到。

// config/dev.js
plugins: [
    path.resolve(__dirname, 'plugins/minifyMainPackage.js'),
]

最后我们来看看压缩后主包的大小,可以发现已经减少至 1.42M 了,相对于此前的 3.45M,压缩了 50%左右,可以解决大部分无法进行二维码预览打包的场景了。

使用
点击以下链接,将该 Taro 插件下载到项目中,修改代码中的 test 数组,配置想要压缩的文件路径。然后按照 Taro 插件文件所在路径,在 Taro 配置中引入即可。

GitHub: https://github.com/CANntyield/taro-plugin-repository/blob/main/minifyMainPackage.js

5.总结

本文主要是解决在使用 Taro 开发大型项目时经常遇到的两个编译打包相关的问题,在解决问题的过程中,深入源码去了解 Taro 的编译打包机制和 webpack 相关机制,检索常用优化相关的解决方案,最终完成了这两个 Taro 插件。一是用于优化 Taro 编译打包速度,二是提供了一种解决方案,解决分包过大导致无法使用微信开发者工具进行二维码预览的问题。在文中我都给出了使用方法,大家可以尝试下,如有问题,欢迎指出探讨。

原文链接:编写插件,将 Taro 编译打包耗时缩短至三分之一

posted @ 2022-02-16 15:05  远方的少年🐬  阅读(327)  评论(0编辑  收藏  举报