Steven Van Bael

Freelance software developer

Profiling zsh startup time

I am a heavy tmux and zsh user and today I noticed that starting a new tmux pane started getting slow. Sometimes it took more than 1 second for my shell in the new pane to become interactive. The only thing a new pane is doing is starting up zsh, so zsh initialization was the likely culprit.

Measuring initial startup time

As with any optimization, before we start making changes we want to measure the current performance. The following command shows us how long it takes for zsh to startup and become interactive.

$ time  zsh -i -c exit
zsh -i -c exit  0.67s user 0.06s system 86% cpu 0.842 total

Enabling profiling

Zsh has a builtin profiler to profile startup time usage. It is called zprof and can be enabled by adding zmodload zsh/zprof to the top and zprof to the bottom of your ~/.zshrc.

Now when we start up zsh, it will start profiling, execute the rest of ~/.zshrc and print out the results:

$ zsh
num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1         305.98   305.98   50.52%    305.95   305.95   50.51%  nvm_die_on_prefix
 2)    1         463.42   463.42   76.51%     82.96    82.96   13.70%  nvm
 3)    1          79.69    79.69   13.16%     79.53    79.53   13.13%  __kubectl_bash_source
 4)    1          74.43    74.43   12.29%     74.43    74.43   12.29%  nvm_ensure_version_installed
 5)    3          30.63    10.21    5.06%     30.63    10.21    5.06%  compaudit
 6)    1          12.47    12.47    2.06%     12.32    12.32    2.03%  __kops_bash_source
 7)    1          20.58    20.58    3.40%      7.16     7.16    1.18%  compinit
 8)    2           3.33     1.66    0.55%      3.33     1.66    0.55%  env_default
 9)    2           3.24     1.62    0.53%      3.24     1.62    0.53%  grep-flag-available
10)    1         465.72   465.72   76.89%      2.30     2.30    0.38%  nvm_auto
11)    1           1.31     1.31    0.22%      1.31     1.31    0.22%  colors
12)   18           1.03     0.06    0.17%      1.03     0.06    0.17%  compdef
13)    1           0.73     0.73    0.12%      0.73     0.73    0.12%  nvm_supports_source_options
14)    1           0.27     0.27    0.04%      0.27     0.27    0.04%  is-at-least
15)    2           0.31     0.16    0.05%      0.16     0.08    0.03%  complete
16)    4           0.11     0.03    0.02%      0.11     0.03    0.02%  is_plugin
17)    3           0.11     0.04    0.02%      0.11     0.04    0.02%  nvm_has
18)    1           0.06     0.06    0.01%      0.06     0.06    0.01%  start_tmux
19)    2           0.05     0.03    0.01%      0.05     0.03    0.01%  bashcompinit
20)    1         466.49   466.49   77.02%      0.04     0.04    0.01%  nvm_process_parameters
...

These results show that almost 90% of startup time is taken by nvm (the Nodejs version manager) and bash completion initialization for kubectl and kops (2 kubernetes tools).

I commented out the nvm initialization as I don’t use Node anymore. I moved the Kubernetes bash completion initialization inside a kube() function that was already there for configuration my kubernetes credentials. This way the bash completions only load lazily when I configure my shell for a specific kubernetes cluster.

function kube() {
  if [ $commands[kubectl] ]; then
      source <(kubectl completion zsh)
  fi
  if [ $commands[kops] ]; then
    source <(kops completion --shell zsh)
  fi
  # omitted kubectl configuration code
}

Measuring results again

After making the changes I ran zsh with the profiler again:

$ zsh
num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    3          22.71     7.57   60.31%     22.71     7.57   60.31%  compaudit
 2)    1          17.40    17.40   46.20%      6.85     6.85   18.20%  compinit
 3)    2           3.14     1.57    8.34%      3.14     1.57    8.34%  grep-flag-available
 4)    2           2.73     1.36    7.24%      2.73     1.36    7.24%  env_default
 5)    1           0.96     0.96    2.55%      0.96     0.96    2.55%  colors
 6)   16           0.89     0.06    2.37%      0.89     0.06    2.37%  compdef
 7)    1           0.22     0.22    0.58%      0.22     0.22    0.58%  is-at-least
 8)    4           0.08     0.02    0.22%      0.08     0.02    0.22%  is_plugin
 9)    1           0.07     0.07    0.19%      0.07     0.07    0.19%  start_tmux

And with profiling disabled to measure the result of our optimizations

$ time  zsh -i -c exit
zsh -i -c exit  0.04s user 0.03s system 70% cpu 0.108 total

Now it starts up in 108ms instead of 842ms which is a noticeable improvement when opening new tmux panes.

The largest offenders now seem to be compaudit and compinit which need 22ms and 17ms to complete. I did a Google search to find solutions for improving these but I could not find anything that was worth the effort. For now +- 100ms startup time for Zsh is good enough and I’m going to call it a day.