Reducing ZSH startup time by 95%

24th Feb 2020

tl;dr: zsh took 3.5 seconds to start, now it takes 0.135 seconds. replace nvm’s bootstrap code with lazy-nvm.sh to load nvm when you need it, later.

determining the cause

Use zprof to benchmark zsh itself:

# put this at the top of your .zshrc
zmodload zsh/zprof
# and at the end of your .zshrc
zprof

It will output a table of where zsh time is being consumed:

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1         358.29   358.29   30.21%    358.15   358.15   30.19%  nvm_die_on_prefix
 2)    1         987.54   987.54   83.25%    286.34   286.34   24.14%  nvm_auto
 3)    2         701.19   350.60   59.11%    219.83   109.91   18.53%  nvm
 4)    1         122.96   122.96   10.37%    111.39   111.39    9.39%  nvm_ensure_version_installed
 5)    2          89.48    44.74    7.54%     89.48    44.74    7.54%  compaudit
 6)    1         120.47   120.47   10.16%     30.99    30.99    2.61%  compinit
 7)    1          72.17    72.17    6.08%     24.54    24.54    2.07%  zgen-init

That’s a lot of time spent on nvm! We use our terminal for more than just node, so lets see if we can trade waiting for our terminal against waiting for our first node command to start.

lazy-nvm.sh

The following script will invoke nvm only once you run your first node (or npm, npx, or nvm) command.

https://gist.github.com/grrowl/cec975ecfe690d13918f40ff5827fecb

function lazy_nvm {
  unset -f nvm
  unset -f npm
  unset -f node
  unset -f npx

  if [ -d "${HOME}/.nvm" ]; then
    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # linux
    [ -s "$(brew --prefix nvm)/nvm.sh" ] && source $(brew --prefix nvm)/nvm.sh # osx
  fi
}

# aliases
function nvm { lazy_nvm; nvm "$@"; }
function npm { lazy_nvm; npm "$@"; }
function node { lazy_nvm; node "$@"; }
function npx { lazy_nvm; npx "$@"; }

installation

Save lazy-nvm.sh to your home directory ~/, then update your .bashrc or .zshrc to replace the nvm bootstrap code with source lazy-web.sh

-if [ -d "${HOME}/.nvm" ]; then
-  export NVM_DIR="$HOME/.nvm"
-  [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
-  [ -s "$(brew --prefix nvm)/nvm.sh" ] && source $(brew --prefix nvm)/nvm.sh
-fi
+source lazy-nvm.sh

the results

before:

zsh startup time
3.631 seconds
node startup
0.436 seconds

zsh startup time
0.151 seconds
🎉 (24x improvement, 95% saving)
node initial startup
3.042 seconds
node subsequent startup
0.38 seconds

As you can see, we pretty clearly just traded zsh startup for a one-time (per shell) node startup.

benchmarks

The most important time is the last number, “total”

before:

# zsh startup time
repeat 5 {time zsh -i -c exit}
  1.93s user 1.68s system 95% cpu 3.766 total
  1.90s user 1.59s system 99% cpu 3.502 total
  1.90s user 1.62s system 97% cpu 3.626 total
  1.86s user 1.55s system 97% cpu 3.515 total
  1.93s user 1.67s system 96% cpu 3.745 total
# node startup time
repeat 5 {time (node -e "process.exit()")}
  0.04s user 0.01s system 98% cpu 0.044 total
  0.03s user 0.01s system 96% cpu 0.040 total
  0.03s user 0.01s system 94% cpu 0.045 total
  0.03s user 0.01s system 95% cpu 0.046 total
  0.03s user 0.01s system 96% cpu 0.043 total

after:

# zsh startup time (cold)
repeat 5 {time (zsh -i -c exit)}
  0.09s user 0.05s system 95% cpu 0.145 total
  0.08s user 0.05s system 95% cpu 0.136 total
  0.08s user 0.05s system 96% cpu 0.137 total
  0.08s user 0.05s system 96% cpu 0.134 total
  0.10s user 0.07s system 84% cpu 0.202 total

since we’re using a subshell to benchmark, these are the “cold” node startup times

# node startup time (cold)
repeat 5 {time (node -e "process.exit()")}
  1.65s user 1.69s system 92% cpu 3.613 total
  1.59s user 1.30s system 99% cpu 2.900 total
  1.56s user 1.27s system 99% cpu 2.852 total
  1.60s user 1.32s system 99% cpu 2.925 total
  1.60s user 1.30s system 99% cpu 2.918 total

after the first time you call node in a terminal, the overhead is comparable as before.

# node startup time (warm)
repeat 5 {time node -e "process.exit()"}
  0.03s user 0.01s system 93% cpu 0.040 total
  0.03s user 0.01s system 97% cpu 0.038 total
  0.03s user 0.01s system 97% cpu 0.037 total
  0.03s user 0.01s system 97% cpu 0.039 total
  0.03s user 0.01s system 97% cpu 0.038 total