当前位置: 首页 > 后端技术 > Node.js

如何提高VSCode扩展的启动速度——不仅仅是Webpack

时间:2023-04-03 19:41:22 Node.js

概述扩展允许用户向VSCode中的开发工作流添加新的语言、调试器和工具。VSCode提供了丰富的可扩展模块,允许扩展访问用户界面并提供扩展功能。通常VSCode会安装多个扩展,所以作为扩展开发者,我们应该时刻关注扩展的性能,以免拖慢其他扩展,甚至拖慢VSCode的主进程。以下是开发扩展时应遵循的原则:避免使用同步方法。同步方法会阻塞整个Node进程,直到它返回结果。因此,您应该尽可能使用异步方法。如果您发现很难用异步方法替换同步方法,那么您应该考虑重构您的代码。只包括你需要的模块。有些依赖模块很大,比如lodash。通常我们不需要lodash的所有方法,所以引用整个lodash模块是没有意义的。lodash的每个方法都有自己的模块,你应该只引用你需要的部分。小心启动条件。在大多数情况下,您的扩展不需要启动。不要使用“*”作为开始条件。如果你的扩展确实需要一直监听一些事件,可以考虑将主要代码放在setTimeout中以低优先级运行。按需加载模块。import...from...是一种比较常见的引用模块的方式,但有时这并不一定是一种好方式。比如一个叫request-promise的模块,加载起来会很费时间(我这边测试需要1到2秒),但我们只需要在特定情况下请求远程资源。例如,本地缓存已过期。上面提到的前三个原则已经被许多开发人员遵循。在本文中,我们将讨论按需加载的方法。这种方式应该符合我们平时写TypeScript和JavaScript的习惯,同时尽可能减少改动现有代码的工作量。按需加载模块符合习惯一般来说,我们在脚本的最前面使用import来加载模块,比如下面的代码:import*asosfrom'os';Node会同步加载指定的模块,同时阻塞下面的代码。我们需要一个新方法,例如impor,它可用于导入模块,但不会立即加载模块:constosModule=impor('os');//osModule不可访问,因为尚未加载os模块要实现这一点,我们需要使用Proxy对象。代理对象用于自定义一些基本操作的行为。我们可以自定义get方法,只有当这个模块被调用时我们才开始加载它。get:(_,key,reciver)=>{if(!mod){mod=require(id);}returnReflect.get(mod,key,reciver);}使用Proxy对象后,osModule就是一个Proxy实例,只有当我们调用其中一个方法时,os模块才会被加载。constosModule=impor('os');//os模块还没有被加载...constplatform=osModule.platform()//os模块从这里加载当我们只想使用模块的一部分时广泛使用import{...}....但这迫使Node必须访问模块以检查其属性值。这样getter将被调用,模块将在那时加载。使用后台任务按需加载模块还不够,我们可以进一步优化用户体验。在扩展启动和用户运行命令加载模块之间,我们有足够的时间来预加载模块。一种简单的思考方式是创建一个后台任务来加载队列中的模块。时间线我们开发了一个名为AzureIoTDeviceWorkbench的扩展,它可以结合多个Azure服务和流行的物联网开发板,轻松开发、编译、部署和调试物联网项目。由于AzureIoTDeviceWorkbench的范围广泛,此扩展启动起来非常繁重。同时,它需要监听USB事件并在物联网设备插入计算机时做出响应。图1:使用延迟加载和正常加载的AzureIoTDeviceWorkbench的启动时间我们比较了在各种情况下使用延迟加载和正常加载的AzureIoTDeviceWorkbench的启动时间。图1中从上到下的图表分别在没有工作空间时、打开非物联网项目工作空间时、打开物联网项目工作空间时启动。左图是冷启动,右图是热启动。冷启动仅在第一次安装扩展时发生,在VSCode进行一些缓存后,它将是一个热启动。x轴表示以毫秒为单位的时间。Y轴是加载的模块数。在正常负载下,扩展在图表的末尾被激活。我们发现该扩展的激活非常先进,具有冷启动和热启动的延迟加载功能,尤其是当VSCode在没有打开工作区的情况下启动时,懒加载的启动速度快30倍左右,热启动快20倍左右。打开非IoT项目工作区时,冷启动懒加载比正常加载快10倍,热启动时快20倍。VSCode打开物联网项目时,AzureIoTDeviceWorkbench需要引用大量模块来加载项目。即便如此,我们甚至冷启动时的启动速度是原来的2倍,热启动时的启动速度是3倍。以下是延迟加载的完整时间线:图2:AzureIoTDeviceWorkbench使用延迟加载的完整时间线与图1相同。图2中的图表也显示了冷启动和热启动下没有工作空间,而非物联网则打开ProjectWorkspace和OpenIoTProjectWorkspace。图中可以看出后台任务加载模块的加载时间阶梯非常清晰。用户几乎不会注意到这个小手势,扩展程序启动非常顺利。为了让所有VSCode扩展开发人员都能使用这种性能提升,我们发布了一个名为impor的Node模块,并且我们已经使这个模块可用于AzureIoTDeviceWorkbench。您只需更改很少的代码即可将其应用到您的项目中。模块打包几乎所有VSCode扩展都具有Node模块依赖性。由于Node模块的工作方式,依赖关系可能非常深。另外,模块的结果也可以很复杂,这就是Node模块黑洞所说的。为了清理Node模块,我们使用了一个很棒的工具webpack。webpack是现代JavaScript应用程序的静态模块打包器。当webpack处理应用程序时,它会递归地构建一个包含应用程序所需的每个模块的依赖关系图,然后将所有这些模块打包到一个或多个包中。TreeshakingTreeshaking是一个常用于描述在JavaScript上下文中删除死代码的术语。它依赖于ES2015模块系统中的静态结构特性,例如导入和导出。这个术语和概念实际上起源于ES2015模块捆绑工具汇总。使用webpack摇树非常容易。我们需要指定一个入口文件和一个输出文件名,webpack会处理剩下的事情。使用treeshaking后,未引用的文件,包括JavaScript代码、markdown文件等将被删除。然后webpack会将所有文件组合成一个包文件。代码分离将所有代码合并到一个文件中并不是一个好主意。为了使用按需加载,我们需要将代码拆分成多个部分,只加载我们需要的部分。现在,需要一种方法来分离代码是我们需要解决的问题。一种可能的解决方案是将每个Node模块分离到一个文件中。但是手动将各个Node模块的路径写到webpack的配置文件中是不可接受的。幸运的是,我们可以使用npm-ls在生产模式下获取所有Node模块。所以在webpack配置文件的输出部分,我们使用[name].js作为输出来编译各个模块。应用程序打包模块当我们想要加载一个模块时,比如happy-broccoli,Node将首先尝试在node_modules文件夹中找到happy-broccoli.js。如果该文件不存在,Node会在happy-broccoli文件夹中查找index.js文件。如果仍然找不到,它会在package.json中寻找main。为了应用打包好的模块,我们可以将它们放入tsc输出目录下的node_models文件夹中。如果有模块不兼容webpack打包,直接复制到输出目录的node_modules文件夹下。这是扩展项目结构的示例:|-src||-extension.ts||-输出||-节点模块|||-happy-broccoli.js|||-与捆绑模块不兼容|||-package.json||||-extension.js||-node_modules||-快乐西兰花||-package.json|||-与捆绑模块不兼容||-package.json||-package.json|-webpack.config.js|-tsconfig.jsonNode模块未打包时,AzureIoTDeviceWorkbench包含4368个文件,打包后只剩下343个文件。Webpack配置实例'usestrict';constcp=require('child_process');constfs=require('fs-plus');constpath=require('path');functiongetEntry(){constentry={};constnpmListRes=cp.execSync('npmlist-onlyprod-json',{encoding:'utf8'});constmod=JSON.parse(npmListRes);constunbundledModule=['impor'];for(constmodofunbundledModule){constp='node_modules/'+mod;fs.copySync(p,'out/node_modules/'+mod);}constlist=getDependeciesFromNpm(mod);constmoduleList=list.filter((value,index,self)=>{returnself.indexOf(value)===index&&unbundledModule.indexOf(value)===-1&&!/^@types\//.测试(值);});for(constmodofmoduleList){entry[mod]='./node_modules/'+mod;}returnentry;}functiongetDependeciesFromNpm(mod){letlist=[];constdeps=mod.dependencies;if(!deps){返回列表;}for(constmofObject.keys(deps)){list.push(m);list=list.concat(getDependeciesFromNpm(deps[m]));}returnlist;}/**@type{import('webpack').Configuration}*/constconfig={target:'node',entry:getEntry(),output:{path:path.resolve(__dirname,'out/node_modules'),文件名:'[name].js',libraryTarget:"commonjs2",devtoolModuleFilenameTemplate:"../[resource-path]",},resolve:{extensions:['.js']}}module.exports=配置;与典型的webpack解决方案相比,不打包整个扩展会带来很大的好处,而是将每个模块单独打包使用webpackpack后,扩展很可能会抛出几十个错误。将每个模块分开使得调试非常容易。同时,按需加载指定模块也可以将对性能的影响降到最低。实验结果将模块打包应用到AzureIoTDeviceWorkbench上,使用延迟加载与正常加载进行对比。图3:AzureIoTDeviceWorkbench懒加载打包模块启动时间与正常加载对比模块打包大大减少了启动时间。对于冷启动,延迟加载甚至加载所有模块在所有情况下都比正常加载花费更少的时间。普通webpack典型解决方案*lazyload懒加载打包模块**无工作空间,冷启动19474ms1116ms599ms196ms无工作空间,热启动2713ms504ms118ms38ms非IoT项目工作空间,冷启动11188ms1050ms858ms218ms非IoT项目工作区,热启动4825ms530ms272ms102msIoT项目工作台,冷启动15625ms1178ms7629ms2001msIoT项目工作区,热启动5186ms1513ms517ms*,**AzureIoTDeviceWorkbench所需的某些模块与webpack不兼容且未捆绑.表一:AzureIoTDeviceWorkbench在不同情况下的启动时间表一所示的启动时间是指扩展入口开始到activate函数结束的时间://startimport*asvscodefrom'vscode';...exportasyncfunctionactivate(context:vscode.ExtensionContext){...//startupcomplete}...通常启动前的时间比VSCodeRunningExtensions页面中显示的启动时间长。例如,以热启动方式打开IoT项目工作区的启动时间在表中为517毫秒,但在运行VSCode的扩展页面中约为200毫秒。在典型的webpack解决方案中,启动时间只与启动模式有关,因为所有模块总是以相同的方式加载。在AzureIoTDeviceWorkbench中应用延迟加载时,无论有无打包模块,工作区的启动速度都不会比打开IoT工作区快得多。当我们打开IoT项目工作空间时,大部分模块都是被引用的,懒加载带来的优势并不明显,所以懒加载打包模块的启动时间和典型的webpack方案差不多。结束语本文提出了一种按需加载打包模块的方法。名为AzureIoTDeviceWorkbench的重量级扩展用于在各种场景中测试此方法。并且其启动速度提升了数十倍。在某些情况下,这种方法比典型的webpack解决方案带来了更高的性能提升。