Skip to content

[Perf] nvm use: skip re-resolving an exact, installed version - #3937

Merged
ljharb merged 2 commits into
nvm-sh:masterfrom
hridhinvarkeyjoseph:perf/use-exact-version-fast-path
Oct 8, 2026
Merged

ljharb merged 2 commits into
nvm-sh:masterfrom
hridhinvarkeyjoseph:perf/use-exact-version-fast-path

Conversation

@hridhinvarkeyjoseph

Copy link
Copy Markdown
Contributor

Fixes #3930.

Problem

On every new shell, nvm.sh calls nvm_auto use. With no node on PATH yet, nvm_auto resolves the default alias to an exact installed version (e.g. v24.21.0) and then calls:

nvm use --silent v24.21.0

nvm use then resolves that already-exact version from scratch, twice:

  1. VERSION="$(nvm_match_version "${PROVIDED_VERSION}")" → nvm_version
  2. nvm_ensure_version_installed "${VERSION}" ... → nvm_version again, then nvm_is_version_installed

Each nvm_version call does alias resolution plus nvm_ls, forking several subshells. The existing code already flagged this spot:

# This nvm_ensure_version_installed call can be a performance bottleneck
# on shell startup. Perhaps we can optimize it away or make it faster.

Profiling (master, clean env -i environment, avg of 10 calls)

call time
nvm use --silent v24.21.0 168ms
nvm_ensure_version_installed v24.21.0 57ms
nvm_version v24.21.0 48ms

About 105ms of the 168ms is spent re-deriving a version that is already exact. Changing my default alias from 24 to v24.21.0 made no difference (~290ms either way), so exact versions go through the full resolution path too.

Change

Adds nvm_is_installed_exact_version, a cheap check that returns success only when the argument:

  • is exactly vX.Y.Z (digits and dots only, so v12, v12.0, v12.0.0-rc.1, node, lts/*, iojs-v1.0.0, system, etc. do not qualify),
  • is not shadowed by an alias of the same name (aliases take precedence in nvm_version, so this preserves that), and
  • is installed, using the existing nvm_is_version_installed (executable bin/node, or node.exe on Windows; respects the old directory layout via nvm_version_path).

When it passes, nvm use sets VERSION directly and skips nvm_match_version and nvm_ensure_version_installed. Everything after that is unchanged: PATH/MANPATH, NVM_BIN/NVM_INC, hash -r, the current symlink, nvm_die_on_prefix, --save, and output.

All other inputs (partial versions, aliases, lts/*, --lts, system, iojs, .nvmrc, and versions that aren't installed) go through the existing code path, so their behavior and error messages are identical.

Results

Sourcing nvm.sh in a clean environment (env -i), macOS arm64, zsh, one installed Node version, default alias = 24 (avg of 4 warm runs):

startup
master ~330ms
this PR ~235ms

That's roughly 95ms (~29%) saved on every new shell. Users with more installed versions or slower fork should see a larger absolute saving, since the skipped code calls nvm_ls.

Tests

  • New: test/fast/Unit tests/nvm_is_installed_exact_version covers: no argument; exact version not installed; non-executable node binary; installed exact version; a range of non-exact inputs (12.0.0, 12, v12, v12.0, v12.0.0-rc.1, node, stable, lts/*, iojs-v1.0.0, system, default); and an alias named v12.0.0 taking precedence over the fast path.
  • Existing: nvm_is_version_installed, nvm_ensure_version_installed, and all Running 'nvm use …' fast tests pass in sh, bash, and zsh.
  • Running 'nvm use system' should work as expected fails on my machine, but fails identically on unmodified master (I have no system node installed), so it's unrelated to this change.
  • I couldn't verify ksh locally: macOS ships ksh93, which has no local, so the existing tests fail there as well. CI should cover it.

Manual check:

$ nvm use v24.21.0   # fast path
Now using node v24.21.0 (npm v11.19.0)
$ nvm use 24         # existing path
Now using node v24.21.0 (npm v11.19.0)
$ nvm use v99.0.0    # not installed: existing path, same error, exit 3
N/A: version "v99.0.0" is not yet installed.

When `nvm use` is given an exact `vX.Y.Z` that is already installed,
and no alias with that exact name exists,
use it directly instead of re-resolving it through
`nvm_match_version` and `nvm_ensure_version_installed`,
both of which call `nvm_version`.

`nvm_auto` takes this path on every shell startup with an already-resolved version,
so this removes ~95ms from each new shell
(~330ms -> ~235ms in a clean environment on macOS/zsh).

Fixes nvm-sh#3930
@hridhinvarkeyjoseph
hridhinvarkeyjoseph force-pushed the perf/use-exact-version-fast-path branch 2 times, most recently from 5a60628 to c96b0d7 Compare October 1, 2026 09:52
@hridhinvarkeyjoseph

hridhinvarkeyjoseph commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the bash-only failure: the new helper needed to be added to nvm unload's function list. All fast bash jobs now pass.

The one remaining failure (WSL nvm install (Ubuntu-24.04, 16)) timed out during wsl --install, before any nvm code ran, and the matching (Ubuntu-24.04, 16, script) job passed, so it looks like a runner flake. Could it be re-run when convenient?

@hridhinvarkeyjoseph

Copy link
Copy Markdown
Contributor Author

Folded in the change from #3945 (skip nvm_version in nvm_resolve_local_alias for an exact, installed version), since both are the same fast path on the startup route — one PR to review instead of two.

@ljharb
ljharb force-pushed the perf/use-exact-version-fast-path branch from 9fca02a to 6df817b Compare October 7, 2026 23:27

@ljharb ljharb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tweaked the second commit to use the helper the first commit introduced.

@ljharb
ljharb force-pushed the perf/use-exact-version-fast-path branch from 6df817b to 913b8cb Compare October 8, 2026 18:14
@ljharb
ljharb merged commit 913b8cb into nvm-sh:master Oct 8, 2026
407 of 410 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nvm's shell hook adds ~250ms to every new zsh shell startup

2 participants