资讯专栏INFORMATION COLUMN

如何提升VS Code扩展的启动速度——不只是Webpack

xiaolinbang / 878人阅读

摘要:同时它又需要监听事件,当物联网设备插入计算机后做出响应。比如以热启动打开物联网项目工作区的启动时间在表中是毫秒,但是正在运行的扩展页面中大约是毫秒。当在中应用懒加载时,无论是否使用打包模块,没有工作区时启动速度都远快于打开物联网工作区。

概述
扩展可以让用户在VS Code中向开发工作流程添加新的语言、调试器和工具。VS Code提供了丰富的可扩展模块,允许扩展访问用户界面、提供扩展功能。

通常情况下VS Code会安装多个扩展,所以作为一名扩展开发者,我们应该时刻关注扩展的性能,避免拖慢其它扩展,甚至是VS Code的主进程。

下面是我们在开发一款扩展时应该遵循的原则:

避免使用同步方法。同步方法会阻塞整个Node的进程,直到其返回结果。所以,你应该尽可能地使用异步方法。如果发现很难用异步方法替换同步方法,那么你应该考虑一下重构代码。

只引用你需要的模块。有一些依赖模块非常巨大,比如说lodash。通常我们不需要lodash的全部方法,所以引用整个lodash模块并不合理。lodash的每个方法都有独立的模块,你应该只引用你需要的部分。

谨慎对待启动条件。大多数情况下,你的扩展并不需要启动。不用使用“*”作为启动条件。如果你的扩展确实需要一直监听一些事件,考虑将主要的代码放在setTimeout里以低优先级运行。

按需加载模块。import ... from ...是比较常用的引用模块的方法,但是有时这并不一定是个好的方法。比如一个叫做request-promise的模块,加载起来会耗费非常多的时间(在我自己这边测试需要1至2秒),但可只能有在特定的情况下我们才会需要请求远程的资源,比如本地的缓存过期了。

上面提到的前三个原则很多开发者已经遵守了,在这篇文章中,我们会讨论按一种需加载的方法。这种方法要符合我们平时写TypeScript和JavaScript的习惯,同时也要尽可能减少更改现有代码的工作量。

按需加载模块 符合习惯

一般来说,我们在脚本的最顶端使用import来加载模块,比如下面的代码:

import * as os from "os";

Node会同步加载指定的模块,同时阻塞后面的代码。

我们需要一个新的方法,比如叫做impor吧,用它可以引入模块,但并不马上加载这个模块:

const osModule = impor("os"); // osModule不可访问,因为os模块还没有被加载

为了达到这一目的,我们需要使用Proxy对象。Proxy对象被用来自定义一些基本操作的行为。

我们可以自定义get方法,只有当这个模块被调用时我们才开始加载它。

get: (_, key, reciver) => {
    if (!mod) {
        mod = require(id);
    }
    return Reflect.get(mod, key, reciver);
}

使用Proxy对象后,osModule是一个Proxy实例,并且只有当我们调用它的一个方法后,os模块才会被加载。

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
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 importexport。这个术语和概念实际上是兴起于 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个文件。

Webpack配置实例
"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解决方案对比

不将整个扩展打包,而是对每个模块分别打包会带来很大的好处。使用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方案带来了更优异的性能提升。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109512.html

相关文章

  • 如何打造一个令人愉悦前端开发环境(一)

    摘要:我觉得这方面的原因是当时对和的依赖,导致大家对的兴趣不弄,错过了最佳时机,这个其实跟百度自己的的技术栈有很大关系。这个阮一峰对于前端构建的变化吐槽过,说新的构建工具就是的构建工具。 文章来源 最近几年,前端发展越来越迅速,各种萌新加入了前端这个大家庭,大有赶IOS、超Android的趋势呀!同时,萌新们提出了各种前端工作问题,除了最基础的html、css、js三板斧之外,最让人头疼的应...

    KavenFan 评论0 收藏0
  • 如何打造一个令人愉悦前端开发环境(一)

    摘要:我觉得这方面的原因是当时对和的依赖,导致大家对的兴趣不弄,错过了最佳时机,这个其实跟百度自己的的技术栈有很大关系。这个阮一峰对于前端构建的变化吐槽过,说新的构建工具就是的构建工具。 文章来源 最近几年,前端发展越来越迅速,各种萌新加入了前端这个大家庭,大有赶IOS、超Android的趋势呀!同时,萌新们提出了各种前端工作问题,除了最基础的html、css、js三板斧之外,最让人头疼的应...

    Yangyang 评论0 收藏0
  • Visual Studio Code 断点调试 Vue

    摘要:个人还是更加习惯于断点调试。这篇文章将介绍如何配置和来完成直接在断点调试代码并且在的调试窗口看到中相同的值。现在就可以在文件的代码中打断点进行调试了。 很多人习惯在 Chrome 的调试窗口中调试 Vue 代码, 或者直接使用 console.log 来观察变量值, 这是非常痛苦的一件事,需要同时打开至少 3 个窗体。个人还是更加习惯于断点调试。这篇文章将介绍如何配置 Visual S...

    qpwoeiru96 评论0 收藏0
  • VS Code、ATOM这些开源文本编辑器代码实现中有哪些奇技淫巧?

    摘要:知乎上也有相关的讨论,开发的下一代编辑器莫非已经定义为上一代编辑器了吗。 这篇是我在知乎的回答,原文在这里:justjavac: VS Code、ATOM这些开源文本编辑器的代码实现中有哪些奇技淫巧? 研究 V8 比较多,也关注了一下 vscode 和 atom 的性能,每次 vscode、atom 的 change log 我都会看一遍。印象最深的是 vscode 1.14 的一次更...

    adie 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<