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.631seconds- node startup
0.436seconds
- zsh startup time
0.151seconds- 🎉 (24x improvement, 95% saving)
- node initial startup
3.042seconds- node subsequent startup
0.38seconds
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