TechEarl

nvm vs fnm vs Volta: Which Node Version Manager?

nvm vs fnm vs Volta, compared by speed, auto-switching, platform support, and pinning model. Which Node version manager to pick in 2026, with install commands, the .nvmrc vs package.json question, and honest caveats from running all three.

Ishan Karunaratne⏱️ 12 min readUpdated
Share thisCopied
nvm vs fnm vs Volta comparison: which Node.js version manager to choose by speed, auto-switching, and per-project pinning

I have run all three of these on my own machines and across team setups, and the short version is this: nvm is the default everyone already knows, fnm is the fast cross-platform replacement for it, and Volta is the one that pins Node per repo so a whole team lands on the same version automatically. If you want a one-line recommendation for nvm vs fnm vs Volta in 2026: pick fnm for a personal machine (same .nvmrc files as nvm, far faster shell startup, works on Windows), and Volta for a team repo where you want Node and the package manager pinned in package.json and switched transparently. nvm is still a fine choice if you already have it wired into your dotfiles and team docs. Below is the full comparison, the install commands for each, the per-project pinning question (.nvmrc vs .node-version vs package.json), and the honest caveats that decide it.

How are nvm, fnm, and Volta different?

All three let you install multiple Node versions side-by-side and switch between them, but they differ in three ways that actually matter day to day: how fast they are, whether they auto-switch when you cd into a project, and where they read the pinned version from.

  • nvm is a Bash script that shims node/npm/npx. It is the most documented, runs on Linux and macOS (and Windows only through WSL; native Windows needs the separate nvm-windows project), and adds noticeable startup latency to every new shell because it sources a large script in your rc file.
  • fnm is a single Rust binary that does the same job as nvm with a fraction of the startup cost, runs natively on Linux, macOS, and Windows, and reads the same .nvmrc files (plus .node-version).
  • Volta flips the model: instead of a global "active version" you switch by hand, every project declares its Node and package manager versions in package.json, and Volta swaps to the right one transparently when you run a command in that directory.

Jump to:

Comparison table

nvmfnmVolta
ImplementationBash scriptRust binaryRust binary
PlatformsLinux, macOSLinux, macOS, WindowsLinux, macOS, Windows
Shell startup costHighVery lowVery low
Auto-switch on cdManual (or slow hook)Yes (--use-on-cd)Yes (transparent, no hook)
Pins from.nvmrc.nvmrc, .node-versionpackage.json "volta"
Pins package manager tooNoNoYes (npm, Yarn)
Windows storySeparate nvm-windows projectNative, same commandsNative installer
Best forExisting setups, tutorialsPersonal machinesTeams, monorepos

If you cannot decide between the three, the decision tree is short: on Windows or chasing shell speed, fnm; want the repo to dictate the version for everyone, Volta; already deep into nvm and not hurting, stay on nvm.

nvm: the default everyone knows

nvm is the original and still the most widely referenced. Install or update to the latest release:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
source ~/.bashrc       # or ~/.zshrc on macOS

Install and switch:

bash
nvm install --lts              # latest LTS
nvm install 22.11.0            # a specific version
nvm use --lts                  # activate for this shell
nvm alias default --lts        # default for new shells
nvm ls                         # list installed versions

Per-project pinning uses an .nvmrc file with a single version line. The catch is that nvm does not auto-switch on cd unless you add a hook to your rc file, and the hook the README provides spawns a subshell on every directory change, which is the slow path. nvm is a Bash script, so it does not run natively on Windows; Windows users reach for the unrelated nvm-windows project, which has similar commands but a different codebase and a different (no .nvmrc) pinning story.

bash
echo "22" > .nvmrc
nvm use                        # reads .nvmrc in the current directory

The honest caveat: sourcing nvm in your .zshrc adds real milliseconds to every shell you open. On a machine where you spawn shells constantly (tmux panes, editor terminals), that adds up, and it is the single most common reason people migrate to fnm. See the full upgrade walkthrough for nvm alongside every other route.

fnm: nvm but fast and cross-platform

fnm (Fast Node Manager) is a Rust rewrite that keeps nvm's mental model and adds Windows support and near-instant shell startup. Install:

bash
# Linux / macOS
curl -fsSL https://fnm.vercel.app/install | bash

# macOS via Homebrew
brew install fnm

# Windows (PowerShell)
winget install Schniz.fnm

The line that earns fnm its reputation is the shell hook, which enables auto-switching when you change directories:

bash
# ~/.zshrc or ~/.bashrc
eval "$(fnm env --use-on-cd)"

# PowerShell ($PROFILE)
fnm env --use-on-cd | Out-String | Invoke-Expression

Install, use, and pin:

bash
fnm install --lts
fnm use lts-latest             # or a specific version
fnm default lts-latest         # default for new shells
fnm list                       # local versions

The decisive feature: fnm reads both .nvmrc and .node-version, so migrating from nvm needs no file changes. Drop the hook into your rc file and every cd into a project with a pinned version switches Node for you, with no conscious step and no forgotten nvm use.

bash
echo "22" > .node-version      # fnm reads either file
cd into-this-project           # fnm auto-switches

The caveat is small: fnm manages Node only, not your package manager version. If you also need to pin npm or Yarn per repo, that is Volta's job.

Volta: per-project pinning in package.json

Volta takes the opposite approach to nvm and fnm. There is no global "I am currently on Node 22" state you flip by hand; instead each repo declares its versions in package.json, and Volta intercepts node/npm calls and runs the version that repo pinned. Install:

bash
# Linux / macOS
curl https://get.volta.sh | bash

# Windows
winget install Volta.Volta

Install Node globally and then pin it inside a project:

bash
volta install node@lts         # available everywhere
volta pin node@22              # writes "volta" key into package.json
volta pin npm@10               # pin the package manager too

That produces this in package.json:

json
{
  "volta": {
    "node": "22.11.0",
    "npm": "10.9.0"
  }
}

Now every developer and every CI run that executes node or npm inside that repo gets exactly those versions, with no shell hook and nothing to remember. The pinning lives in source control, so a teammate cloning the repo is on the right Node the first time they run a command. This is why Volta wins for teams. The trade-off: Volta is opinionated about npm and classic Yarn, and its handling of Yarn Berry (2+) and pnpm is less seamless. pnpm support is still experimental and gated behind a VOLTA_FEATURE_PNPM=1 environment variable (global installs are not supported); for the current state check the Volta pnpm docs, or lean on Corepack instead.

The pinning question: .nvmrc vs .node-version vs package.json

This is the part that actually decides the tool, because the pinning file is the contract between your local machine, your teammates, and CI. The three managers read different things:

FileRead byPins
.nvmrcnvm, fnm, actions/setup-nodeNode only
.node-versionfnm, asdf, actions/setup-nodeNode only
package.json "volta"Volta, actions/setup-nodeNode AND package manager

.nvmrc is the most portable: a one-line file (22, lts/iron, or 22.11.0) that nvm, fnm, and GitHub's actions/setup-node all understand. .node-version is the same idea but read by fnm and asdf rather than nvm. The "volta" key is the only one that also pins your package manager, which is exactly the thing that drifts on a team when one person is on npm 9 and another on npm 11.

For maximum compatibility, I keep a .nvmrc even on Volta repos. It costs nothing, and it means a teammate who has not installed Volta yet can still fnm use or nvm use and land on the right major. For the deeper treatment of pinning patterns, including engines.node and engine-strict, see pinning a Node version with .nvmrc.

Speed: why fnm and Volta win shell startup

The performance gap is not subtle. nvm is a Bash script that your shell sources on every new session; on a cold start it can add tens to a couple hundred milliseconds depending on how many versions you have installed and how your rc file is structured. On my own machine nvm sits around 70 to 80ms of added shell-init, fnm is in the low teens, and Volta is effectively a rounding error (low single digits). Treat those as ballpark, not gospel: the number swings with your hardware, your rc file, and how many versions you have installed. fnm and Volta are compiled Rust binaries, so their shell-init cost is closer to "you cannot feel it."

You can measure your own:

bash
# Time 5 fresh interactive shells (lower is better)
for i in 1 2 3 4 5; do /usr/bin/time -p zsh -i -c exit; done 2>&1 | grep real

If those real numbers bother you and nvm is in your rc file, switching to fnm is the highest-leverage change you can make, and because fnm reads your existing .nvmrc files the migration is just install plus one rc-file line. Volta is similarly cheap on startup; its cost shows up the first time you run a command in a repo with a new pinned version (it downloads that version on demand), which is a one-time hit, not a per-shell tax.

CI: which one your pipeline should read

In CI you usually do not run a version manager at all. GitHub Actions' actions/setup-node reads any of the pinning files directly, which is the cleanest way to keep CI on the same version as local dev:

yaml
- uses: actions/setup-node@v6
  with:
    node-version-file: '.nvmrc'    # or 'package.json' for Volta repos
    cache: 'npm'

Point node-version-file at .nvmrc (works for nvm and fnm repos) or at package.json (the action reads the "volta.node" key). Either way, the version your developers pinned locally is the version CI tests against, with no separate matrix entry to keep in sync. If you need to test across multiple majors, swap to an explicit node-version: [20.x, 22.x, 24.x] matrix instead.

Which one should I use?

After running all three in anger, my defaults:

  • Personal machine, you want it fast and cross-platform: fnm. It reads your existing .nvmrc files, starts shells instantly, and runs the same on Windows as on a Mac.
  • A team repo or monorepo where everyone must be on the same version: Volta. Pinning lives in package.json, in source control, and switches transparently. Pin the package manager too.
  • You already run nvm and it is not bothering you: stay on nvm. It is functionally equivalent for a single project, and migration only pays off if shell startup or Windows support is a real pain point.

For installing Node from scratch before you reach for any of these, see how to install Node.js; for the full set of upgrade routes (version managers, the official installer, package managers, Docker, CI), the Node.js update guide is the hub.

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsNode.jsJavaScriptnvmfnmVoltaVersion ManagementCLIDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

MySQL MyISAM to InnoDB Conversion: Why and How

MyISAM has no transactions, no foreign keys, and corrupts on crash. InnoDB has been the MySQL default since 5.5 and is the only engine that gets new features. Here is how to convert every MyISAM table in a MySQL database with a single SQL script.