mrkaluzny homepage
Get in touch
mrkaluzny homepage
Tools & Tech

nvm vs fnm: Which Node Version Manager Should You Use?

Nov 12, 2026

If you’ve been writing JavaScript for a while, nvm is probably the first thing you install on a new machine. I did that for years without a second thought. It was in every .zshrc I set up, right next to oh-my-zsh, and I never once asked what it cost me (spoiler: about a second per terminal tab).

Then I started running AI agents in several projects at once, every one of them opening its own shells, and nvm suddenly became the slowest part of my day. I moved to fnm in October, and I wrote up the whole migration, together with my move to Ghostty. This post is the comparison I wish I’d had before I switched.

nvm vs fnm: The Short Answer

Use fnm, unless you depend on something only nvm does.

  • Pick fnm if you open a lot of shells, work on both Windows and macOS, or have projects with .node-version files. It loads in 14 ms instead of 600+, auto-switching on cd is one flag, and it’s the same tool on every OS.
  • Stay on nvm if you use nvm reinstall-packages, a default-packages file or build Node from source. Same if your CI scripts and team docs assume nvm and nobody is complaining about speed.

A Bit of History: The Shell Script Everyone Uses

nvm (Node Version Manager) has been the default answer to “how do I install Node” for over a decade. It’s a shell script: you install it with a curl one-liner, it adds a few lines to your .zshrc or .bashrc, and every new shell loads it. It has about 96,000 GitHub stars, and v0.41.0 came out on November 4, 2026.

fnm (Fast Node Manager) does the same job, but it’s a single binary written in Rust (because of course it is). Your shell config calls fnm env once, it prints a few environment variables, and exits. It has about 27,000 stars, and the latest release is 1.39.0 from March 2026.

Under the hood they work the same way. Both download official builds from nodejs.org, keep each version in its own folder, and give each version its own global npm packages. Here’s why fnm wins anyway:

  • Startup Cost: fnm adds about 7 ms to a new shell. nvm adds over 600 ms, and more than a second if it also activates your default version.
  • Auto-Switching: fnm switches Node versions on cd with one flag. nvm needs a two-dozen-line hook copied from its README.
  • Version Files: fnm reads .nvmrc, .node-version and engines.node from package.json. nvm only reads .nvmrc.
  • Windows: fnm is the same tool on macOS, Linux and Windows. nvm doesn’t run on Windows outside WSL.

I prefer fnm because it does everything I used nvm for, without the startup cost and without a copy-pasted hook in my .zshrc.

nvm vs fnm: Speed

Measured on my MacBook Pro M1 Pro with zsh 5.9, median of 20 runs. Each test starts a clean zsh -f and loads only the version manager, so the numbers show the cost of nvm or fnm alone:

What runs on shell start Median
Empty zsh -f (baseline) 7 ms
fnm env --use-on-cd 14 ms
source nvm.sh 635 ms
source nvm.sh + nvm use default 1161 ms

Loading nvm costs about 45 times more than loading fnm. Those nvm numbers are from v0.40.3, and it’s not getting lighter: in v0.41.0, nvm.sh grew from 4,661 to 6,942 lines, and zsh reads all of it in every new shell.

If you open three terminals a day, 600 ms won’t bother you. I open dozens, and coding agents like Claude Code and Codex open even more. Every split pane, every new tab and every shell an agent spawns pays that cost again.

The usual fix for a slow nvm is lazy loading. Plugins like zsh-nvm with NVM_LAZY_LOAD=true create placeholder node, npm and nvm commands and only load nvm the first time you call one. Your prompt shows up fast, but the first node call in every shell still waits, and a hook that switches versions on cd has to load nvm anyway. With fnm you don’t need the workaround.

nvm vs fnm: Switching Versions on cd

This is where the difference is easiest to see. Both tools can switch to the right Node version when you cd into a project with an .nvmrc. Here’s what it takes.

nvm Example

# ~/.zshrc, from the nvm README. Place this after nvm initialization!
autoload -U add-zsh-hook

load-nvmrc() {
  local nvmrc_path
  nvmrc_path="$(nvm_find_nvmrc)"

  if [ -n "$nvmrc_path" ]; then
    local nvmrc_node_version
    nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")

    if [ "$nvmrc_node_version" = "N/A" ]; then
      nvm install
    elif [ "$nvmrc_node_version" != "$(nvm version)" ]; then
      nvm use
    fi
  elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ] && [ "$(nvm version)" != "$(nvm version default)" ]; then
    echo "Reverting to nvm default version"
    nvm use default
  fi
}

add-zsh-hook chpwd load-nvmrc
load-nvmrc

fnm Example

# ~/.zshrc
eval "$(fnm env --use-on-cd --version-file-strategy=recursive --shell zsh)"

Less Code in Your .zshrc The nvm hook is shell code you copy, paste and never read again, and it calls nvm on every directory change. With fnm it’s a flag, and the fnm team maintains the logic, not you.

Monorepo Support --version-file-strategy=recursive makes fnm look in parent folders for the nearest version file. That matters in a monorepo, where you often cd into a package that doesn’t have its own .nvmrc.

Same Setup Everywhere The fish and PowerShell versions are one line too, so the whole team gets the same behavior on every OS.

nvm vs fnm: Features

nvm fnm
Written in Shell script Rust, single binary
Install on macOS curl install script brew install fnm
Windows No, only WSL (nvm-windows is separate) Yes
Shells sh, dash, bash, zsh, ksh bash, zsh, fish, PowerShell
Reads .nvmrc Yes Yes
Reads .node-version No Yes
Reads engines.node in package.json No Yes, if there’s no version file
Switch version on cd Hook copied from the README --use-on-cd flag
Run one command on another version nvm exec 22 node app.js fnm exec --using=22 node app.js
Install default global packages $NVM_DIR/default-packages No
Copy globals from another version nvm reinstall-packages No
Build Node from source Yes No
Enable Corepack on install No --corepack-enabled

Windows deserves its own note. nvm doesn’t run there outside WSL. The popular nvm-windows is a different project by a different author, with its own commands and behavior, so a team on mixed machines ends up with two tools and two sets of instructions. fnm is the same binary on all three platforms.

The Corepack flag matters less than it used to. Node 25 stopped shipping Corepack, so --corepack-enabled only does something on Node 24 and older. On newer versions you install it with npm i -g corepack, whatever version manager you use.

Where nvm Still Wins

To be fair to nvm, look at the three rows where fnm says “No”. If you install a fresh Node and expect default-packages to bring back your global CLIs, or you run nvm install --reinstall-packages-from=current 'lts/*' on every upgrade, fnm has no equivalent.

I reinstall my handful of globals by hand and it takes a minute. If your list is long, or you build Node from source, that’s a real reason to stay on nvm.

Which Version File Should You Use?

fnm checks for a version in this order: .nvmrc, then .node-version, then the engines.node range in package.json. For a range like >=22 <23, it picks the newest installed version that fits. nvm only reads .nvmrc.

If you’re choosing what to commit, use .nvmrc. nvm and fnm both read it, and so do GitHub Actions (actions/setup-node with node-version-file: .nvmrc) and most hosting platforms.

Put a major version in it, like 24, not lts/*. lts/* means “the newest LTS”, and that changes under you. Node 26 became LTS in October, so every project with lts/* quietly moved to a new major for anyone setting up a fresh machine. fnm adds a smaller annoyance on top: every time a new LTS patch is released, fnm use refuses to switch until you install it.

$ fnm use
error: Requested version lts-latest is not currently installed

A plain 24 in .nvmrc matches any installed 24.x, so neither problem comes up.

How to Switch from nvm to fnm

The commands map almost one to one:

nvm fnm
nvm install 24 fnm install 24
nvm install --lts fnm install --lts
nvm use fnm use
nvm use 22 fnm use 22
nvm alias default 24 fnm default 24
nvm alias work 22 fnm alias 22 work
nvm current fnm current
nvm ls fnm ls
nvm ls-remote --lts fnm ls-remote --lts
nvm exec 22 node app.js fnm exec --using=22 node app.js
nvm uninstall 18 fnm uninstall 18

Watch the argument order on alias. nvm takes the name first, fnm takes the version first.

The switch itself takes about ten minutes:

  1. Write down what you have with nvm ls and npm ls -g --depth=0. Global packages don’t carry over.
  2. Remove nvm. Delete ~/.nvm, the NVM_DIR lines and the load-nvmrc hook from your shell config.
  3. Install fnm with brew install fnm on macOS, the install script on Linux, or winget install Schniz.fnm on Windows.
  4. Hook it into your shell with the one-liner from the fnm example above. For fish and PowerShell:
# fish: ~/.config/fish/conf.d/fnm.fish
fnm env --use-on-cd --shell fish | source
# PowerShell profile
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression
  1. Install your versions with fnm install 24 and fnm install 22, set a default with fnm default 24, and reinstall your global packages.

If you’re on Apple Silicon and need Node older than 16, my migration post covers the Rosetta fix. And if you deploy to Laravel Forge, I wrote up a recipe that installs fnm on the server so every site uses the version from its own .nvmrc.

Alternatives to nvm and fnm

These are the other names that come up in every “what’s better than nvm” thread:

  • Volta: Unmaintained. Its README says so at the top and recommends moving to mise. It still works, but I wouldn’t start a new setup on it.
  • mise: Manages Node alongside Python, Go, Ruby and dozens of other tools, with one config file per project. One catch for nvm users: since mise 2025.10.0 it ignores .nvmrc and .node-version until you turn them on with mise settings add idiomatic_version_file_enable_tools node.
  • n: The simplest of the bunch. No shell hook at all. It installs one active Node version into /usr/local, and switching is a command you run, not something that happens when you cd.

If Node is the only runtime you manage, fnm does the job with the least setup. If you also juggle Python and Go versions, mise is worth the extra config.

FAQ

Is fnm better than nvm?

For most people, yes. It does the same job, reads the same .nvmrc files and loads in 14 ms instead of 600 ms or more. nvm still has a few extras fnm lacks: default-packages, reinstall-packages and building from source.

Does fnm work with .nvmrc?

Yes. fnm reads .nvmrc, .node-version and the engines.node field in package.json, so existing nvm projects work without changes.

Does fnm work on Windows?

Yes, with PowerShell, and you can install it with winget. nvm doesn’t run on Windows outside WSL.

Can I keep nvm and fnm installed at the same time?

You can, but both put node on your PATH, and whichever loads last in your shell config wins. That’s a confusing way to spend an afternoon. Pick one and remove the other.

Is fnm still maintained?

Yes. Commits are still landing, the last one in late October 2026. It releases less often than nvm, with 1.39.0 in March 2026, but it’s a small binary that downloads Node builds and sets your PATH, so there isn’t much that needs to change.

The Future is Rust (Of Course)

nvm earned its place. For more than ten years it was the tool that made juggling Node versions bearable, and it still has a few tricks fnm doesn’t. But it was built for a time when you opened a terminal and kept it open all day. Today my terminal opens and closes shells all the time, and plenty of them aren’t even opened by me.

fnm gives you the same commands, reads the same .nvmrc files, and takes ten minutes to set up. So next time a new tab takes a moment too long to show a prompt, run time zsh -i -c exit and look at your .zshrc. There’s a good chance nvm is the reason, and fnm is the fix.