Husband. Father. Software engineer. Ubuntu Linux user.
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!
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
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.
So, what did I learn along the way?
~/.zshrc
init time.~/.zshrc
, consider
running a profile to see if the changes made the init process significantly
slower.👋 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!
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.