Christian Gill

Debugging Zsh init times

I recently moved from Ubuntu to MacOS and in the process I made several changes to my dotfiles to make them work. But I wasn't happy with the result, specially because zsh initialization time ended up being 3-4s. That doesn't seem like much but for someone that works mostly on the terminal (alacritty, tmux & vim) is a deal breaker.

So I decided to look into the problem and find a way to optimize my zsh init time.

It makes sense to describe a bit what I had in my dotfiles.

  • Antibody as the plugin manager
  • Several oh-my-zsh plugins, most of which I wasn't using
  • A custom zsh plugin I built to have directory based aliases. I use it mostly to have project related commands (which are usually the same command with a few different tweaks: e.g. different projects require spinning up a server in different ways but they all have the same "run-server" alias)
  • zsh syntax highlighting
  • zsh suggestions
  • purs as the prompt, which is quite fast

The first attempt was to remove some stuff from my Antibody plugin list. But sadly it didn't improve init speed that much. I didn't know yet how slow my dir_alias plugin was.

After googling for a bit I found out these two articles which basically had all I needed to optimize zsh init time properly.

Millennial developer discovers debugging and profiling

I found very useful to be able to debug and profile zsh instead of guessing what could be taking so much time.

Check zsh init time N times

$ repeat 10 {time zsh -i -c exit}

This is useful to find the mean time your zsh takes to initialize. As I said, in my case it was around 3-4s 🐢

Debug mode

Prints everything that runs during initialization. This is helpful to see what sort of things are part of your zsh init. I didn't make much use of it but can be handy if you want to make further, more granular improvements.

$ zsh -i -v

Profiling

This was by far the more useful thing I learned about zsh.

Add this line to the top of your ~/.zshrc:

zmodload zsh/zprof

And this line to the end, after anything else:

zprof

Or if you want to play around with the output dump it in a file:

zprof > ~/zsh-profiling.txt

This will output a table with the top ten slowest commands run during initialization, sorted by the time they took.

num  calls                time                       self                name
-----------------------------------------------------------------------------------
 1)    3        10.15    10.15   15.22%     10.15    10.15   15.22%   command_name
 ...

The slow ones

After profiling I found out that my dir_alias plugin was quite slow. It uses a zsh hook to run whenever a directory changes, it checks if the current directory or any parent one contain .aliasfile, sets the aliases from them and also cleans up the ones from the previous directory. Which turns out to be slow when done in bash. So, good by friend 👋

But compaudit & compinit were the slowest ones in my case. They are used to setup completion. Which is something I was not ready to give up. But there's something that can be done. compinit reads ~/.zcompdump every time, which can be changed to only check it once a day.

# Take from:  https://gist.github.com/ctechols/ca1035271ad134841284
# Adapted by: https://carlosbecker.com/posts/speeding-up-zsh/

autoload -Uz compinit
if [ $(date +'%j') != $(stat -f '%Sm' -t '%j' ~/.zcompdump) ]; then
  compinit
else
  compinit -C
fi

These were the two changes that made the most improvements. Besides that I removed a few other plugins that wasn't using, added a function to lazy load some programs that I rarely need and tidied up a bit my zsh.

Now my init time is around 170-180ms. That's something I can live with 🏎️🎉

References

Most of the things I share here are taken from these two articles: