Better NVM Lazy Loading


tl;dr:

export NVM_DIR="$HOME/.nvm"

# This lazy loads nvm
nvm() {
  unset -f nvm
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use # This loads nvm
  nvm $@
}

# This loads nvm bash_completion
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

# This resolves the default node version
DEFAULT_NODE_VER="$((cat "$NVM_DIR/alias/default" || cat ~/.nvmrc) 2> /dev/null)"
while [ -s "$NVM_DIR/alias/$DEFAULT_NODE_VER" ] && [ ! -z "$DEFAULT_NODE_VER" ]; do
  DEFAULT_NODE_VER="$(cat "$NVM_DIR/alias/$DEFAULT_NODE_VER")"
done

# This resolves the path to the default node version
DEFAULT_NODE_VER_PATH="$(find $NVM_DIR/versions/node -maxdepth 1 -name "v${DEFAULT_NODE_VER#v}*" | sort -rV | head -n 1)"

# This adds the default node version path to PATH
if [ ! -z "$DEFAULT_NODE_VER_PATH" ]; then
  export PATH="$DEFAULT_NODE_VER_PATH/bin:$PATH"
fi

Lazy loaded NVM that behaves as expected without unnecessary complexity or hacks.

  • All binaries - including globally installed npm packages - are available before loading NVM.
  • NVM is not loaded until the nvm command is used.
  • Supports the default alias and ~/.nvmrc.
  • Supports alias chains such as default -> lts/erbium -> v12.14.1 and partial versions such as v12 -> 12.14.1.
  • Simple.

Caveats:

  • Does not support NVM internal aliases such as stable.

NVM can add a long delay to your shell startup. Especially in environments like WSL. The solution is to lazy load it. However, the node, npm, npx, and any globally installed npm binaries such as gulp, eslint, etc. are not available until NVM is loaded.

The existing popular solutions fall short for a couple reasons. The top results from an "npm lazy load" Google search suffer some or all of these issues:

  • Whitelists binaries that can be used before loading NVM by creating an alias for each one.
    • This is cumbersome to maintain as it requires you to create a new alias for every global npm package you install.
  • Requires loading NVM before any of the binaries can be executed.
    • This just kicks the slow startup problem to the first execution of node, npm, etc. It is also unnecessary as NVM is not required to execute the binaries.
  • Does not support aliases such as lts/erbium as the default.
    • Requires you to use a specific version as the default.
  • Hardcodes a specific version of node.
    • Again, cumbersome to maintain as it is disconnected from the default alias and ~/.nvmrc. This requires updating whenever you change your default version or behavior will be inconstant with NVM.
  • Uses hacky methods of discovering binaries and other unnecessary complexity.

Lazy load NVM

For reference, here is the boilerplate script NVM installs:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

Let's lazy load instead:

export NVM_DIR="$HOME/.nvm"

# This lazy loads nvm
nvm() {
  unset -f nvm
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use # This loads nvm
  nvm $@
}

# This loads nvm bash_completion
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

As you can see, the lines for setting NVM_DIR and sourcing bash_completion remain unchanged. We move the code for loading NVM into a function and add the --no-use flag. The function unsets itself, loads NVM, and executes nvm. This way NVM is not loaded until the nvm command is actually used.

  • The --no-use flag prevents NVM from automatically executing nvm use after loading which is unnecessary and can be slow.

Make binaries accessible

All the binaries are already on your drive ready to go. There is no reason you must load NVM before executing them. All you need to do is include them in PATH (which is exactly what NVM does).

export PATH="$NVM_DIR/versions/node/v12.14.1/bin:$PATH"

Now we can access all the v12.14.1 binaries without having to load NVM. This includes globally installed npm package binaries.

If you don't mind the version being hardcoded, you can stop here.

Use the default version

We don't want to hardcode a version. Instead, we want to mirror NVM's behavior and use either the default alias or ~/.nvmrc:

DEFAULT_NODE_VER="$((cat "$NVM_DIR/alias/default" || cat ~/.nvmrc) 2> /dev/null)"

First we try using the default alias. If this fails we try using ~/.nvmrc. NVM stores aliases as files and we get the version from its contents just like .nvmrc files.

Support aliases

It is possible to set the default to an alias (which could, in turn, reference another alias). For example default -> myalias -> lts/erbium -> v12.14.1. Let's follow the chain of possible aliases to get the root version:

while [ -s "$NVM_DIR/alias/$DEFAULT_NODE_VER" ] && [ ! -z "$DEFAULT_NODE_VER" ]; do
  DEFAULT_NODE_VER="$(cat "$NVM_DIR/alias/$DEFAULT_NODE_VER")"
done

If alias/$DEFAULT_NODE_VER exists, we know we have an alias. Set DEFAULT_NODE_VER to the contents of the alias and repeat. When it does not exist, assume we have reached the root version and stop.

  • [ -s ... ] checks if the alias exists.
  • [ ! -z ... ] checks if we hit a dead-end (and prevents an infinite loop).

Support partial versions

It is also possible to set a partial version. For example v12 -> v12.14.1. Let's resolve by matching the newest version with the given version as the prefix:

# This resolves the path to the default node version
DEFAULT_NODE_VER_PATH="$(find $NVM_DIR/versions/node -maxdepth 1 -name "v${DEFAULT_NODE_VER#v}*" | sort -rV | head -n 1)"
  • v${...#v} ensures the version is always prefixed with exactly 1 v as both 12.14.1 and v12.14.1 are possible.
  • sort -rV order matches versions newest to oldest.
  • head -n 1 keep the newest matched version.

Caveats

This is a poor-man's version of NVM's alias resolution and as such does not support NVM internal aliases such as stable. NVM does not store these aliases in files and instead calculates these on-the-fly.

Installation

Put it all together and you get the script at the top of this page. Replace the default script NVM added to your .bashrc/.zshrc/etc. Be sure to compare the boilerplate lines for setting NVM_DIR, bash_completion, and nvm.sh as these may change in future versions.