共计 9063 个字符,预计需要花费 23 分钟才能阅读完成。
一、背景
随着我的项目越来越大,编译的耗时也在默默地一直减少。无论是开发阶段还是生产集成,编译耗时都成为了一个不容小觑的痛点。
在经验了近 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.js
plugins: ['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.js
plugins: [path.resolve(__dirname, 'plugins/minifyMainPackage.js'),
]
最初咱们来看看压缩后主包的大小,能够发现曾经缩小至 1.42M 了,绝对于此前的 3.45M,压缩了 50% 左右,能够解决大部分无奈进行二维码预览打包的场景了。
不过,目前,微信小程序曾经反对分包 Lee,不过主包还是不能超过 2M,下面的形式针对的是主包太大的解决方案。本文次要解决了两个问题:一是用于优化 Taro 编译打包速度,二是提供了一种解决方案,解决分包过大导致无奈应用微信开发者工具进行二维码预览的问题。