Lazy-load nvm to Reduce ZSH's Startup Time

Inspired by articles by Chai Phonbopit on ทำไม ZSH ของเราช้าจัง? + ทำให้เร็วขึ้นได้มั้ย and by Matthew J. Clemente on Speeding Up My Shell (Oh My Zsh), I can make my zsh terminal loads faster by lazy-loading nvm.

(Normally I’m not bothered by my zsh’s startup time because I don’t open new terminal windows/tabs a whole lot in a day. But if I can make things a bit faster, then why not get some marginal gains?.)

1. Measure the current startup time

zsh’s startup can be measured by running a script:

$ for i in $(seq 1 10); do /usr/bin/time zsh -i -c exit; done

# or using $SHELL - should work with bash
$ for i in $(seq 1 10); do /usr/bin/time $SHELL -i -c exit; done

(source)

  • seq prints sequences of numbers. We use it with for .. in loop to run the commands 10 times
  • time is a utility command to execute a command and then print out time used
  • -i running zsh in interactive mode - meaning we can run (or pass) commands to it to execute
  • -c tells zsh to take the next part which is exit as a command to execute, not as a parameter

My zsh startup time before optimize was ~1.1-1.2 seconds.

2. Analyze my .zshrc file

I’m using zsh with oh-my-zsh. Whatever goes into .zshrc file can add more startup time to zsh.

I don’t have much in my .zshrc file. It looks like this:

# (1)
. /Users/armno/code/z/z.sh

# (2)
BASE16_SHELL="$HOME/.config/base16-shell/"
[ -n "$PS1" ] && \
    [ -s "$BASE16_SHELL/profile_helper.sh" ] && \
        eval "$("$BASE16_SHELL/profile_helper.sh")"

# (3)
export ZSH="/Users/armno/.oh-my-zsh"
ZSH_THEME="cloud-armno"
DISABLE_UPDATE_PROMPT="true"
plugins=(zsh-completions zsh-autosuggestions zsh-syntax-highlighting)
source $ZSH/oh-my-zsh.sh

source ~/.aliases
source ~/.functions

export LC_ALL=en_US.UTF-8
export GPG_TTY=$(tty)

export PATH="/usr/local/opt/ruby/bin:$PATH"
export PATH="/usr/local/sbin:$PATH"

# (4)
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
  1. Loads z
  2. Loads base16_shell
  3. Initialize oh-my-zsh and its plugins
  4. Loads nvm

The biggest bottleneck seems to be nvm’s scripts, as mentioned by many people.

3. Disable nvm’s scripts entirely

I comment out the last 2 lines from my .zshrc

# export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
# [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

open a new termnial tab, and re-run the profiling script. With this alone, zsh’s startup time goes down from ~1.2s to ~0.17s.

This is now a lot faster, but it also means I will not be able to use nvm.

I still need nvm, sometimes.

If you’re not using nvm, I would suggest comment out some scripts/plugins from your .zshrc or .bashrc file, re-run profiling script, and repeat. It should help to see which scripts are slowing down the startup time.

4. Lazy-load nvm

Lazy-loading nvm makes nvm available only when it is needed: when running nvm, node, npm or gloablly installed command for the first time, and not right away then the shell starts.

It can be done by using zsh-nvm plugin.

First, I install the plugin.

$ git clone https://github.com/lukechilds/zsh-nvm $ZSH/custom/plugins/zsh-nvm

Then in .zshrc file, set NVM_LAZY_LOAD environment variable to true and add zsh-nvm to plugins=() list.

export NVM_LAZY_LOAD=true

plugins=(... zsh-nvm)

(note: setting NVM_LAZY_LOAD variable must be placed before the plugins= line in .zshrc file.)

Then I source the .zshrc file so my changes take effect.

5. Measure again

Running the same script from 1. again, zsh’s startup time is now at ~0.2 seconds.

Summary

  • I use the one-liner script to measure zsh’s startup time
  • Instead of using nvm’s official install script, I use zsh-nvm plugin to load nvm
  • I set NVM_LAZY_LOAD environment in my .zshrc file to true
  • My zsh’s startup time reduces from ~1.2 seconds to ~0.2 seconds.

Related Posts