Skip to main content

fnm vs nvm vs Volta: Node.js Versions 2026

·PkgPulse Team
0

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

Ecosystem & Community

All three tools have stable, well-maintained communities in 2026. nvm's 80K GitHub stars reflect its status as the default for much of the industry — virtually every Node.js tutorial, framework quickstart, and cloud platform guide assumes nvm is installed. This ubiquity is both a strength and a liability: it's the safest choice for documentation compatibility, but its shell script architecture means the performance limitations are baked in.

fnm's community is more focused on developer experience. The GitHub issues are primarily feature requests and quality-of-life improvements rather than bug reports. The Windows support story is a clear differentiator — teams that work cross-platform consistently choose fnm over nvm specifically because the .nvmrc support works identically on all platforms without needing WSL.

Volta has the strongest enterprise adoption story. It's the version manager of choice at companies that manage large numbers of JavaScript repositories and need consistency guarantees. The package.json integration is particularly valuable in organizations where different teams manage different repositories — version pinning in package.json is version-controlled and code-reviewed, making it a governance tool as much as a developer convenience.

Real-World Adoption

nvm is installed on the majority of developer machines that run Node.js, simply because it predates the alternatives by years. The installed base is enormous, and most developers see no compelling reason to migrate away when it "works fine."

fnm has become the default recommendation in newer tutorials and guides focused on developer experience. Vercel's documentation for Next.js projects now suggests fnm as the primary Node.js version manager. Teams onboarding Windows developers without requiring WSL have largely standardized on fnm.

Volta is the version manager of choice at companies like Atlassian and various large enterprises that manage JavaScript at scale. Its ability to pin both the Node.js version and the package manager version in a single package.json entry is uniquely valuable for ensuring reproducible builds across large developer teams.

When combined with the right test runner, version consistency pays dividends — Bun Test vs Node Test vs Vitest zero-config 2026 covers how each runner interacts with different Node.js versions and runtimes.

Migration Guide

Migrating from nvm to fnm is deliberately zero-friction. Install fnm, replace eval "$(nvm.sh)" in your shell config with eval "$(fnm env --use-on-cd)", and run fnm install to install the versions you use. Your existing .nvmrc files work unchanged. The command names are almost identical — fnm install instead of nvm install, fnm use instead of nvm use.

Migrating to Volta requires a different mental model shift. You're not replacing nvm's commands — you're changing how version management works. The key migration step is running volta pin node in each of your projects to add the version pins to package.json. Once done, you can remove .nvmrc files from those projects or leave them as fallbacks for developers not using Volta.

Common pitfall with Volta: global tools installed via npm after switching to Volta may have version conflicts. The fix is to reinstall global tools via volta install rather than npm install -g to ensure they get properly shimmed. Teams that discover this gotcha after switching often wish they had read the "global tool management" section of Volta's docs before migrating.

Final Verdict 2026

fnm is the pragmatic upgrade for nvm users. Install it, replace one line in your shell config, and enjoy 10x faster version switching with zero workflow changes. For teams with Windows developers, it's not just faster — it's the difference between a working workflow and one that requires WSL as a prerequisite.

Volta is the right choice when you need hard guarantees about Node.js versions in production codebases. The package.json integration makes version requirements explicit, version-controlled, and automatically enforced without any developer action. For teams managing dozens of repositories with different Node.js requirements, this enforcement mechanism is worth the migration effort.

nvm remains completely valid if you're satisfied with its performance and your workflow doesn't include Windows developers. The 80K stars and years of documentation aren't going anywhere.

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.

For broader tooling context, see best JavaScript package managers 2026 and best monorepo tools 2026.

Team Standardization

Node version managers work best when the entire team uses the same tool. Mixing fnm and nvm on the same project creates invisible friction — .nvmrc files that not everyone respects, version mismatch errors that only appear on certain machines, and CI configurations that differ from local environments.

The .nvmrc / .node-version file is the shared contract. Both fnm and nvm read .nvmrc. Volta reads package.json's engines.node field but also supports .nvmrc. Set one of these files at the repo root and enforce it in CI by running the version manager's install command as part of setup.

CI consistency matters more than local speed. Most CI systems (GitHub Actions, CircleCI, GitLab CI) pin Node versions via actions/setup-node or system package managers, not version managers. Make sure your CI Node version matches what's in your .nvmrc. A team can have fast local installs with fnm and deterministic CI setup with actions/setup-node — they're not mutually exclusive.

Windows support is the tiebreaker for many teams. fnm supports Windows natively. nvm requires nvm-windows, which is a separate project with different behavior. If any team member is on Windows, fnm is the safer choice for guaranteed consistent behavior. Volta also works cross-platform and adds the package manager pinning feature if you need consistent npm/pnpm/yarn versions too.

The practical recommendation: migrate to fnm for new projects; use Volta if you want package.json-driven version pinning across Node, npm, and yarn as a single source of truth.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.