Maven编译打包性能优化实践整理
Posted ZWZhangYu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Maven编译打包性能优化实践整理相关的知识,希望对你有一定的参考价值。
文章目录
概览
【1】maven支持并发设置多线程编译
【2】子模块是可以并行编译打包的
【3】每个模块开启一个线程,但是这个线程不是立刻开启的,是由打包程序自己决定启用多少个线程
【4】dependency:analyze优化,寻找那些不需要的依赖,然后移除
【5】并发执行TEST
【6】适当的跳过某些模块,当前构建只处理改动的模块
并行执行
默认情况下Maven 会按顺序串行构建所有的模块,这往往不能充分利用CPU的性能,我们可以设置maven构建为并行模式,即多线程构建对应的模块。
mvn -T 4 install -- will use 4 threads
-- 使用4个线程去构建
mvn -T 1C install
-- 每一个CPU核使用一个线程,逻辑CPU核数
通常情况下建议使用-T 1C 每个CPU一个线程,Linux下可以通过lscpu查询,Windows可以通过任务管理器查看。
Wall-Clock
在某些情况下我们在构建后会出现如下红框所示的提示
默认情况下(没有 -T),Maven 会按顺序而不是并行构建所有模块。 因此,只有一个流程,实际处理时间是累计的。
您以 4 个线程开始构建,因此 总时间除以 4 个线程,总 CPU 时间保持不变(即每个CPU实际处理的时间,但不是最终结果的时间),但您所用的时间是总的时间除以4个线程的一些并行化开销。 这是你看墙上的时钟时看到的时间,因此它被称为挂钟时间。
当前发布只重新编译修改模块
对于一些大型多模块的项目来说,很多时候的发布或者说紧急上线发布都只是修改一小段代码,绝大多数情况仅仅这涉及一个模块。但是在以往的情况下我们却需要clean 和package所有的模块,这显然有些浪费,所以如果需要追求效率,那么可以设置本次发布只编译其中特定的一个或多个模块;
mvn clean install -DskipTests -pl ruoyi-api -T 1C
并行执行单元测试
在执行构建的过程中,单元测试往往会占据较大的一部分执行时间。默认情况下单元测试的执行也是串行执行的,针对这个也是可以进行并行化优化的,maven-sure-fire 插件提供了并行测试的方式,我们可以使用它来优化速度,下面的一个配置作为一个基本的参考
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<argLine>-Xmx2G -XX:MaxPermSize=1G -XX:-UseSplitVerifier -XX:-TieredCompilation -XX:TieredStopAtLevel=1</argLine>
<parallel>methods</parallel>
<forkCount>3C</forkCount>
<reuseForks>true</reuseForks>
<threadCount>20</threadCount>
</configuration>
</plugin>
配置要求:
Junit 4.7+
Surefire plugin 2.16+
Maven Daemon
mvnd 是 Maven Daemon 的缩写 ,翻译成中文就是 Maven 守护进程。
mvnd 是 apache/maven 的一个子项目,它并不是一个全新的构建工具,而是对 maven 的扩展。它内置了 maven,其实现原理是构建了一个或者多个 maven 守护进程来执行构建服务。
maven-mvnd 特性:
【1】嵌入 Maven (所以不需要单独安装 Maven);maven 过渡到 maven-mvnd的过程中实现无缝切换!所以不需要再安装maven或进行复杂的配置更改。
【2】实际的构建发生在一个长期存在的后台进程中,也就是守护进程。如果没有为构建请求服务的空闲守护进程,则可以并行产生多个守护进程。
【3】一个守护进程实例可以处理来自 mvnd 客户机的多个连续请求。
【4】使用 GraalVM 构建的本地可执行文件。与传统的 JVM 相比,它启动更快,使用的内存更少。
【5】maven所支持的命令它也是支持的,需要将mvn改成mvnd即可。
下载链接:https://github.com/mvndaemon/mvnd/releases
其中:bin目录是mvnd的执行程序,conf配置文件目录,mvn就是内嵌的maven模块,其中和我们之前用的maven一样,是可以直接将原来的maven的setting配置文件复制过来的。
# maven 打包命令
mvn clean package -Dmaven.test.skip=true
# mvnd 打包命令
mvnd clean package -Dmaven.test.skip=true
注意:上面的结果并不是在所有情况下都有很大的提升,尤其是在第一次构建时需要拉取插件等肯定要耗时的,但是在我的电脑上(4核)多次测试下,基本上是有一倍左右的性能提升的。
maven依赖优化
在很多的项目中,往往都存在一些冗余的JAR包依赖,可能是某次的测试加入但是之后没有删除,可能是引用传递的依赖,也可能是历史代码但在后面升级后已经有其他的替换方案了,可没有剔除原先的依赖等等情况。想要更好的提升构建速度,那么就需要从源头出发,优化项目本身的大小,剔除一些无用的依赖。我们可以通过mvn dependency:analyze的方式去分析依赖情况。
mvn dependency:analyze
如上图所示,可以看到俩行主要的提示
【Used undeclared dependencies found】
这个是指某些依赖的包在代码中有用到它的代码,但是它并不是直接的依赖(就是说没有在pom中直接声明),是通过引入传递下来的包。
即项目在pom中声明了A.jar的依赖(没有声明B.jar的依赖) ,但是A自己的依赖中引入了B.jar,说明project中的代码用到了B.jar的代码 这个时候你就可以把B.jar直接声明在pom中。
【Unused declared dependencies found】
这个是指我们在pom中声明了依赖,但是在实际代码中并没有用到这个包,也就是多余的包。这个时候我们就可以把这个依赖从pom中剔除。
注意事项:
【1】上面的工具只是帮助我们分析了依赖,但是注意并不是说完全就可以确定对应的依赖可以直接剔除。在决定剔除之前和之后要有仔细的核对和检查,保证不会有影响。
【2】在上面分析之后,可以通过mvn dependency:tree或者IDEA的maven依赖插件去分析下对应的依赖情况,做下核对检查。
小结
【1】本文的主要的优化还是利用多线程的方式打包多模块,其中多模块的数量越多且项目越大那么所取得的优化效果也会越加的明显。
【2】本文多次提及CPU的核数,因此如果构建服务器CPU核数就一个那么就没有必要了,当然就目前来看,maven构建的服务器应该很少是单核的。
【3】需要注意,多线程不一定就比单线程快,这意味这个优化并非一定可以取得好的效果,多线程的启动和切换需要成本,具体的优化需要根据自己的项目多次实践比对。
【4】文章中所提到的只编译修改模块,虽然在发布编译时需要修改命令,但是其取得的效果是很好的,这个可以通过Jenkins设置变量配合一些条件判断进行处理。尤其是跳过一些编译时间长且没有发生变动的代码模块尤其有效,避免了一些无用的操作等待。
【5】合理使用mvn dependency:analyze工具,移除无用的JAR依赖,给项目瘦身,减少项目的体积,从来源上优化。
Taro编译打包优化实践
一、背景
随着项目越来越大,编译的耗时也在默默地不断增加。无论是开发阶段还是生产集成,编译耗时都成为了一个不容小觑的痛点。
在经历了近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编译打包速度,二是提供了一种解决方案,解决分包过大导致无法使用微信开发者工具进行二维码预览的问题。
以上是关于Maven编译打包性能优化实践整理的主要内容,如果未能解决你的问题,请参考以下文章