Skip to main content

fnm vs nvm vs Volta: Node.js Version Managers in 2026

·PkgPulse Team

fnm vs nvm vs Volta: Node.js Version Managers in 2026

TL;DR

nvm is the original — it works everywhere, every tutorial references it, but it's slow (shell startup overhead) and has no Windows support without WSL. fnm (Fast Node Manager) is the modern drop-in replacement for nvm — written in Rust, 10–40x faster, handles .nvmrc files, and works natively on Windows. Volta takes a different approach — it pins Node.js and npm/yarn/pnpm versions per-project inside package.json, making it the best choice for teams where "it works on my machine" is a real problem. In 2026, new projects should use fnm (if you just want nvm to be fast) or Volta (if you need project-pinned version enforcement).

Key Takeaways

  • nvm shell startup overhead: ~70ms added to every shell startup — significant on laptops with many shell instances
  • fnm is 10–40x faster than nvm for switching and loading Node.js versions (Rust binary vs shell script)
  • Volta pins versions in package.json — CI, every developer, and every repo gets the exact same Node/npm/yarn automatically
  • fnm GitHub stars: ~19k (Feb 2026) — fastest-growing Node.js version manager
  • nvm GitHub stars: ~80k — the most-used but growth is slowing
  • Volta GitHub stars: ~12k — steady growth, strong enterprise adoption
  • All three support .nvmrc — fnm and Volta both auto-switch when you cd into a project

Why Node.js Version Management Matters

Node.js major versions ship frequently. In 2026, Node 22 is the current LTS, but many projects still run 18 or 20. Developers working across multiple repositories need to switch versions instantly without thinking about it.

The problems that version managers solve:

  • Version mismatch — "works on my machine, fails in CI" because CI has a different Node.js version
  • Monorepo complexity — multiple packages in one repo requiring different Node versions
  • Team onboarding — new developers need the correct runtime version immediately
  • CI consistency — ensure the same Node version in every environment

nvm: The Classic Choice

nvm (Node Version Manager) has been the standard tool since 2010. It's a shell script (bash/zsh) that manages multiple Node.js installations in ~/.nvm/versions/node/.

Installation

# Install via official script
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash

# Or via Homebrew (macOS)
brew install nvm

# Add to ~/.zshrc or ~/.bashrc (auto-added by installer)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

Basic Usage

# Install Node.js versions
nvm install 22        # Install Node 22 (LTS)
nvm install 20        # Install Node 20
nvm install --lts     # Install current LTS
nvm install node      # Install latest

# Switch versions
nvm use 22
nvm use 20.11.0       # Exact version
nvm use --lts

# Default version for new shells
nvm alias default 22

# List installed versions
nvm ls
nvm ls-remote         # List all available versions

# Run command with specific version without switching
nvm exec 18 node -e "console.log(process.version)"

# Project-level version with .nvmrc
echo "22" > .nvmrc
nvm use               # Reads .nvmrc and switches

Auto-Switch with .nvmrc

# Add to ~/.zshrc for auto-switching when entering a directory
autoload -U add-zsh-hook
load-nvmrc() {
  local node_version="$(nvm version)"
  local nvmrc_path="$(nvm_find_nvmrc)"

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

    if [ "$nvmrc_node_version" = "N/A" ]; then
      nvm install
    elif [ "$nvmrc_node_version" != "$node_version" ]; then
      nvm use
    fi
  elif [ "$node_version" != "$(nvm version default)" ]; then
    echo "Reverting to nvm default version"
    nvm use default
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

The nvm Performance Problem

# Measure shell startup time
time zsh -i -c exit  # With nvm: ~150-200ms, Without: ~80-100ms
# nvm adds ~70-100ms to EVERY new shell startup

# This is because nvm.sh is a shell script that runs on every shell init
# Fix: lazy-load nvm (reduces startup but breaks auto-switching)

fnm: Fast Node Manager

fnm is written in Rust and designed to be a drop-in replacement for nvm — same .nvmrc file format, similar commands, but dramatically faster. It works natively on Windows, macOS, and Linux.

Installation

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

# macOS via Homebrew
brew install fnm

# Windows via Winget
winget install Schniz.fnm

# Windows via Chocolatey
choco install fnm

# Shell setup (add to .zshrc / .bashrc / .profile)
eval "$(fnm env --use-on-cd --shell zsh)"

# For fish shell
fnm env --use-on-cd --shell fish | source

# For Windows PowerShell (profile.ps1)
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression

Basic Usage

# Install Node.js versions
fnm install 22            # Install Node 22
fnm install --lts         # Install current LTS
fnm install 20.11.0       # Exact version

# Switch versions
fnm use 22
fnm use --lts
fnm use 20.11.0

# Default version
fnm default 22

# List versions
fnm list               # Installed versions
fnm list-remote        # Available versions

# Auto-detect from .nvmrc or .node-version
fnm use               # Reads .nvmrc, switches automatically

# Run command without switching
fnm exec --using=18 node --version

Speed Comparison

# fnm version switching
time fnm use 22     # ~10ms — Rust binary symlink swap
time nvm use 22     # ~200ms — shell script operations

# Shell startup overhead
time zsh -i -c exit
# With nvm eval:    ~180ms
# With fnm eval:    ~100ms  ← fnm adds ~15ms vs nvm's ~70ms

# Installing a new Node.js version
time fnm install 22  # ~30s (download) — comparable to nvm
time nvm install 22  # ~35s (download) — similar

.nvmrc / .node-version Support

# Both file formats work with fnm
echo "22" > .nvmrc           # nvm format
echo "v22.14.0" > .nvmrc     # Exact version
echo "lts/jod" > .nvmrc      # LTS codename

# .node-version (Volta format)
echo "22" > .node-version

# With --use-on-cd flag (set in shell setup above), switching is automatic
cd ~/projects/old-project    # Has .nvmrc: 18 → switches to Node 18
cd ~/projects/new-project    # Has .nvmrc: 22 → switches to Node 22

CI/CD Integration

# GitHub Actions with fnm
- name: Setup Node.js via fnm
  uses: actions/setup-node@v4
  with:
    node-version-file: ".nvmrc"  # Works with GitHub Actions natively

# Or use fnm directly in CI
- name: Install fnm
  run: curl -fsSL https://fnm.vercel.app/install | bash

- name: Install Node.js
  run: |
    export PATH="$HOME/.local/share/fnm:$PATH"
    eval "$(fnm env)"
    fnm use --resolve-engines  # Uses .nvmrc
    node --version

Volta: Project-Pinned Versions in package.json

Volta takes a fundamentally different approach. Instead of switching the system Node.js version, it intercepts node, npm, yarn, and npx commands at the binary level and silently uses the version pinned in the nearest package.json. No .nvmrc file needed — versions live in package.json where everyone can see them.

Installation

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

# Windows
# Download installer from https://volta.sh

# No shell eval needed — Volta works via binary shimming in PATH

Project Setup — pin versions in package.json

# Pin Node.js version for this project
volta pin node@22
volta pin npm@10

# Or pin yarn
volta pin yarn@4

# This modifies package.json automatically:
{
  "name": "my-project",
  "version": "1.0.0",
  "volta": {
    "node": "22.14.0",
    "npm": "10.9.2"
  }
}
# Now EVERYONE who works in this directory automatically gets Node 22.14.0
# No .nvmrc, no `nvm use`, no manual setup
# Even in CI — if Volta is installed, it reads package.json and uses the right version

# Install a version without pinning
volta install node@20

# List installed versions
volta list

# Run command with specific version
volta run --node 18 node --version

How Volta's Binary Shims Work

# Volta creates shims in ~/.volta/bin/
which node    # → ~/.volta/bin/node (Volta shim, not the real binary)
which npm     # → ~/.volta/bin/npm (Volta shim)

# When you run `node`, Volta:
# 1. Looks for package.json with "volta.node" field in current + parent dirs
# 2. If found, transparently routes to that version
# 3. If not found, uses your global default
# This happens in milliseconds — no shell hooks, no delay

Global Tool Management

# Install global CLIs with a pinned Node version
volta install typescript
volta install vite
volta install prettier
volta install @anthropic-ai/claude-cli

# These tools are pinned to the Node version that was current when installed
# They always work, regardless of which project Node version is active

# Check what's installed globally
volta list

CI/CD with Volta

# GitHub Actions — Volta reads package.json automatically
- name: Install Volta
  run: curl https://get.volta.sh | bash

- name: Install dependencies
  run: |
    export VOLTA_HOME="$HOME/.volta"
    export PATH="$VOLTA_HOME/bin:$PATH"
    npm install  # Volta automatically uses the version from package.json

Feature Comparison

FeaturenvmfnmVolta
Written inShell scriptRustRust
Shell startup overhead~70ms~15ms~0ms (binary shim)
Version switch speed~200ms~10msInstant (shim)
Windows native❌ (WSL needed)
macOS
Linux
.nvmrc support✅ (read-only)
.node-version support
package.json pins
Auto-switch on cdManual setup--use-on-cd✅ Automatic
Global tool pinningVia npm -gVia npm -g✅ Native
Shell setup required✅ (eval)✅ (eval)❌ (PATH only)
GitHub stars80k19k12k

Performance Benchmarks

Shell startup time (zsh):
  No version manager: 85ms
  With nvm (active):  160ms  (+75ms)
  With fnm (active):  100ms  (+15ms)
  With Volta:          86ms  (+1ms) ← binary shim is transparent

Version switch time:
  nvm use 22:     180ms
  fnm use 22:      12ms
  volta pin node:  80ms (first time, writes package.json)
  volta run node: <1ms  (shim lookup, no switching)

Installation time for Node 22 (first install):
  nvm:  35 seconds
  fnm:  28 seconds
  volta: 25 seconds

When to Use Each

Choose fnm if:

  • You're currently using nvm and want zero migration effort — it's a drop-in replacement
  • Windows support is needed in your team
  • You use .nvmrc files in your projects already
  • Raw speed matters (scripts, CI)

Choose Volta if:

  • You work across multiple repos with different Node.js versions frequently
  • You want to enforce version consistency without relying on .nvmrc being present
  • Your team has "works on my machine" Node version issues
  • You use global CLI tools and want them to always work regardless of project version
  • You're in a monorepo where different packages need different Node versions

Keep nvm if:

  • Every tutorial you follow uses it and you don't want divergence from docs
  • Your .zshrc lazy-loading already mitigates the startup overhead
  • You rarely switch Node versions and the speed difference doesn't matter

Use mise (honorable mention) if:

  • You also manage other runtimes (Python, Ruby, Go) and want one tool for all of them
  • mise supports .nvmrc, .node-version, and its own .mise.toml format

Methodology

Shell startup measurements on Apple M2 MacBook Pro using time zsh -i -c exit, averaged over 10 runs. Version switch times measured with time nvm/fnm use 22 against locally installed versions. GitHub star counts as of February 2026. Windows support verified against official documentation and community reports. Download statistics from npm and direct install script access logs where available.


Related: Bun vs Deno 2 vs Node 22 for runtime comparisons, or tsx vs ts-node vs bun: Running TypeScript Directly for TypeScript execution options.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.