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⏱️ 15 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.

fnm vs Volta: head to head

If you have ruled out nvm and it is down to these two, pick fnm for a personal machine and Volta for a team repo. That is the whole verdict, and the rest is why.

Both are compiled Rust binaries, so on raw shell startup they are effectively tied (low single digits of init cost on my machine), and both auto-switch versions for you, but the mechanism differs. fnm switches on cd through a shell hook (eval "$(fnm env --use-on-cd)") and reads the version from .nvmrc or .node-version, the same files nvm uses. Volta needs no hook at all: it shims node/npm and reads the "volta" key in package.json, swapping versions the moment you run a command in that directory.

That pinning split is the deciding factor. fnm pins Node only, lives in dotfiles, and migrates from nvm with zero file changes, which is exactly what you want solo. Volta pins Node and the package manager in source-controlled package.json, so a teammate is on the right Node and npm the first time they run a command, with nothing to install or remember. fnm wins for personal speed and nvm compatibility; Volta wins when the repo should dictate the version for everyone.

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. After switching, confirm you are on the version you expect with the checks in how to check your Node version. 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. I cover the action end to end, including caching and the version-file behavior, in the setup-node CI guide:

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?

For the best Node version manager in 2026, my pick is fnm for a personal machine (fast, cross-platform, drops in over your existing nvm setup) and Volta for a team repo (pins Node and the package manager in package.json for everyone). 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

Close to it. fnm reads the same .nvmrc files nvm uses (and also .node-version), and its command set maps almost one-to-one: fnm install --lts, fnm use, fnm default. The migration is installing fnm, adding eval "$(fnm env --use-on-cd)" to your rc file, and removing the nvm init lines.

The differences are minor: a few flag names differ, and fnm manages Node only, not your package manager. For most workflows you will not notice you switched.

It can, but it solves a slightly different problem. nvm and fnm give you a global active Node version you switch by hand or on cd. Volta pins the version per repo in package.json and switches transparently, so a whole team lands on the same Node and package manager without anyone running a command.

If you work solo across many projects, fnm's cd auto-switching covers most of the same ground. If you work on a team and want the repo to dictate the version, Volta is the better fit. See the pinning guide for how the files compare.

nvm is a Bash script that your shell sources on every new session via a line in ~/.bashrc or ~/.zshrc. Loading that script, plus any auto-switching hook, adds latency to each shell you open. With many installed versions it gets worse.

fnm and Volta are compiled Rust binaries, so their shell-init cost is negligible. Switching from nvm to fnm is the usual fix, and because fnm reads your existing .nvmrc files it is a near-zero-effort migration.

Not the Unix nvm on native Windows; it is a Bash script, so it runs only under WSL. For native Windows there is a separate project, nvm-windows, with similar commands but a different codebase and no .nvmrc support.

On Windows I reach for fnm (winget install Schniz.fnm) or Volta (winget install Volta.Volta) instead. Both run natively and use the same commands as on macOS and Linux.

asdf and mise (formerly rtx) are general-purpose version managers: one tool that pins Node, Ruby, Python, and dozens of other runtimes from a single .tool-versions file. mise is the faster, Rust-based successor and also reads .node-version and .nvmrc. If you juggle several language runtimes and want one manager for all of them, reach for mise. If Node is the only runtime you care about, a Node-specific tool is simpler: fnm for speed, Volta for per-repo pinning in package.json.

The pinning files overlap, so you are not locked in. A repo with a .nvmrc works with nvm, fnm, asdf, and mise without changes.

Yes, and the gap is in shell startup, not in installing or switching Node. nvm is a Bash script your shell sources on every new session; fnm is a compiled Rust binary, so the per-shell init cost drops from tens of milliseconds to low single digits. On a machine where you open shells constantly (tmux panes, editor terminals), that is the difference you feel. Switching is cheap because fnm reads your existing .nvmrc files unchanged. See the Node update guide for the full set of routes.

Yes, I keep both. The "volta" key in package.json drives Volta and pins the package manager, but a teammate who has not installed Volta yet can still run fnm use or nvm use if there is an .nvmrc in the repo. It costs nothing and removes a "wrong Node version" failure mode for new contributors.

See also

Sources

Authoritative references this article was fact-checked against.

TagsNode.jsJavaScriptnvmfnmVoltaVersion ManagementCLIDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Convert every MyISAM table to InnoDB: why MyISAM is dead (no transactions, no FKs, crash-unsafe), the ALTER TABLE syntax, FULLTEXT considerations, and a script to convert all tables in a database.

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.