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