Decreasing my zsh startup time

Sometimes I noticed a new shell's startup being really slow. This annoyed me from time to time, so today I decided to try some optimizations.

Measure

I measured my startup time in a rather crude way. I'm loading my zsh configuration files like in the following snippet:

# Source all .zsh files in my dotfiles
for config_file ($HOME/.dotfiles/**/*.zsh); do
  source $config_file
done

I relied on subjective analysis and changed it to the following code:

# Source all .zsh files in my dotfiles
for config_file ($HOME/.dotfiles/**/*.zsh); do
  echo $config_file
  source $config_file
  read throwaway\?"Press return for next file"
done

What this does is print the path to the configuration file, load it and then wait for a return key press to load the next file.

With this, I could start a new shell and see which files are slow to load.

Remove brew --prefix

From the measurements I noticed that the slow files were those where I load some tools via homebrew. I load for example grc and hub that way. I used this approach for my PATH as well with PHP and the Android SDK.

I then timed an execution of brew --prefix hub and got the following result:

$ time brew --prefix hub
/usr/local/opt/hub
brew --prefix hub  0.12s user 0.03s system 98% cpu 0.145 total

It takes around 0.15 seconds to load the path prefix for hub! If you do that a couple of times in your zsh configuration, it sums up to a lot of time.

I then changed all the occurences to the absolute path, as it's mostly just /usr/local/opt and the package name. It's not as flexible anymore, but that's a small price for a faster shell startup time.

Change rbenv initialization

The one other file that loaded slowly was my rbenv initialization. It calls the following snippet:

if which rbenv > /dev/null; then
  eval "$(rbenv init -)"
fi

A bit of googling revealed the --no-rehash option which prevents an expensive rehashing of ruby and rubygems executables when loading rbenv. You can always rehash later with rbenv rehash.

So I measured it again, to be sure it helps.

$ { time ( eval "$(rbenv init -)" ) } 2>&1
( eval "$(rbenv init -)"; )  0.20s user 0.06s system 99% cpu 0.262 total
$ { time ( eval "$(rbenv init - --no-rehash)" ) } 2>&1
( eval "$(rbenv init - --no-rehash)"; )  0.03s user 0.03s system 105% cpu 0.060 total

As you can see, --no-rehash sped it up quite a bit.
As a side-effect, I learned how to measure code blocks in zsh ;-)

Results

With these changes, I decreased my zsh startup time from 1.5s to 0.6s.

It's nice to see that two small changes can speed up an application I use every day.
For more information, you can see my full zsh configuration (along with my other dotfiles) on GitHub.