当前位置: 首页 > 科技观察

Webpack性能的多进程封装

时间:2023-03-12 20:43:59 科技观察

在之前的文章《Webpack 性能系列一: 使用 Cache 提升构建性能》中,我们讨论了如何在Webpack的上下文中应用各种缓存措施来提高构建性能。接下来,我们继续聊聊Webpack计划中一些有效的并行计算。缓存的本质是保存第一轮计算后的结果,下次直接复用计算结果,跳过计算过程;并行的本质是同时并发执行多个操作,以提高单位时间内的计算效率。是计算机科学中常用的性能优化方法。受限于Node.js的单线程架构,原生Webpack对所有资源文件的所有解析、翻译、合并等操作,本质上都是在同一个线程中串行执行,CPU利用率极低。因此,很自然地,社区中出现了一些基于多进程模式运行Webpack的解决方案,或者Webpack构建过程的某一部分,例如:HappyPack:Runresourceloadinglogicinmulti-processmodeThread-loader:webpack官方产品,多进程模式也运行资源加载逻辑TerserWebpackPlugin:支持多进程代码压缩,uglify功能Parallel-Webpack:多进程运行多个Webpack构建实例这些解决方案的核心设计非常相似:创建为某个计算任务创建一个子进程,然后通过IPC将需要的参数传递给子进程,开始计算操作。计算完成后,子进程会将结果通过IPC传回主进程,主进程托管的组件实例将结果提交给Webpack。下面,我将介绍各个方案的用法、原理和缺点,读者可以根据自己的需要进行选择。使用HappyPackHappyPack是一个Webpack组件库,它使用多进程运行文件加载器-加载器序列,从而提高构建性能。可以算是Webpack社区最流行的并发方案,但是作者已经明确表示不会继续维护了。推荐读者优先使用Webpack官方推出的类似解决方案:Thread-loader。官方链接:https://github.com/amireh/happypack如何使用基本用法要使用,首先安装依赖:yarnaddhappypack之后需要将原来的loader配置替换成happypack/loader,如:module.exports={//...module:{rules:[{test:/\.js$/,//使用happypack/loader替换原来的Loader配置use:'happypack/loader',//use:[//{//loader:'babel-loader',//options:{//presets:['@babel/preset-env']//}//},//'eslint-loader'//]}]}};之后,需要创建一个happypack插件实例,将原来的loader配置迁移到插件中。一个完整的例子:constHappyPack=require('happypack');module.exports={//...module:{rules:[{test:/\.js$/,use:'happypack/loader',//use:[//{//loader:'babel-loader',//options:{//presets:['@babel/preset-env']/////},//'eslint-loader'//]}]},plugins:[newHappyPack({loaders:[{loader:'babel-loader',option:{presets:['@babel/preset-env']}},'eslint-loader']})]};配置完成后,再次启动npxwebpack命令,利用HappyPack的多进程能力,提升构建性能。以Three.js为例,项目包含362个JS文件,共约3万行代码:开启HappyPack前,构建时间约为11000ms至18000ms,开启后耗时减少至5800ms至5000ms之间8000ms,提升约47%。配置多个实例上面的简单示例只能使用相同的Loader序列处理相同的文件类型。在实际应用中,可以为不同的文件配置多个对应的loader数组,例如:constHappyPack=require('happypack');module。exports={//...module:{rules:[{test:/\.js?$/,use:'happypack/loader?id=js'},{test:/\.less$/,use:'happypack/loader?id=styles'},]},plugins:[newHappyPack({id:'js',loaders:['babel-loader','eslint-loader']}),newHappyPack({id:'styles',loaders:['style-loader','css-loader','less-loader']})]};例子中js和less资源使用happypack/loader作为唯一的loader,赋值id='js'|“样式”参数;其次,示例中创建了两个HappyPack插件实例,分别配置了处理js和css的loaders数组。happypack/loader通过id值关联到HappyPack实例,从而实现多资源配置。上面共享线程池的多实例模式更贴近实际应用场景,但是默认情况下,每个HappyPack插件实例管理自己消费的进程,导致整体需要维护大量的进程池,这进而带来新的性能损失。为此,HappyPack提供了一套简单易用的共享进程池功能。使用它只需要创建一个HappyPack.ThreadPool实例并通过size参数限制进程总数,然后将该实例配置到各个HappyPack插件的threadPool属性即可,例如:constos=require('os')constHappyPack=require('happypack');consthappyThreadPool=HappyPack.ThreadPool({size:os.cpus().length-1});module.exports={//。.plugins:[newHappyPack({id:'js',threadPool:happyThreadPool,loaders:['babel-loader','eslint-loader']}),newHappyPack({id:'styles',threadPool:happyThreadPool,loaders:['style-loader','css-loader','less-loader']})]};使用共享进程池功能后,HappyPack会预先创建一组共享的HappyThread对象,所有插件实例的资源转换需求最终会通过HappyThread对象转发给空闲进程处理,从而保证进程总数可控。原理HappyPack的运行流程如下图所示:大致可以分为:happypack/loader收到翻译请求后,从Webpack配置中读取对应的HappyPack插件实例,调用插件的compile方法-ininstance创建一个HappyThread实例(或者从HappyThreadPoolIdle例子中取出来)HappyThread内部调用child_process.fork创建子进程,并执行HappyWorkerChannel文件HappyWorkerChannel创建一个HappyWorker,开始执行Loader翻译逻辑.中间过程经历了好几层,最后HappyWorker类重新实现了一套类似于WebpackLoader的翻译逻辑。代码复杂度比较高,读者可以稍微看懂。缺点虽然HappyPack能够有效提升Webpack的打包构建速度,但是它也有一些明显的缺点:作者已经明确表示不会继续维护,扩展性和稳定性得不到保证。随着Webpack自身的发展和迭代,可以预见,总会有那么一天,HappyPack无法完全兼容Webpack。HappyPack底层以自己的方式重新实现了loader逻辑。源码和使用方法没有Thread-loader那么清爽和简单。不支持某些装载机。例如awesome-typescript-loader使用Thread-loaderThread-loader也是一个多进程运行loader提高Webpack构建性能的组件,在功能上和HappyPack非常相似。两者的主要区别在于:Thread-loader是Webpack官方提供的,目前还在不断迭代维护中。从理论上讲,它更可靠。Thread-loader只提供了一个单一的loader组件,使用起来比较简单。HappyPack启动后,会在其运行的loader中注入emitFile等接口,而Thread-loader没有这个特性,因此对loader的要求会更高,兼容性也会更高。可怜的官方链接:https://github.com/webpack-contrib/thread-loader如何使用首先需要安装Thread-loader依赖:yarnadd-Dthread-loader其次需要先配置Thread-loader放置在loader数组中,保证最先运行,如:module.exports={module:{rules:[{test:/\.js$/,use:['thread-loader','babel-loader','eslint-loader'],},],},};配置完成后,再次启动npxwebpack命令。仍然以Three.js为例,Thread-loader开启前,构建时间约为11000ms~18000ms,开启后耗时减少至8000ms左右,提升约37%。原理Webpack将loader相关的逻辑抽象到loader-runner库中,Thread-loader也复用该库完成Loader的运行逻辑。核心步骤:启动时,通过pitch拦截Loader执行链,解析Webpack配置对象,获取线程-loader后面的Loader列表,调用child_process.spawn创建工作子进程,传递Loader列表等参数、文件路径和子进程的上下文,并在子进程中调用loader-runner。翻译文件翻译完成后,将结果返回给主进程。流程参考:https://github.com/webpack/loader-runnerWebpack原理系列之七:loader的写法缺点Thread-loader是Webpack官方推荐的并行处理组件。它的实现和使用非常简单,但也存在一些问题:Loader中不能调用emitAsset等接口,会导致style-loader等加载器无法正常工作。解决方法是在thread-loader之前放置这样的组件,比如['style-loader','thread-loader','css-loader']Loader无法获取编译、编译器等实例对象,也无法获取Webpack配置.这会导致部分Loader无法与Thread-loader一起使用,需要读者仔细筛选和测试。Parallel-WebpackThread-loader、HappyPack等组件提供的并行能力只作用于执行加载器——Loader的过程,对后续的AST解析、依赖收集、打包、代码优化等没有影响。理论上的好处还是比较有限的。对此,社区还提供了另一种并行度更高,在多个独立进程中运行Webpack实例的方案——Parallel-Webpack。官方链接:https://github.com/trivago/parallel-webpack如何使用基本用法使用前还需要安装依赖:yarnadd-Dparallel-webpackParallel-Webpack支持两种用法,第一种在webpack.config中.js配置文件中导出多个Webpack配置对象,如:module.exports=[{entry:'pageA.js',output:{path:'./dist',filename:'pageA.js'}},{entry:'pageB.js',output:{path:'./dist',filename:'pageB.js'}}];之后执行命令npxparallel-webpack完成构建,上面的示例配置会同时打包pageA.js和pageB.js两个产品。组合变量Parallel-Webpack还提供了createVariants函数,用于根据给定的变量组合生成多个Webpack配置对象,如:constcreateVariants=require('parallel-webpack').createVariantsconstwebpack=require('webpack')constbaseOptions={entry:'./index.js'}//配置变量组合//属性名是webpack配置属性;该属性值是一个可选变量//下面的变量组合最终会产生2*2*4=16种形式配置对象constvariants={minified:[true,false],debug:[true,false],target:['commonjs2','var','umd','amd']}functioncreateConfig(options){constplugins=[newwebpack.DefinePlugin({DEBUG:JSON.stringify(JSON.parse(options.debug))})]return{output:{路径:'./dist/',文件名:'MyLib.'+options.target+(options.minified?'.min':'')+(options.debug?'.debug':'')+'。js'},plugins:plugins}}module.exports=createVariants(baseOptions,variants,createConfig)上面的例子使用了createVariants函数,根据variants变量搭配16种不同的minified、debug、target组合,最终生成如下产品:[WEBPACK]Building16targetsinparallel[WEBPACK]StartedbuildingMyLib.umd.js[WEBPACK]StartedbuildingMyLib.umd.min.js[WEBPACK]StartedbuildingMyLib.umd.debug.js[WEBPACK]开始构建MyLib.umd.min.debug.js[WEBPACK]StartedbuildingMyLib.amd.js[WEBPACK]StartedbuildingMyLib.amd.min.js[WEBPACK]StartedbuildingMyLib.amd.debug.js[WEBPACK]StartedbuildingMyLib.amd.min.debug.js[WEBPACK]]]StartedbuildingMyLib.commonjs2.js[WEBPACK]StartedbuildingMyLib.commonjs2.min.js[WEBPACK]StartedbuildingMyLib.commonjs2.debug.js[WEBPACK]StartedbuildingMyLib.commonjs2.min.debug.js[WEBPACK]StartedbuildingMyLib.commonjs2.min.debug.js[WEBPACK]StartedbuildingMyLib.var.js[WEBPACK]StartedbuildingMyLib.var.min.js[WEBPACK]StartedbuildingMyLib.var.debug.js[WEBPACK]StartedbuildingMyLib.var.min.debug.js原理parallel-webpack的实现很简单,基本上就是Webpack上的一个shell,核心逻辑:根据传入的配置项个数,调用worker-farm创建多个worker进程。在worker进程中,调用Webpack执行构建。worker进程执行完毕后,调用node-ipc向这里的主进程发送结束信号,至此所有工作完成。缺点虽然parallel-webpack比Thread-loader和HappyPack的并行度更高,但是进程实例之间没有任何形式的通信,这可能会导致相同的工作在不同的进程——或者不同的CPU核心重复执行。例如,当同一段代码需要同时打包成压缩版和非压缩版时,在parallel-webpack方案下,会重复预资源加载、依赖解析、AST解析等操作,只有代码生成的最后阶段会有所不同。.该技术的实施对单入口项目没有任何好处,只会增加流程创建的成本;但特别适用于MPA等多入口场景,或者需要同时编译esm、umd、amd等多种产品形态的类库场景。并行压缩在Webpack的上下文中,Uglify-js、Uglify-es和Terser通常用于代码混淆和压缩。三者都在不同程度上原生实现了多进程并行压缩功能。TerserWebpackPlugin完整介绍:https://webpack.js.org/plugins/terser-webpack-plugin/以Terser为例,插件TerserWebpackPlugin默认开启了并行压缩。通常,保持默认配置,即parallel=true以获得最大的性能增益。开发者也可以通过parallel参数关闭或设置具体的并行进程数,例如:constTerserPlugin=require("terser-webpack-plugin");module.exports={optimization:{minimize:true,minimizer:[newTerserPlugin({parallel:2//number|boolean})],},};上述配置可以设置最大并行进程数为2。对于Webpack4及更早版本,代码压缩插件UglifyjsWebpackPlugin也有类似的功能和配置项,这里不再赘述。BestPractice理论上,并行确实可以提高系统的运行效率,但是在Node的单线程架构下,所谓的并行计算只能通过依赖和fork子进程来进行,而创建进程的行为本身就有很大的消耗——大约600ms,建议读者根据实际需要使用上面的多进程方案。对于小型项目,构建成本可能较低,但多进程技术的引入增加了整体成本。对于大型项目,由于HappyPack官方已经明确表示不维护,建议使用Thread-loader组件来提升Make阶段的性能。在生产环境中,还可以配合terser-webpack-plugin的并行压缩功能,提高整体效率。【小编推荐】HarmonyOS官方战略合作共建-HarmonyOS技术社区如何从Windows10免费升级到Windows11?Windows11正式启动!安装方法和ISO镜像下载都在这里爆!苹果正式关闭iOS14.8验证系统你升级了吗?Windows11正式版发布,体积缩小40%。你有没有更新Windows11?为什么我还没有收到推送?