Cr4zy Uru5 blogs
使用 asar 提升 VSCode 插件的性能

最近在开发一款 VSCode 插件过程中遇到了一个性能瓶颈,插件的依赖项过于庞大导致插件体积很大,插件安装和启动耗时也比较长,影响了用户的使用体验。为了解决这个问题,本文提出了一种使用 asarnode_modules 进行打包的方法,经测试插件体积减少了 46%,安装时间减少了 75% 以上,启动时间减少了 22%,插件的性能得到了有效提升。

该方法具备一定的通用性,尤其是对插件依赖项过多时有较为明显的提升。如果你是 VSCode 插件开发者并且面临着类似的问题,欢迎尝试下此方案。

问题分析

性能的主要瓶颈在于插件中的 node_modules 非常庞大,有 117,317 个文件,体积达到了 185.5 MB,而且这些依赖需要在插件运行时使用,vsce(VSCode 插件构建工具)需要把 node_modules 也打包到产物内,导致插件产物(vsix 文件)的体积也很大,且文件零碎,用户安装插件时速度很慢。

以下是构建时 vsce 给出的信息,可以看到 vsce 也发现了这个问题并且给出了一个解决方案:Bundling Extensions

优化前的版本

VSCode 提出的优化方案主要有两个方面:

  1. 添加 .vscodeignore 构建时忽略插件运行时不需要的文件
  2. 对插件使用 rollup 等构建工具在 vsce 前进行打包,消灭 node_modules

很遗憾,这个方案无法有效优化这个插件,因为一方面这些依赖需要在运行时使用因此不能忽略,另一方面插件中的依赖大量使用了动态 require,因此 rollup 等构建工具无法完整的将所有依赖打包。

如果你的插件依赖没有遇到动态 require 的问题,推荐直接使用构建工具的方案而不是 asar

因此,顺着第二个思路,我们需要找到一种方法将插件的依赖完整打包。

解决方案

在一次调试 VSCode 的过程中,发现了 VSCode 的 Resources/app 目录下有一个 node_modules.asar 的文件,顿时受到了启发。开发过 Electron 的朋友应该都接触过这个文件类型,而 VSCode 也是基于 Electron,VSCode 选择将自身的依赖构建成 asar 必然有性能方面的考量。那么我们是否也可以利用 asar 对插件的依赖进行处理呢?

我们先看下 Electron 对 asar 的介绍(https://github.com/electron/asar):

asar 是一种简单的扩展存档格式,它和 tar 类似,无需压缩即可将所有文件连接在一起,同时具备随机访问支持

asar 的优点:

  • 支持随机访问
  • 使用 JSON 存储文件信息
  • 容易解析

由此来看,asar 很匹配我们的需求。接下来就具体介绍如何在插件中使用。

构建 asar 文件

asar 的构建非常简单,首先安装:

$ npm install asar -D

然后在 package.json 添加一个运行脚本:

{
  "build:asar": "asar pack ./node_modules ./dist/node_modules.asar"
}

运行 npm run build:asar 即可得到 asar 文件

忽略 node_modules

得到 node_modules.asar 后,插件就不再需要 node_modules 目录,因此需要在 .vscodeignore 文件中添加一下:

node_modules

让 VSCode 支持加载 asar

由于 VSCode 是基于 Electron 的,因此天然能加载 asar 中的文件。例如插件中有一个依赖项 miniprogram-ci,我们通过将路径改为以下方式即可在插件中加载 asar 文件中的依赖项:

// 将 require('miniprogram-ci') 替换为:
require('./node_modules.asar/miniprogram-ci')

然而我们很难将 node_modules 中所有依赖项中的 require 也都改写成这种形式,这会导致依赖项的依赖无法加载。为了解决这个问题,我研究了下 VSCode 的源代码,发现 VSCode 是采用改写 Node 模块加载的行为实现的。

让 require 支持查找 asar

首先我们回顾下 Node 是如何加载依赖的。当 require('miniprogram-ci') 时,Node 会先生成所有可能的查找路径(paths),例如这个项目的插件入口位于 dist/extension/index.js,引用该依赖时 Node 会形成以下查找路径并按顺序尝试,直到成功

/Users/[用户名]/.vscode/extensions/[插件名]/dist/extension/node_modules
/Users/[用户名]/.vscode/extensions/[插件名]/dist/node_modules
/Users/[用户名]/.vscode/extensions/[插件名]/node_modules(成功)
/Users/[用户名]/.vscode/extensions/node_modules
…

因此默认情况下 Node 是不会默认查找 node_modules.asar 的,我们可以通过修改 Node 的这一查找行为将 asar 的路径也塞到 paths

参考 VSCode 源码中的实现(bootstrap-node.js),通过改写 Module._resolveLookupPaths 即可达到目的。

在插件入口文件最上方增加以下代码:

const Module = require('module');
const path = require('path');
const asarPath = path.join(__dirname, '..', 'node_modules.asar');
const nodeModulesPath = path.join(__dirname, '..', '..', 'node_modules');
const originalResolveLookupPaths = Module._resolveLookupPaths;

Module._resolveLookupPaths = function (moduleName, parent) {
  const paths = originalResolveLookupPaths(moduleName, parent);

  if (Array.isArray(paths)) {
    for (let i = 0, len = paths.length; i < len; i++) {
      if (paths[i] === nodeModulesPath) {
        paths.splice(i, 0, asarPath);
        break;
      }
    }
  }

  return paths;
};

即当 require 某个模块时,搜索其查找路径 paths 是否存在插件根目录下的 node_modules 路径,如有则将 asar 文件路径插入到 paths 中。搜索的目的是为了确认这个模块是否是 node_modules 下的,排除 Node 内置模块和其它情况的引用。

优化效果

至此,插件就可以摆脱 node_modules,性能得到很大的提升。我们从三个方面对比下前后的性能变化:

插件体积

优化前:67.7 MB,110,223 个文件,构建用时 3m 5s

优化后:36.4 MB,764 个文件,构建用时 2m 56s

插件体积减少了 46%,构建用时持平,并没有因为增加了 asar 构建环节而显著变慢

优化后的版本

优化前后的 vsix 产物可以在这里看到,其中版本 1.4.7 为优化前,1.4.8 为优化后: https://github.com/crazyurus/miniprogram-vscode-extension/releases

构建用时的变化可以在 GitHub Actions 中看到:https://github.com/crazyurus/miniprogram-vscode-extension/actions

插件安装用时

安装时间 VSCode 没有直接给出,大致测量了下:

优化前:1m 以上

优化后:15s 左右

插件安装用时也有大幅提升,且用时的减少可以有效减缓用户网络导致安装失败的影响,有效提升了安装成功率

插件加载用时

VSCode 每次加载插件都会统计用时,可以在插件列表或详情页看到:

插件加载用时

优化前:95ms

优化后:74ms

插件加载用时也有一定提升,主要避免了大量零碎文件的加载导致的 I/O 消耗

进一步优化

node_modules 中的所有依赖并不是插件运行时都需要的,我们可以将 devDependencies 排除在外不打包到 asar 中,进一步减少体积。

  1. 可以通过 npm install --production 实现仅安装 dependencies 中的依赖,此时 node_modules 中的依赖只与运行时相关。得到 asar 产物后再安装全部依赖完成插件的其它构建步骤,感兴趣的朋友可以尝试下。

  2. 也可以通过增加参数 asar --unpack-dir 将部分体积大的依赖排除,例如:

$ asar pack ./node_modules ./dist/node_modules.asar --unpack-dir &quot;{@types,ts-node,typescript}&quot;

排除掉插件开发和构建环节有关 TypeScript 的依赖,asar 会将这些依赖复制到 node_modules.asar.unpacked 文件夹下,我们只需删除或忽略这个文件夹即可。

最后

向大家推广一下这款 VSCode 插件 微信小程序开发工具,支持 WXML 等小程序特有语法的高亮和代码提示,以及小程序的预览、上传、体积分析等,欢迎开发小程序的朋友体验以及反馈意见。

另外这个插件是开源的,对 VSCode 插件开发感兴趣的朋友也可以了解下:https://github.com/crazyurus/miniprogram-vscode-extension