fnm vs nvm vs Volta: Node.js Version Managers in 2026
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
cdinto 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
| Feature | nvm | fnm | Volta |
|---|---|---|---|
| Written in | Shell script | Rust | Rust |
| Shell startup overhead | ~70ms | ~15ms | ~0ms (binary shim) |
| Version switch speed | ~200ms | ~10ms | Instant (shim) |
| Windows native | ❌ (WSL needed) | ✅ | ✅ |
| macOS | ✅ | ✅ | ✅ |
| Linux | ✅ | ✅ | ✅ |
| .nvmrc support | ✅ | ✅ | ✅ (read-only) |
| .node-version support | ❌ | ✅ | ✅ |
| package.json pins | ❌ | ❌ | ✅ |
| Auto-switch on cd | Manual setup | ✅ --use-on-cd | ✅ Automatic |
| Global tool pinning | Via npm -g | Via npm -g | ✅ Native |
| Shell setup required | ✅ (eval) | ✅ (eval) | ❌ (PATH only) |
| GitHub stars | 80k | 19k | 12k |
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
.nvmrcfiles 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
.nvmrcbeing 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
.zshrclazy-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.tomlformat
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.