一、背景
随着我的项目越来越大,编译的耗时也在默默地一直减少。无论是开发阶段还是生产集成,编译耗时都成为了一个不容小觑的痛点。
在经验了近5年的继续开发迭代后,咱们的我的项目也在不久前由原来的微信原生小程序开发形式迁徙至Taro。Taro 是一套应用React 语法的多端开发解决方案,应用 Taro,咱们能够只书写一套代码,再通过 Taro 的编译工具,将源代码别离编译出能够在不同端(微信/百度/支付宝/字节跳动/QQ/京东小程序、快利用、H5、React-Native 等)运行的代码。所以,携程的很多小程序也应用Taro进行开发。
不过,因为业务比拟多,我的项目编译后代码靠近12M。在日常开发阶段执行构建命令,只是编译打包新的开发相干局部文件就须要耗时近1分钟。在生产环境下执行构建命令,编译打包我的项目中所有文件,则长达10分钟。此外,随着基建局部、单个简单页面性能越来越多,代码量也越来越大,会导致主包或者一些分包的大小超过2M,这将使得微信开发者工具的二维码预览性能无奈应用,开发体验十分蹩脚。
针对上述问题,咱们尝试优化Taro编译打包工作。为了优化Taro的编译打包,咱们须要理解Taro内置的Webpack的配置,而后应用webpack-chain提供的办法链式批改配置。接下来,咱们还须要解决分包过大无奈进行二维码预览的问题。
二、 Taro内置的Webpack配置
咱们晓得Taro编译打包的工作是由webpack来实现的,既然想要优化打包速度,首先要晓得Taro是如何调用webpack进行打包的,同时也要理解其内置的webpack配置是怎么的。
通过浏览Taro源码后能够晓得,Taro是在@tarojs/mini-runner/dist/index.js文件中,调用了webpack进行打包,能够自行去查看相干的代码。
咱们着重关注该文件中的build函数,代码如下。
function build(appPath, config) { return __awaiter(this, void 0, void 0, function* () { const mode = config.mode; /** process config.sass options */ const newConfig = yield chain_1.makeConfig(config); /** initialized chain */ const webpackChain = build_conf_1.default(appPath, mode, newConfig); /** customized chain */ yield customizeChain(webpackChain, newConfig.modifyWebpackChain, newConfig.webpackChain); if (typeof newConfig.onWebpackChainReady === 'function') { newConfig.onWebpackChainReady(webpackChain); } /** webpack config */ const webpackConfig = webpackChain.toConfig(); return new Promise((resolve, reject) => { const compiler = webpack(webpackConfig); const onBuildFinish = newConfig.onBuildFinish; let prerender; const onFinish = function (error, stats) { if (typeof onBuildFinish !== 'function') return; onBuildFinish({ error, stats, isWatch: newConfig.isWatch }); }; const callback = (err, stats) => __awaiter(this, void 0, void 0, function* () { if (err || stats.hasErrors()) { const error = err !== null && err !== void 0 ? err : stats.toJson().errors; logHelper_1.printBuildError(error); onFinish(error, null); return reject(error); } if (!lodash_1.isEmpty(newConfig.prerender)) { prerender = prerender !== null && prerender !== void 0 ? prerender : new prerender_1.Prerender(newConfig, webpackConfig, stats, config.template.Adapter); yield prerender.render(); } onFinish(null, stats); resolve(stats); }); if (newConfig.isWatch) { logHelper_1.bindDevLogger(compiler); compiler.watch({ aggregateTimeout: 300, poll: undefined }, callback); } else { logHelper_1.bindProdLogger(compiler); compiler.run(callback); } }); });}
能够看到,该函数承受两个参数,appPath和config,appPath是以后我的项目的目录,参数config就是咱们编写的Taro配置。在调用webpack前,Taro会解决webpackConfig,包含将Taro内置的webpack配置,以及将用户在Taro配置文件中的webpackChain配置进去。
定位到了webpack地位,那么让咱们来看看Taro最终生成的webpack配置的相干代码。
const webpack = (options, callback) => { const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); if (webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; if (Array.isArray(options)) { compiler = new MultiCompiler( Array.from(options).map(options => webpack(options)) ); } else if (typeof options === "object") { options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); compiler.options = options; new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler); if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if (callback) { if (typeof callback !== "function") { throw new Error("Invalid argument: callback"); } if ( options.watch === true || (Array.isArray(options) && options.some(o => o.watch)) ) { const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {}; return compiler.watch(watchOptions, callback); } compiler.run(callback); } return compiler;};
须要留神的是在开发和生产环境下,内置的webpack配置是有差异的,比方在生产环境下,才会调用terser-webpack-plugin进行文件压缩解决。咱们用的是vscode代码编辑器,在调用webpack地位前,debugger打断点,同时应用console命令输入变量webpackConfig,即最终生成的webpack配置。在vscode自带的命令行工具DEBUG CONSOLE,能够十分不便的点击开展对象属性,查看Taro生成的webpack配置。这里展现下,在development环境下,Taro内置的webpack配置,如下图。
这些都是常见的webpack配置,咱们次要关注两局部的内容,一是module中配置的rules,配置各种loader来解决匹配的对应的文件,例如常见的解决scss文件和jsx文件。二是plugins中配置的TaroMiniPlugin插件,该插件是Taro内置的,次要负责了将代码编译打包成小程序代码的工作。
当初,咱们理解了Taro中的webpack配置以及他们的一个工作过程,接下来该思考的是如何去批改优化该配置,来帮忙咱们优化编译打包的速度。须要留神的是,Taro打包用到了webpack-chain机制。webpack配置实质是一个对象,创立批改比拟麻烦,webpack-chain就是提供链式的 API 来创立和批改webpack 配置。API的 Key 局部能够由用户指定的名称援用,这有助于 跨我的项目批改配置形式 的标准化。
webpack-chain自身提供了很多的例子,能够参考:https://github.com/Yatoo2018/webpack-chain/tree/zh-cmn-Hans
三、优化Webpack打包配置
通过前文的介绍,咱们曾经理解了Taro生成的webpack配置,也把握了批改这些配置的办法,接下来就是思考怎么批改webpack配置能力优化编译打包速度。为此,咱们引入了speed-measure-webpack-plugin,该插件能够统计出编译打包过程中,plugin和loader的耗时状况,能够帮忙咱们明确优化方向。
将speed-measure-webpack-plugin配置好后,再次执行构建命令,输入后果如下图所示。
能够看到,总共3分钟的编译工夫,TaroMiniPlugin就占了2分多钟,耗时还是很重大的。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速度,导致统计进去该插件耗时重大。
因而,优化Webpack的打包次要就在这两loader,也就相当于优化了TaroMiniPlugin。而在优化计划上,咱们选取了两种常见的优化策略:多核和缓存。
3.1 多核
对于多核,咱们这里采纳webpack官网举荐的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语法规定删除该条具名规定即可。
最初,通过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等其余的耗时loader。
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-plugin-compiler-optimization插件
有了下面的优化计划之后,咱们于是着手写优化插件。总的来说,本插件是利用了Taro插件化机制裸露进去的modifyWebpackChain钩子,采纳webpack-chain办法,链式批改webpack配置。将多核和缓存优化策略配置到Taro的webpack中,来晋升编译打包速度。
插件的装置地址如下:
GitHub:https://github.com/CANntyield/taro-plugin-compiler-optimization
Npm:https://www.npmjs.com/package/taro-plugin-compiler-optimization
首先,在我的项目中装置插件:
npm install --save-dev thread-loader cache-loader taro-plugin-compiler-optimization
而后,在taro的config.js中增加如下脚本:
// 将其配置到taro config.js中的plugins中// 根目录/config/index.jsplugins: ['taro-plugin-compiler-optimization']
最初,咱们再执行一下打包工作,发现总耗时曾经缩短至56.9s,TaroMiniPlugin、babel-loader还有css-loader耗时有着显著的缩短,而配置了缓存的TerserPlugin也从22.8s缩短至13.9s,优化成果还是很显著的。
四、压缩资源文件
微信开发者工具中,如果想要在真机上调试小程序,通常是须要进行二维码预览的。因为微信限度,打包进去的文件,主包、分包文件不能超过2M,否则进行二维码预览无奈胜利。然而随着我的项目越来越大,主包文件超过2M是没方法的事件,尤其是通过babel-loader解决后的文件,更是会蕴含了十分多的正文、过长的变量名等,导致文件过大。行业内最基本的解决办法是分包,因为微信小程序曾经将总包大小调整到了10M。不过本文不探讨如何分包,这里次要探讨如何调整包的大小。
咱们在执行build构建命令时,启用terser-webpack-plugin压缩文件,将主包文件放大至2M以下。不过,问题也是很显著的,那就是每次都须要破费大量的工夫用于构建打包工作,效率切实是太低了。而且这种状况下,不会监听文件变动,进行模块热替换工作,这种工作效率十分低下。
因而,咱们的策略是在开发环境下配置webpack,调用terser-webpack-plugin进行压缩。同时配置插件参数,压缩指定文件,而不是全副压缩。关上微信开发者工具,点开代码依赖剖析,如下图。
从图中能够看到,主包文件曾经超过了2M。其中common.js、taro.js、vendors.js、app.js四个文件显著较大,并且每个Taro我的项目编译打包后必然生成这四个文件。pages文件夹也高达1.41M,该文件夹是咱们配置的tabBar页面,因而该文件夹大小间接受到tabBar页面复杂度的影响。除此之外,其余文件都比拟小,能够临时不思考进行解决。
首先,执行以下命令装置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配置文件plugins中引入该Taro插件,倡议在config/dev.js配置文件中引入,只会在开发环境下才会应用到。
// config/dev.jsplugins: [ path.resolve(__dirname, 'plugins/minifyMainPackage.js'),]
最初咱们来看看压缩后主包的大小,能够发现曾经缩小至1.42M了,绝对于此前的3.45M,压缩了50%左右,能够解决大部分无奈进行二维码预览打包的场景了。
不过,目前,微信小程序曾经反对分包Lee,不过主包还是不能超过2M,下面的形式针对的是主包太大的解决方案。本文次要解决了两个问题:一是用于优化Taro编译打包速度,二是提供了一种解决方案,解决分包过大导致无奈应用微信开发者工具进行二维码预览的问题。