npm 镜像源工具速度对比
动机
我在日常的项目管理需要频繁地切换 npm
的镜像源。
而 nrm 的 bug
很多,速度很慢,并且已经不维护了;新一点的 nnrm 和 mini-nrm 也基本都需要 2s 以上的切换时间。
这在需要频繁的切换镜像源的场景下,体验非常糟糕。
所以有了 dnrm,一个 deno 实现的 nrm,每次切换源都在 100ms 内,速度超级快,开发体验拉满。
使用
安装
1. 模块安装
deno install --allow-read --allow-write --allow-env --allow-net -rfn dnrm https://deno.land/x/dnrm/mod.tsdeno install --allow-read --allow-write --allow-env --allow-net -rfn dnrm https://deno.land/x/dnrm/mod.tsdeno install --allow-read --allow-write --allow-env --allow-net -rfn dnrm https://deno.land/x/dnrm/mod.ts
npx deno-npx install --allow-read --allow-write --allow-env --allow-net -rfn dnrm https://deno.land/x/dnrm/mod.tsnpx deno-npx install --allow-read --allow-write --allow-env --allow-net -rfn dnrm https://deno.land/x/dnrm/mod.tsnpx deno-npx install --allow-read --allow-write --allow-env --allow-net -rfn dnrm https://deno.land/x/dnrm/mod.ts
在一些不想装 deno 的临时场景下 ?
# 注意: 这种使用方式仍然很慢 (切换时间在 200ms 左右),因为加载 deno 垫片需要时间,不过仍然比其他的包快很多npm i deno-nrm -g# 注意: 这种使用方式仍然很慢 (切换时间在 200ms 左右),因为加载 deno 垫片需要时间,不过仍然比其他的包快很多 npm i deno-nrm -g# 注意: 这种使用方式仍然很慢 (切换时间在 200ms 左右),因为加载 deno 垫片需要时间,不过仍然比其他的包快很多 npm i deno-nrm -g
2. 本地安装
-
下载该项目到本地
-
在项目根目录下执行命令
deno task installdeno task installdeno task install
cli
# 查看当前源dnrm# 切换 taobao 源dnrm use taobao# 查看所有源dnrm ls# 测试所有源dnrm test# 设置源在本地dnrm use taobao --local# 查看帮助dnrm -h# 查看版本号dnrm -V# 查看当前源 dnrm # 切换 taobao 源 dnrm use taobao # 查看所有源 dnrm ls # 测试所有源 dnrm test # 设置源在本地 dnrm use taobao --local # 查看帮助 dnrm -h # 查看版本号 dnrm -V# 查看当前源 dnrm # 切换 taobao 源 dnrm use taobao # 查看所有源 dnrm ls # 测试所有源 dnrm test # 设置源在本地 dnrm use taobao --local # 查看帮助 dnrm -h # 查看版本号 dnrm -V
优化原理
接下来进入正文,具体说说里边用到的优化原理。
deno
首先我们用了 deno,在绝大多数情况下,deno 的冷启动比 nodejs 要快。
例如简单执行 ?
console.log("hello, world")console.log("hello, world")console.log("hello, world")
runtime | ms |
---|---|
deno | 37 |
nodejs | 48 |
另外是 deno 允许我们引入依赖的具体某个模块,而不需要引入具体的依赖再引入模块,减少了解析脚本的时间。
例如 ?
import { exists } from "https://deno.land/std@0.192.0/fs/exists.ts";// 最终解析脚本只会解析 exists.ts 及其依赖的脚本,而不会解析一整个 fs 包import { exists } from "https://deno.land/std@0.192.0/fs/exists.ts"; // 最终解析脚本只会解析 exists.ts 及其依赖的脚本,而不会解析一整个 fs 包import { exists } from "https://deno.land/std@0.192.0/fs/exists.ts"; // 最终解析脚本只会解析 exists.ts 及其依赖的脚本,而不会解析一整个 fs 包
正则匹配
npm
的配置文件 .npmrc
其实是个类 .env
文件,这意味着你可以用类似 dotenv 的解析器来解析配置,但这带来了序列化和反序列化的时间消耗。
而在 dnrm 中直接进行正则匹配来读取和写入配置,同时不需要引入任何依赖,速度超级快。
热路径查询
在 go 语言中,因为结构体第一个字段的地址跟结构体的指针是相同的,所以第一个字段的访问速度比其他字段要快,我们称它为 hot path
例如 ?
type Foo {bar uint32 // 这个字段的访问速度速度更快jack uint32}type Foo { bar uint32 // 这个字段的访问速度速度更快 jack uint32 }type Foo { bar uint32 // 这个字段的访问速度速度更快 jack uint32 }
而在 js
当中是没有这种规则的,但我们仍然可以先预设一个常用的对象字段来处理。
// 常规热路径export const hotUrlRegistrys: Record<string,string> = {"https://registry.npmjs.org/": "npm","https://registry.npmmirror.com/": "taobao",};// 镜像源export const registrys: Registrys = {npm: "https://registry.npmjs.org/",yarn: "https://registry.yarnpkg.com/",github: "https://npm.pkg.github.com/",taobao: "https://registry.npmmirror.com/",npmMirror: "https://skimdb.npmjs.com/registry/",tencent: "https://mirrors.cloud.tencent.com/npm/",};// 镜像的 keyexport const registryKeys = Object.keys(registrys);// 获取镜像源export function getConfigRegistry(configText: string) {const [url = ""] = registryReg.exec(configText) || [];// 热路径先处理,多数情况下可以跳过循环return hotUrlRegistrys[url] ??registryKeys.find((k) => registrys[k] === url);}// 常规热路径 export const hotUrlRegistrys: Record< string, string > = { "https://registry.npmjs.org/": "npm", "https://registry.npmmirror.com/": "taobao", }; // 镜像源 export const registrys: Registrys = { npm: "https://registry.npmjs.org/", yarn: "https://registry.yarnpkg.com/", github: "https://npm.pkg.github.com/", taobao: "https://registry.npmmirror.com/", npmMirror: "https://skimdb.npmjs.com/registry/", tencent: "https://mirrors.cloud.tencent.com/npm/", }; // 镜像的 key export const registryKeys = Object.keys(registrys); // 获取镜像源 export function getConfigRegistry(configText: string) { const [url = ""] = registryReg.exec(configText) || []; // 热路径先处理,多数情况下可以跳过循环 return hotUrlRegistrys[url] ?? registryKeys.find((k) => registrys[k] === url); }// 常规热路径 export const hotUrlRegistrys: Record< string, string > = { "https://registry.npmjs.org/": "npm", "https://registry.npmmirror.com/": "taobao", }; // 镜像源 export const registrys: Registrys = { npm: "https://registry.npmjs.org/", yarn: "https://registry.yarnpkg.com/", github: "https://npm.pkg.github.com/", taobao: "https://registry.npmmirror.com/", npmMirror: "https://skimdb.npmjs.com/registry/", tencent: "https://mirrors.cloud.tencent.com/npm/", }; // 镜像的 key export const registryKeys = Object.keys(registrys); // 获取镜像源 export function getConfigRegistry(configText: string) { const [url = ""] = registryReg.exec(configText) || []; // 热路径先处理,多数情况下可以跳过循环 return hotUrlRegistrys[url] ?? registryKeys.find((k) => registrys[k] === url); }
直接配置替换
多数的 npm
镜像源切换工具喜欢调用子进程来执行 npm config set registry=...
,这会跑超级多的 npm
内部分支,也是卡的主要原因。
dnrm 直接读写目标配置文件,省去了这部分开销。
按需处理
配置文件
多数情况下,我们只是想简单地看看目前是什么镜像源,这时不需要创建文件了,可以直接跳过这个步骤
参数解析
cli
命令行的参数解析是超级费时间的,特别是你需要有一个友好的 help
信息或者参数校验时。
但我们在日常使用当中,这些都是低频的操作,所以也应该做按需 ?
import { getConfig } from "./src/config.ts";import {printListRegistrys,printListRegistrysWithNetworkDelay,printRegistry,} from "./src/registrys.ts";if (import.meta.main) {const { args } = Deno;// 简单的使用应该提前执行,并避免耗时的参数解析和模块加载if (args.length === 0) {const { configRegistry } = await getConfig();printRegistry(configRegistry);Deno.exit(0); // 执行成功后直接退出,跳过参数解析}// ....// 复杂的查看 help 信息和参数校验,应该后置并按需导入以提高性能const { action } = await import("./src/cli.ts");await action();}import { getConfig } from "./src/config.ts"; import { printListRegistrys, printListRegistrysWithNetworkDelay, printRegistry, } from "./src/registrys.ts"; if (import.meta.main) { const { args } = Deno; // 简单的使用应该提前执行,并避免耗时的参数解析和模块加载 if (args.length === 0) { const { configRegistry } = await getConfig(); printRegistry(configRegistry); Deno.exit(0); // 执行成功后直接退出,跳过参数解析 } // .... // 复杂的查看 help 信息和参数校验,应该后置并按需导入以提高性能 const { action } = await import("./src/cli.ts"); await action(); }import { getConfig } from "./src/config.ts"; import { printListRegistrys, printListRegistrysWithNetworkDelay, printRegistry, } from "./src/registrys.ts"; if (import.meta.main) { const { args } = Deno; // 简单的使用应该提前执行,并避免耗时的参数解析和模块加载 if (args.length === 0) { const { configRegistry } = await getConfig(); printRegistry(configRegistry); Deno.exit(0); // 执行成功后直接退出,跳过参数解析 } // .... // 复杂的查看 help 信息和参数校验,应该后置并按需导入以提高性能 const { action } = await import("./src/cli.ts"); await action(); }
按需加载依赖
像上边的例子 import("./src/cli.ts")
来按需引入参数解析模块,如果没有用到,则可以免去该模块及其背后依赖的解析时间。
另一个就是刚刚讲 deno
时说的,我们可以引入依赖内部对应的模块 ?
import { exists } from "https://deno.land/std@0.192.0/fs/mod.ts";// × 不应该这么做,这会解析 fs 下所有的模块import { exists } from "https://deno.land/std@0.192.0/fs/mod.ts"; // × 不应该这么做,这会解析 fs 下所有的模块import { exists } from "https://deno.land/std@0.192.0/fs/mod.ts"; // × 不应该这么做,这会解析 fs 下所有的模块
import { exists } from "https://deno.land/std@0.192.0/fs/exists.ts";// √ 最终解析脚本只会解析 exists.ts 及其依赖的脚本,而不会解析一整个 fs 包import { exists } from "https://deno.land/std@0.192.0/fs/exists.ts"; // √ 最终解析脚本只会解析 exists.ts 及其依赖的脚本,而不会解析一整个 fs 包import { exists } from "https://deno.land/std@0.192.0/fs/exists.ts"; // √ 最终解析脚本只会解析 exists.ts 及其依赖的脚本,而不会解析一整个 fs 包
以上就是 dnrm 内部做的所有优化,可以前往 ? dnrm 查看详情,如果喜欢,欢迎 star
,也欢迎 issue
和 pr
?