Mike Kasberg

Husband. Father. Software engineer. Ubuntu Linux user.

Image for Optimizing Zsh Init with ZProf (and Switching to Mise)

Optimizing Zsh Init with ZProf (and Switching to Mise)

29 May 2025

I’ve been noticing for a while that on my personal laptop, new terminal tabs/windows were very slow to start (more than a second). It bugged me a lot since I knew something seemed wrong, but I’d been putting off digging into the problem for a long time. A few days ago, I finally took some time to investigate! I’m glad I did, because I found some low hanging fruit that ended up saving more than two seconds on zsh (Oh-My-Zsh) init!

ZProf

I knew zsh was slow to start up, but I didn’t know why. Fortunately, there’s a tool that can easily profile this! Zsh comes with a tool called zprof that can profile zsh startup. To use it, you just add zmodload zsh/zprof as the first line in your ~/.zshrc (to cause the following lines to be profiled), and zprof as the last line in your ~/.zshrc (to print the profile results). With those lines in ~/.zshrc, opening a new terminal will print profiling information! Here’s the important bits of what my first profile showed.

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1        1693.34  1693.34   55.59%   1693.34  1693.34   55.59%  load-nvmrc
 2)    2         587.73   293.87   19.29%    286.15   143.07    9.39%  nvm
 3)    1         264.29   264.29    8.68%    264.29   264.29    8.68%  compdump
 4)    1         238.42   238.42    7.83%    207.30   207.30    6.80%  nvm_ensure_version_installed
 5)    2         692.22   346.11   22.72%    182.89    91.44    6.00%  compinit
 6)  845         176.20     0.21    5.78%    176.20     0.21    5.78%  compdef
 7)    4          70.58    17.65    2.32%     70.58    17.65    2.32%  compaudit
 ...

There’s an obvious optimization here. load-nvmrc is responsible for more than half of my zsh init time, taking more than 1.5 seconds! There might have been some kind of misconfiguration or reason for the poor nvm performance. (I see 2 calls to nvm, which seems wrong.) But I actually didn’t bother to troubleshoot nvm any further. I’d been interested in trying mise-en-place for a while, and this seemed like a great excuse to give it a go (since it can replace nvm). I ripped out the nvm Oh-My-Zsh plugin and saw immediate improvement in my init time!

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    2         499.94   249.97   40.72%    499.94   249.97   40.72%  compdump
 2) 1674         327.03     0.20   26.63%    327.03     0.20   26.63%  compdef
 3)    2        1193.22   596.61   97.18%    300.31   150.15   24.46%  compinit
 4)    4          67.30    16.83    5.48%     67.30    16.83    5.48%  compaudit
 5)    1          19.11    19.11    1.56%     19.11    19.11    1.56%  is_update_available
 ...

-----------------------------------------------------------------------------------

 3)    2        1193.22   596.61   97.18%    300.31   150.15   24.46%  compinit
       2/4        67.30    33.65    5.48%      0.78     0.39             compaudit [4]
    1662/1674    325.67     0.20   26.52%    325.67     0.20             compdef [2]
       2/2       499.94   249.97   40.72%    499.94   249.97             compdump [1]

-----------------------------------------------------------------------------------

       2/2       499.94   249.97   40.72%    499.94   249.97             compinit [3]
 1)    2         499.94   249.97   40.72%    499.94   249.97   40.72%  compdump

-----------------------------------------------------------------------------------

    1662/1674    325.67     0.20   26.52%    325.67     0.20             compinit [3]
 2) 1674         327.03     0.20   26.63%    327.03     0.20   26.63%  compdef

-----------------------------------------------------------------------------------

       2/4        67.30    33.65    5.48%      0.78     0.39             compinit [3]
       2/4        66.52    33.26    5.42%     66.52    33.26             compaudit [4]
 4)    4          67.30    16.83    5.48%     67.30    16.83    5.48%  compaudit
       2/4        66.52    33.26    5.42%     66.52    33.26             compaudit [4]

But something still seemed off. Although I’d saved more than 1.5 seconds of init time by ripping out nvm, nearly 0.5 seconds still felt abnormally slow to initialize a shell. I didn’t know what compdump was, but I decided to find out! LLMs are excellent for helping to debug things like this. I’ve found that they particularly excel at providing detailed explanations of problems from relatively short error or trace output – particularly for common problems in open source tooling that are well-documented. I pasted the above output into Gemini 2.5 Pro and asked it to interpret the zprof output and provide recommendations to improve my init time.

Gemini noted (correctly) that there were two calls to compinit, and that this function should normally only be called once during zsh init. It also let me know that Oh-My-Zsh calls compinit for me when sourcing $ZSH/oh-my-zsh.sh, and recommended that I make sure I’m not calling compinit elsewhere in my ~/.zshrc. As it turns out, I was calling compinit elsewhere in my ~/.zshrc. A long time ago, I’d pasted a few lines that added some completions to my fpath and called compinit. I could avoid calling compinit twice by just modifying the fpath before sourcing oh-my-zsh.sh! I edited my ~/.zshrc to do that, and saved another 0.25 seconds by removing the duplicate call!

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1         250.01   250.01   42.55%    250.01   250.01   42.55%  compdump
 2)  843         146.91     0.17   25.00%    146.91     0.17   25.00%  compdef
 3)    1         552.65   552.65   94.05%    137.29   137.29   23.36%  compinit
 ...

-----------------------------------------------------------------------------------

 3)    1         552.65   552.65   94.05%    137.29   137.29   23.36%  compinit
       1/2        19.79    19.79    3.37%      0.28     0.28             compaudit [5]
     831/843     145.57     0.18   24.77%    145.57     0.18             compdef [2]
       1/1       250.01   250.01   42.55%    250.01   250.01             compdump [1]

-----------------------------------------------------------------------------------

       1/1       250.01   250.01   42.55%    250.01   250.01             compinit [3]
 1)    1         250.01   250.01   42.55%    250.01   250.01   42.55%  compdump

-----------------------------------------------------------------------------------

     831/843     145.57     0.18   24.77%    145.57     0.18             compinit [3]
 2)  843         146.91     0.17   25.00%    146.91     0.17   25.00%  compdef

This is progress! I’d already saved 1.75 seconds of init time, but I was hungry to keep going since all the fixes so far had been easy and obvious (in hindsight). Still in the Gemini session, I asked the LLM to analyze my new profile and explain what compdump and compinit do. Gemini explained that compdump and compinit are used to set up completions, but it also noted that compdump is used to build a cache file, which shouldn’t be slow every time (even though it was for me). It suggested to try removing the cache files at ~/.zcompdump* to see if compdump is then faster after rebuilding the cache once.

Before removing ~/.zcompdump*, I looked at the files I was about to remove, and noticed the reason why zsh was trying to rebuild the compdump cache each time!

$ ls -l .zcompdump*                                
...
-r--r--r-- 1 mkasberg mkasberg 122128 Dec 28  2022 .zcompdump-mkasberg-latitude-e7450-5.9.zwc
...

Apparently, something I’d done in the distant past (probably due to a laptop migration) had triggered the creation of this compdump file that was readonly, and the existence of this readonly compdump file caused compdump to silently fail, or at least to retry on every shell init rather than only when something had changed. I deleted all the ~/.zcompdump* files, including the readonly one (since they’re just cache files that can be easily regenerated anyway), and after that the zsh init process generated a single cache file on the first run and then stopped regenerating them! This fix shaved an additional 200ms off my zsh init time, and we’re in very good territory now! In total, I shaved nearly 2 seconds off the startup time for opening a new terminal window or tab!!!

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1          47.03    47.03   56.59%     25.72    25.72   30.94%  compinit
 2)    2          21.31    10.66   25.65%     21.31    10.66   25.65%  compaudit
 3)    1          20.19    20.19   24.30%     20.19    20.19   24.30%  is_update_available
 4)    1          27.75    27.75   33.39%      7.56     7.56    9.10%  (anon) [/home/mkasberg/.oh-my-zsh/tools/check_for_upgrade.sh:126]
 5)    1           2.05     2.05    2.47%      2.05     2.05    2.47%  detect-clipboard
 6)    1           1.56     1.56    1.88%      1.56     1.56    1.88%  colors
 7)   12           1.47     0.12    1.76%      1.47     0.12    1.76%  compdef
 8)    3           1.13     0.38    1.36%      1.13     0.38    1.36%  is-at-least
 9)    2           0.86     0.43    1.04%      0.86     0.43    1.04%  add-zsh-hook
10)    1           0.78     0.78    0.94%      0.78     0.78    0.94%  regexp-replace
11)   10           0.29     0.03    0.35%      0.29     0.03    0.35%  is_plugin
12)    2           0.09     0.04    0.11%      0.09     0.04    0.11%  is_theme
13)    2           0.06     0.03    0.07%      0.06     0.03    0.07%  env_default
14)    1           0.02     0.02    0.03%      0.02     0.02    0.03%  bashcompinit

Test Driving Mise

With NVM ripped out, I still needed to setup Mise to replace it. I opted to install Mise with apt on Ubuntu since that would provide an easy mechanism to keep Mise itself up to date. And with Mise installed, setting up a default global Node version was a piece of cake! I used the Oh-My-Zsh Mise plugin to init Mise on shell startup, and it’s only adding about 60ms to startup time (compared to over 1500ms for NVM)!

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 2)    1          58.35    58.35   18.07%     58.35    58.35   18.07%  _mise_hook

So far, I’m very happy with Mise. It’s going to replace not only nvm for me, but also rbenv and probably a couple other tools. It’s fast, it has a nice interface, and it’s easy to configure. I really like the way it stores its user-level configuration at ~/.config/mise/config.toml, and I’ve set mine up to respect .ruby-version and .nvmrc files in my projects.

Lessons Learned

So, what did I learn along the way?

  • ZProf is a great tool to profile slow ~/.zshrc init time.
  • When adding Oh-My-Zsh plugins or making other changes to ~/.zshrc, consider running a profile to see if the changes made the init process significantly slower.
  • Mise is a great tool, and can replace multiple other tools with something faster!

About the Author

Mike Kasberg

👋 Hi, I'm Mike! I'm a husband, I'm a father, and I'm a staff software engineer at Strava. I use Ubuntu Linux daily at work and at home. And I enjoy writing about Linux, open source, programming, 3D printing, tech, and other random topics. I'd love to have you follow me on X or LinkedIn to show your support and see when I write new content!

Share!

If you enjoyed this blog post, I'd love it if you could share it with your network!

I run this blog in my spare time. There's no need to pay to access any of the content on this site, but if you find my content useful and would like to show your support, buying me a coffee is a small gesture to let me know what you like and encourage me to write more great content!

You can also support me by visiting LinuxLaptopPrices.com, a website I run as a side project.

Related Posts