扩展可让用户在VS Code中向开发工做流程添加新的语言、调试器和工具。VS Code提供了丰富的可扩展模块,容许扩展访问用户界面、提供扩展功能。
一般状况下VS Code会安装多个扩展,因此做为一名扩展开发者,咱们应该时刻关注扩展的性能,避免拖慢其它扩展,甚至是VS Code的主进程。javascript
下面是咱们在开发一款扩展时应该遵循的原则:java
import ... from ...
是比较经常使用的引用模块的方法,可是有时这并不必定是个好的方法。好比一个叫作request-promise的模块,加载起来会耗费很是多的时间(在我本身这边测试须要1至2秒),但可只能有在特定的状况下咱们才会须要请求远程的资源,好比本地的缓存过时了。上面提到的前三个原则不少开发者已经遵照了,在这篇文章中,咱们会讨论按一种需加载的方法。这种方法要符合咱们平时写TypeScript和JavaScript的习惯,同时也要尽量减小更改现有代码的工做量。node
通常来讲,咱们在脚本的最顶端使用import来加载模块,好比下面的代码:webpack
import * as os from 'os';
Node会同步加载指定的模块,同时阻塞后面的代码。web
咱们须要一个新的方法,好比叫作impor吧,用它能够引入模块,但并不立刻加载这个模块:typescript
const osModule = impor('os'); // osModule不可访问,由于os模块尚未被加载
为了达到这一目的,咱们须要使用Proxy对象。Proxy对象被用来自定义一些基本操做的行为。npm
咱们能够自定义get方法,只有当这个模块被调用时咱们才开始加载它。json
get: (_, key, reciver) => { if (!mod) { mod = require(id); } return Reflect.get(mod, key, reciver); }
使用Proxy对象后,osModule是一个Proxy实例,而且只有当咱们调用它的一个方法后,os模块才会被加载。promise
const osModule = impor('os'); // os模块尚未被加载 ... const platform = osModule.platform() // os模块从这里开始加载
当咱们只想使用模块的一部分时,普遍使用import {...} for ...
的写法。但是这让Node不得不访问这个模块来检查其属性值。这样getter就会被调用,模块也会在那个时候被加载。缓存
按需加载还不够,咱们能够进一步来优化用户体验。在扩展启动和用户运行命令来加载模块之间,咱们有充足的时间来提早加载模块。
很容易想到的一个办法,是建立一个后台任务来加载队列里的模块。
咱们开发了一个名叫Azure IoT Device Workbench的扩展,它能够结合多个Azure服务和流行的物联网开发板,简单地进行物联网项目的开发、编译、部署和调试。
因为Azure IoT Device Workbench涉及到的范围很是普遍,因此这个扩展启动起来很是繁重。同时它又须要监听USB事件,当物联网设备插入计算机后作出响应。
图一:Azure IoT Device Workbench使用懒加载和正常加载的启动时间
咱们对比了Azure IoT Device Workbench在多种状况下使用懒加载和正常加载的启动时间。图一中由上到下的图表分别是没有工做区、打开非物联网项目工做区和打开物联网项目工做区时启动。左侧的图表是冷启动,右侧是热启动。冷启动只发生在第一次安装扩展时,VS Code作一些缓存以后,都将是热启动。X轴表示时间,以毫秒为单位。Y轴是已加载的模块数量。
With normal load, the extension is activated at end of the chart. We find the extension is activated very advanced with lazy load with both cold boot and warm boot, especially when VS Code launches without workspace open.
对于没有工做区冷启动的状况,懒加载的启动速度大约有30倍的提高,热启动时有大约20倍的提高。打开非物联网项目工做区时,冷启动懒加载比正常加载快了10倍,热启动时快20倍。当VS Code打开物联网项目时,Azure IoT Device Workbench须要引用大量模块来加载项目,即便这样,咱们冷启动时也偶两倍的启动速度,热启动时有3倍的启动速度。
下面是懒加载的完整时间线:
图二:Azure IoT Device Workbench使用懒加载的完整时间线
和图一同样,图二中的图表也表示冷启动和热启动下没有工做区、打开非物联网项目工做区和打开物联网项目工做区。
在图中能够看到后台任务加载模块的加载时间阶梯很是清晰。用户很难注意到这个小动做,扩展启动得很是顺畅。
为了使这个提高性能的方法能够被全部VS Code扩展开发者使用,咱们发布了一个名叫impor
的Node模块,而且咱们已经将这个模块用于Azure IoT Device Workbench。你能够对代码进行不多的更改就将它应用到你的项目中。
几乎全部的VS Code扩展都有Node模块依赖。由于Node模块的工做方式,依赖的曾经可能会很是深。另外,模块的结果也可能很是复杂,也就是Node模块黑洞所说的事情。
为了清理Node模块,咱们使用一个很是棒的工具,webpack。
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序须要的每一个模块,而后将全部这些模块打包成一个或多个 bundle。
tree shaking 是一个术语,一般用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如import
和export
。这个术语和概念其实是兴起于 ES2015 模块打包工具 rollup。
使用webpack进行tree shaking很是简单。咱们须要指定一个入口文件和输出文件名就能够,剩下的事情webpack会处理好。
使用tree shaking后,没有被引用的文件,包括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_modeles文件夹里。
若是哪一个模块不兼容webpack打包,就直接将它复制到输出目录的node_modules文件夹里。
这是一个扩展项目结构的例子:
|- src | |- extension.ts | |- out | |- node_modules | | |- happy-broccoli.js | | |- incompatible-with-bundle-module | | |- package.json | | | |- extension.js | |- node_modules | |- happy-broccoli | |- package.json | | |- incompatible-with-bundle-module | |- package.json | |- package.json |- webpack.config.js |- tsconfig.json
未打包Node模块时Azure IoT Device Workbench包含了4368个文件,打包后只剩下了343个文件。
'use strict'; const cp = require('child_process'); const fs = require('fs-plus'); const path = require('path'); function getEntry() { const entry = {}; const npmListRes = cp.execSync('npm list -only prod -json', { encoding: 'utf8' }); const mod = JSON.parse(npmListRes); const unbundledModule = ['impor']; for (const mod of unbundledModule) { const p = 'node_modules/' + mod; fs.copySync(p, 'out/node_modules/' + mod); } const list = getDependeciesFromNpm(mod); const moduleList = list.filter((value, index, self) => { return self.indexOf(value) === index && unbundledModule.indexOf(value) === -1 && !/^@types\//.test(value); }); for (const mod of moduleList) { entry[mod] = './node_modules/' + mod; } return entry; } function getDependeciesFromNpm(mod) { let list = []; const deps = mod.dependencies; if (!deps) { return list; } for (const m of Object.keys(deps)) { list.push(m); list = list.concat(getDependeciesFromNpm(deps[m])); } return list; } /**@type {import('webpack').Configuration}*/ const config = { target: 'node', entry: getEntry(), output: { path: path.resolve(__dirname, 'out/node_modules'), filename: '[name].js', libraryTarget: "commonjs2", devtoolModuleFilenameTemplate: "../[resource-path]", }, resolve: { extensions: ['.js'] } } module.exports = config;
不将整个扩展打包,而是对每一个模块分别打包会带来很大的好处。使用webpack打包后,扩展极有可能会抛出数十个错误。把每一个模块分离开使调试变得很是容易。同时,按需加载指定的模块也能尽量地下降对性能的影响。
模块打包应用在使用懒加载的Azure IoT Device Workbench上,来同正常加载进行对比。
图三:Azure IoT Device Workbench懒加载打包模块的启动时间和正常加载对比
模块打包大幅减小了启动时间。对于冷启动,在一块儿状况下懒加载甚至加载完全部模块所消耗的所有时间都比正常加载所需的时间少。
正常 | Webpack典型的解决方案* | 懒加载 | 懒加载打包的模块** | |
---|---|---|---|---|
没有工做区,冷启动 | 19474 ms | 1116 ms | 599 ms | 196 ms |
没有工做区,热启动 | 2713 ms | 504 ms | 118 ms | 38 ms |
非物联网项目工做区,冷启动 | 11188 ms | 1050 ms | 858 ms | 218 ms |
非物联网项目工做区,热启动 | 4825 ms | 530 ms | 272 ms | 102 ms |
物联网项目工做区,冷启动 | 15625 ms | 1178 ms | 7629 ms | 2001 ms |
物联网项目工做区,热启动 | 5186 ms | 588 ms | 1513 ms | 517 ms |
*,** Azure IoT Device Workbench须要的一些模块与webpack不兼容,没有被打包。
表一:Azure IoT Device Workbench在不一样状况下的启动时间
表一中所示的启动时间是指扩展入口最开始到activate
函数结束之间的时间:
// 开始启动 import * as vscode from 'vscode'; ... export async function activate(context: vscode.ExtensionContext) { ... // 启动完成 } ...
一般启动以前的时间要比VS Code正在运行的扩展页面中显示的启动时间长。好比以热启动打开物联网项目工做区的启动时间在表中是517毫秒,可是VS Code正在运行的扩展页面中大约是200毫秒。
典型的webpack解决方案中,启动时间只有启动模式有关,由于全部模块都老是以一样的方式被加载。当在Azure IoT Device Workbench中应用懒加载时,不管是否使用打包模块,没有工做区时启动速度都远快于打开物联网工做区。当咱们打开物联网项目工做区时,大部分模块都被引用,懒加载带来的优点不是很明显,因此懒加载打包模块和典型webpack解决方案有相近的启动时间。
在这篇文章中,提出了一种按需加载打包模块的方法。一款叫作Azure IoT Device Workbench的繁重扩展被用来在多种状况下测试这个方法。而且它的启动速度被提高了数十倍。在某些状况下,这个方法比典型的webpack方案带来了更优异的性能提高。