擴展可讓用戶在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方案帶來了更優異的性能提高。