What if I told you that someone hid some weights in your shoes?
Not massive weights that would make your shoes unliftable, but many tiny lead balls, hidden everywhere. Just enough lead balls so you can still walk, but the shoes feel heavier and walking feels harder today and, oh wow, I’m tired, this short walk really knocked it out of me.
You’d probably look at me and say “hey man what’s your problem” and then you’d go “what weights?” and then I’d show you and you’d look at me and go “who did this? you? what’s wrong with you?” and then you’d take the weights out and you’d put on your shoes and cautiously take a few steps and look at me with big eyes and then walk and then skip and then run and then you’d say “it was the weights! I’m not out of shape!” and, hell, maybe you’d do one of those jumps and click your heels together in the air.
***
Alright. Open your terminal and type in the following:
# If you're a ZSH user:
time zsh -i -c exit
# Bash user:
time bash -i -c exit
# Fish user:
time fish -i -c exit
Run this command, say, 5 times. How fast does your shell start up? (I’m seriously interested – leave a comment or reply to this email.)
This is what I get:
$ time zsh -i -c exit
zsh -i -c exit 0.03s user 0.02s system 89% cpu 0.062 total
$ time zsh -i -c exit
zsh -i -c exit 0.04s user 0.03s system 88% cpu 0.072 total
$ time zsh -i -c exit
zsh -i -c exit 0.03s user 0.03s system 88% cpu 0.070 total
$ time zsh -i -c exit
zsh -i -c exit 0.04s user 0.03s system 87% cpu 0.072 total
$ time zsh -i -c exit
zsh -i -c exit 0.04s user 0.03s system 88% cpu 0.070 total
That means spawning a new shell takes ~70ms on my machine.
If it takes, say, longer than 200ms or 300ms for you then the diagnosis is clear: you’ve got lead balls in your shoes.
***
Now you’re thinking: does it matter? 100ms? 200ms? Come on, dude.
And I’m telling you: yes it matters. Of course it matters. Those who don’t honor the milliseconds will end up with seconds. 100%, it absolutely matters. Who wants to walk around with clown shoes full of lead?
Think about it this way: which program do you execute more often than your shell? How many shells do you spawn every day? How many other programs do you run every day that spawn your shell? If you’re anything like me, it’s a lot of shells per day. I’m a heavy terminal and tmux user. I spawn shells like I open new tabs in a browser. Do you want one of your most-used programs to start slow because you didn’t care?
But also: it’s not just about your shell startup time – how fast does your shell prompt render? Do you have one of those multi-line shell prompts that displays 8 different bits of information? How long does it take to recompute that information? Think of your shell prompt as a tiny program. A tiny program that executed every time you run a command in your shell. Now look into the mirror and ask yourself: do I really want to wait for my fancy prompt to render every time I run a command?
Sure, there’s airquotes blazingly airquotes fast prompts and they’re optimized and use caches, but when I add starship
to my zshrc it adds 10ms to its startup. 10ms – not much, I’ll give you that. But consider how many other programs also add 10ms by adding their own hooks into your .profile
file or your shell-rc file, adding new commands or env vars or custom completions. Homebrew? direnv? asdf? Docker? NPM? PNPM? Yarn? That Google SDK stuff? It adds up.
Everybody wants a piece of your shell. Be very careful about giving it to them. Otherwise: clown shoes.
***
How do you fix your slow shell startup time?
First: profile! ZSH, for example, allows you to profile its startup time. You can do similar things for Bash. What exactly you do doesn’t matter as much as actually doing it. You can also binary-search comment out half of your shell rc-file at a time and then see what improvements that made.
I often run this command when tweaking my .zshrc
:
for i in $(seq 1 10); do time $SHELL -i -c exit; done
Second: optimize your shell rc-file! There’s a lot of resources out there. This blog post is pretty good if you’re a ZSH user. This one is very extensive and contains a lot of references to other blog posts too.
Generally speaking, these are the rules of thumb:
Run as few commands as possible
Keep the prompt simple
Do less
The first one – run as few commands as possible – is pretty simple but if you go through your rc-file you’ll probably find a lot of commands being executed. Common example: brew --prefix
– many macOS users have this in their shell rc-file to set aliases. That’s expensive. If I add this to my .zshrc
alias foo="$(brew --prefix)/foo"
alias bar="$(brew --prefix)/bar"
it adds 20ms to the startup time. If possible, replace external commands with shell built-ins or with env vars.
Second rule: keep the prompt simple. Once upon a time I had fancy prompt that displayed the current Ruby version, the current Go version, status of the git directory, and so on. It turned out that getting the Ruby and Go versions was uncached and added 20ms to every prompt. So every time I ran ls
I had to wait an additional 20ms for the prompt to show up. Clown shoes. Look at your prompt and really consider whether you need that information on display at all times or whether you can’t run a command on-demand instead to, for example, find out how much battery your laptop has left.
Third rule: do less. The cardinal rule of optimzations also applies here. Do less in your rc-file. Do you really need fancy autocomplete for all of those commands? Even if it adds 100ms to your startup time? Do you really need all those commands adding their own keybindings, the ones you never use? What about that language version manager – do you need that or is there a faster replacement?
Let me make all of this concrete by showing you some things I did to my .zshrc
.
Here I got rid of 150ms startup time by throwing out a lot of fancy ZSH completion mechanisms that I never used and caching the output of uname
. Then Keegan told me in a comment that I don’t even have to run uname
, I can use an env var – another 5ms.
Here’s 18ms saved by removing brew --prefix
, using -e
to check for file-existence, and avoiding double-sourcing of another file. This trick here to speed up ZSH’s completion-init saves another 20ms. I got the newest version from this gist here. And all of this started 8 years ago with me realising that rbenv
added 100ms to the shell startup time.
So, what’s in your shell rc-file that you can optimize?
My startup time was around 400ms, mostly because of NVM. I was using bash, and switched over to zsh. I'm now lazy loading NVM using the oh-my-zsh plugin and now my startup time is around 100ms. Not very fast but an improvement nonetheless. Thanks!!
Thanks for writing post. This gave me curiousity to check my shell startup time. I found it takes ~1.2 seconds to load SHELL and after fixing nvm (culprit who was taking most of time) it has reduced to 0.121ms which is great 👍
Thank you once again.