引子:那个让我崩溃的早晨
某个周一早晨,我像往常一样打开终端准备开始工作。
# 打开 iTerm2... 等待... 等待... # 终于出现命令提示符,耗时 2.3 秒接着切换到一个需要 Node 16 的老项目:
$ nvm use 16 Now using node v16.20.2 (npm v8.19.4) # 又是 0.5 秒过去了然后跳到另一个 Node 20 的新项目:
$ cd ../new-project $ nvm use 20 Now using node v20.19.6 (npm v10.8.2) # 再等 0.5 秒一天下来,在 5-6 个项目间来回切换,光是等 nvm 响应就浪费了不知道多少时间。更别提有时候忘记nvm use,直接npm install导致 node_modules 版本混乱的惨剧了。
是时候做出改变了。
为什么要离开 nvm?
痛点一:Shell 启动慢得令人发指
nvm 是纯 Bash 脚本实现的。每次打开新终端,它都要:
- 加载
nvm.sh脚本(几千行) - 解析已安装的 Node 版本
- 设置环境变量
- 初始化自动补全
我用time测了一下我的.zshrc加载时间:
# 有 nvm $ time zsh -i -c exit zsh -i -c exit 0.42s user 0.23s system 95% cpu 0.678 total # 注释掉 nvm 后 $ time zsh -i -c exit zsh -i -c exit 0.08s user 0.05s system 92% cpu 0.142 totalnvm 让我的终端启动慢了近 5 倍!
痛点二:版本切换不够智能
每次进入不同项目都要手动nvm use,太原始了。虽然有avn、nvm-auto等插件可以实现自动切换,但:
- 又增加了一层依赖
- 又拖慢了 Shell 速度
- 配置起来也挺麻烦
痛点三:Windows 支持是个笑话
nvm 官方根本不支持 Windows。nvm-windows是另一个独立项目,命令和行为都有差异。团队里用 Windows 的同事经常遇到各种奇怪问题。
痛点四:偶发的诡异 Bug
用了几年 nvm,遇到过各种奇怪问题:
- 全局安装的包莫名消失
npm prefix路径错乱- 多个终端窗口版本不同步
.nvmrc有时候不生效
遇见 fnm:Rust
带来的性能革命
fnm(Fast Node Manager)是用 Rust 写的 Node.js
版本管理器。第一次用的时候,我的反应是:
"就这?结束了?怎么这么快?"
性能实测对比
我在自己的 M1 MacBook Pro 上做了详细测试:
终端启动时间
# nvm $ time zsh -i -c exit 0.678 total # fnm $ time zsh -i -c exit 0.089 total # 提升:7.6 倍版本切换时间
# nvm $ time nvm use 20 Now using node v20.19.6 real 0m0.347s # fnm $ time fnm use 20 Using Node v20.19.6 real 0m0.012s # 提升:29 倍安装新版本
# nvm install 22 # 总耗时约 45 秒(包含下载) # fnm install 22 # 总耗时约 38 秒(包含下载) # 下载速度差不多,但 fnm 的解压和配置更快为什么 fnm 这么快?
- 原生二进制:Rust 编译成机器码,不需要解释器
- 并行处理:充分利用多核 CPU
- 惰性加载:只在需要时才读取版本信息
- 高效的文件操作:Rust 的 I/O 性能本就出色
真实场景:fnm 如何改变我的工作流
场景一:多项目并行开发
我同时维护着这些项目:
| 项目 | Node 版本 | 原因 |
|---|---|---|
| 老后台系统 | 14 | 历史包袱,依赖不支持高版本 |
| 主站前端 | 18 | 稳定的 LTS |
| 新管理后台 | 20 | 需要新特性 |
| 实验性项目 | 22 | 尝鲜最新 API |
| Electron 应用 | 18 | Electron 版本限制 |
以前用 nvm:
$ cd legacy-admin $ nvm use # 等待... Found '/Users/me/legacy-admin/.nvmrc' with version <14> Now using node v14.21.3 $ cd ../main-site $ nvm use # 又等待... Found '/Users/me/main-site/.nvmrc' with version <18> Now using node v18.20.8 # 经常忘记 nvm use,然后... $ npm install # 装了一堆错误版本的依赖 💥现在用 fnm:
$ cd legacy-admin Using Node v14.21.3 # 自动切换,瞬间完成 $ cd ../main-site Using Node v20.19.6 # 无感切换 # 永远不会忘记切换版本,因为是自动的场景二:CI/CD 环境统一
我们团队的 CI 配置以前是这样的:
# .gitlab-ci.yml (使用 nvm) before_script: - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash - export NVM_DIR="$HOME/.nvm" - . "$NVM_DIR/nvm.sh" - nvm install - nvm use换成 fnm 后:
# .gitlab-ci.yml (使用 fnm) before_script: - curl -fsSL https://fnm.vercel.app/install | bash - eval "$(fnm env)" - fnm install - fnm useCI 构建时间减少了约 15 秒(主要是 nvm 初始化太慢)。
场景三:团队协作与跨平台
我们团队成员使用的系统:
- 60% macOS
- 30% Windows
- 10% Linux
nvm 时代的痛苦:
# macOS/Linux 同事 $ nvm use 20 # Windows 同事(nvm-windows) $ nvm use 20.19.6 # 必须写完整版本号! # 而且 .nvmrc 经常不生效fnm 时代的统一:
# 所有平台,相同命令,相同行为 $ fnm use 20Windows 同事终于不用单独维护一套文档了。
场景四:Docker 开发环境
在
Dockerfile 中安装 Node:
# 以前用 nvm(不推荐,但有人这么干) RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \ && . ~/.nvm/nvm.sh \ && nvm install 20 \ && nvm alias default 20 # 镜像体积大,层数多 # 现在用 fnm RUN curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir /usr/local/bin \ && fnm install 20 \ && fnm default 20 # 更简洁,体积更小场景五:Monorepo
中的多版本需求
我们有一个 Monorepo,不同 package 需要不同 Node 版本:
monorepo/ ├── packages/ │ ├── legacy-sdk/ # 需要 Node 14(兼容老用户) │ │ └── .node-version # 14 │ ├── web-app/ # 需要 Node 20 │ │ └── .node-version # 20 │ └── cli-tool/ # 需要 Node 18 │ └── .node-version # 18 └── .node-version # 20(默认)fnm 的--use-on-cd让我在不同 package 间跳转时完全无感:
$ cd packages/legacy-sdk Using Node v14.21.3 $ cd ../web-app Using Node v20.19.6 $ cd ../cli-tool Using Node v18.20.8完整迁移指南
第一步:安装 fnm
macOS (Homebrew):
brew install fnmWindows (Scoop):
scoop install fnmWindows (Chocolatey):
choco install fnmLinux/macOS (curl):
curl -fsSL https://fnm.vercel.app/install | bashCargo (Rust 用户):
cargo install fnm第二步:配置 Shell
这是最关键的一步,配置正确才能享受自动切换的便利。
Zsh (~/.zshrc):
# fnm - Fast Node Manager eval "$(fnm env --use-on-cd --shell zsh)"Bash (~/.bashrc):
# fnm - Fast Node Manager eval "$(fnm env --use-on-cd --shell bash)"Fish (~/.config/fish/config.fish):
# fnm - Fast Node Manager fnm env --use-on-cd --shell fish | sourcePowerShell ($PROFILE):
# fnm - Fast Node Manager fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression参数说明:
| 参数 | 作用 |
|---|---|
| --use-on-cd | 进入目录时自动读取 .node-version 或 .nvmrc 并切换 |
| --shell <shell> | 指定 Shell 类型,生成对应的环境变量设置命令 |
| --version-file-strategy recursive | 向上递归查找版本文件(可选) |
| --corepack-enabled | 自动启用 Corepack(可选) |
第三步:迁移已安装的 Node 版本
查看 nvm 安装了哪些版本:
ls ~/.nvm/versions/node/ # v10.24.1 v14.21.3 v16.20.2 v18.20.8 v20.19.6 v22.21.0在 fnm 中安装对应版本:
# 方式一:通过 LTS 代号安装(推荐,自动获取最新补丁版本) fnm install lts/fermium # v14.x fnm install lts/gallium # v16.x fnm install lts/hydrogen # v18.x fnm install lts/iron # v20.x fnm install lts/jod # v22.x # 方式二:通过大版本号安装 fnm install 14 fnm install 16 fnm install 18 fnm install 20 fnm install 22 # 方式三:安装精确版本 fnm install 18.20.8 fnm install 20.19.6设置默认版本:
fnm default 22第四步:处理全局 npm 包
这是很多人忽略的一步!nvm 下安装的全局包不会自动迁移。
查看 nvm 中的全局包:
# 切换到 nvm 的某个版本 export PATH="$HOME/.nvm/versions/node/v20.19.6/bin:$PATH" npm list -g --depth=0在 fnm 的对应版本中重新安装:
fnm use 20 npm install -g typescript ts-node nodemon pm2 # 你需要的包Pro Tip:可以写个脚本批量处理:
#!/bin/bash # migrate-global-packages.sh # 你常用的全局包 PACKAGES="typescript ts-node nodemon pm2 pnpm yarn" for version in 18 20 22; do echo "Installing global packages for Node $version..." fnm use $version npm install -g $PACKAGES done第五步:注释掉 nvm 配置
编辑你的 Shell 配置文件:
# ~/.zshrc 或 ~/.bashrc # nvm - 已迁移到 fnm,注释掉避免冲突 # export NVM_DIR="$HOME/.nvm" # [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"第六步:验证迁移结果
# 重新加载配置 source ~/.zshrc # 或 source ~/.bashrc # 验证 fnm 工作正常 fnm list fnm current node -v npm -v which node # 测试自动切换 cd /path/to/project-with-nvmrc # 应该看到 "Using Node vX.X.X" node -v第七步:删除 nvm(可选但推荐)
确认一切正常后,可以删除 nvm 释放磁盘空间:
rm -rf ~/.nvm # 如果遇到权限问题 sudo rm -rf ~/.nvm命令速查表
| 功能 | nvm 命令 | fnm 命令 |
|---|---|---|
| 安装指定版本 | nvm install 20 | fnm install 20 |
| 安装最新 LTS | nvm install --lts | fnm install --lts |
| 安装指定 LTS | nvm install --lts=iron | fnm install lts/iron |
| 切换版本 | nvm use 20 | fnm use 20 |
| 设置默认版本 | nvm alias default 20 | fnm default 20 |
| 查看已安装版本 | nvm ls | fnm list |
| 查看远程可用版本 | nvm ls-remote | fnm list-remote |
| 查看当前版本 | nvm current | fnm current |
| 卸载版本 | nvm uninstall 18 | fnm uninstall 18 |
| 在指定版本执行命令 | nvm exec 18 node -v | fnm exec --using=18 node -v |
LTS 版本代号参考
| 代号 | 版本 | 发布日期 | LTS 开始 | 维护结束 | 状态 |
|---|---|---|---|---|---|
| Jod | v22.x | 2024-04 | 2024-10 | 2027-04 | ✅ Active LTS |
| Iron | v20.x | 2023-04 | 2023-10 | 2026-04 | ✅ Active LTS |
| Hydrogen | v18.x | 2022-04 | 2022-10 | 2025-04 | ⚠️ Maintenance |
| Gallium | v16.x | 2021-04 | 2021-10 | 2024-09 | ❌ End-of-Life |
| Fermium | v14.x | 2020-04 | 2020-10 | 2023-04 | ❌ End-of-Life |
建议:新项目使用 v20 或 v22,老项目尽快升级到至少 v18。
进阶技巧
1. 配置版本文件查找策略
默认情况下,fnm 只在当前目录查找.node-version或.nvmrc。如果你的项目结构比较深,可以启用递归查找:
eval "$(fnm env --use-on-cd --version-file-strategy recursive)"2. 启用 Corepack
Corepack 是 Node.js 内置的包管理器版本管理工具,可以锁定 pnpm/yarn 版本:
eval "$(fnm env --use-on-cd --corepack-enabled)"3. 自定义安装目录
默认安装在~/.local/share/fnm,可以自定义:
export FNM_DIR="/path/to/custom/fnm" eval "$(fnm env --use-on-cd)"4. 使用国内镜像加速
# 临时使用 fnm install 20 --node-dist-mirror=https://npmmirror.com/mirrors/node # 永久配置 export FNM_NODE_DIST_MIRROR="https://npmmirror.com/mirrors/node"5. 在脚本中使用 fnm
#!/bin/bash # 确保 fnm 环境已加载 eval "$(fnm env)" # 使用指定版本执行 fnm use 20 node your-script.js # 或者用 exec fnm exec --using=20 node your-script.js常见问题 FAQ
Q: fnm 安装的 Node 在哪里?
~/.local/share/fnm/node-versions/每个版本是一个独立目录,结构清晰。
Q: 为什么which node显示的路径很奇怪?
fnm 使用"多 Shell"机制,每个 Shell 会话有独立的 PATH:
$ which node /Users/xxx/.local/state/fnm_multishells/12345_1234567890/bin/node这是正常的,确保了不同终端窗口可以使用不同版本。
Q: 如何在 VS Code
中使用 fnm 管理的 Node?
VS Code 会自动检测 fnm。如果遇到问题,可以在settings.json中配置:
{ "terminal.integrated.env.osx": { "PATH": "${env:PATH}" } }或者在项目根目录创建.vscode/settings.json:
{{ "eslint.runtime": "node" }Q: fnm 支持 .nvmrc 吗?
完全支持!fnm 会按以下顺序查找版本文件:
.node-version.nvmrcpackage.json的engines.node字段
Q: 如何回退到 nvm?
如果你想回退(虽然我不建议),只需:
- 注释掉 fnm 配置
- 取消注释 nvm 配置
- 重新加载 Shell
你的 nvm 数据(如果没删)还在~/.nvm。
总结:值得迁移吗?
绝对值得。
迁移成本:
- 时间:约 15-30 分钟
- 学习曲线:几乎为零(命令高度相似)
- 风险:极低(可随时回退)
获得收益:
- 终端启动快 5-10 倍
- 版本切换快 20-30 倍
- 自动切换版本,告别手动
nvm use - 跨平台一致性,团队协作更顺畅
- 更少的 Bug 和怪异行为
如果你每天要打开几十次终端、在多个项目间切换,fnm 节省的时间累积起来是非常可观的。更重要的是,那种丝滑无感的体验,会让你的开发心情都变好。