The Problem
My terminal was slow. Really slow. Every time I opened a new shell, there was a noticeable lag before my prompt appeared. Admittedly this was a papercut, but it made me feel like my laptop was slowing down. I don’t really want to upgrade right now, and I don’t really want to go scorched earth and wipe my machine to start again to get a fresh, fast terminal. So I decided to optimize my existing setup.
I jumped straight into fix-it mode. Let’s start with zoxide, that must slow down things, right? So I disabled it, which promptly broke tab completion with errors like __zoxide_z_complete: command not found. Great, now I have broken tab completion and a slow terminal. Fantastic.
I fell into the classic trap of “ready, fire, aim”. I made changes without measuring first.
So I needed to do what I should’ve from the start, I added profiling to figure out where all that time was actually going. 🤦
# Add to the top of ~/.zshrc
zmodload zsh/zprof
# Add to the bottom
zprofAs usual, the results were not what I expected.
The Diagnosis
Startup time: 640ms 🐌
Here’s what zprof revealed:
| Function | Time (ms) | % of Total | Issue |
|---|---|---|---|
compinit | 444 | 78.87% | Called twice! |
compdump | 181 | 32.20% | Writing completion cache |
compdef | 123 | 21.78% | 1,633 calls! |
antigen | 317 | 56.32% | Slow plugin manager |
_omz_source | 53 | 9.33% | Loading Oh-My-Zsh plugins |
That’s nice, but what does it actually mean? I had no idea what compinit or compdef were doing, and I had no idea why they were taking so long. I just knew that zoxide was the last thing I added, so it must be the problem, right?
A Different Kind of Pair Programming
I usually use GitHub Copilot for my day-to-day coding, but this week I decided to try out Claude Code for this optimization problem. The difference was notable.
I pasted my zprof output into Claude and asked it to analyze the results. Within seconds, it identified the major culprits, and zoxide wasn’t one of them.
compinitwas being called twice: once by Antigen, once by Oh-My-Zsh- The plugin manager itself was eating 300ms+
- I had previously put in a quick fix for some plugins not working, and the full Oh-My-Zsh was loading when I only needed a handful of features
- I was loading a built in and external fzf plugin
Two minutes with
zprofand a dash of AI would have saved me 20 minutes of wrong guesses. Measure first, tinker second.
It suggested migrating from Antigen to Zinit. Ironically, I’d tried Zinit a few years ago when I first set up my terminal with Antigen but couldn’t get it configured quite right so I parked it. Now with the power of AI, this would be different.
Here’s what made it valuable: this wasn’t just handing off the problem. I asked questions throughout:
- “Why is compinit being called twice?”
- “What’s the difference between these fzf plugins?”
- “Can we keep the OMZ git plugin features I use?”
We debugged the ZLE warnings together, and I understood each change we made. It felt more like pair programming than autocomplete, and I ended up with a configuration I still understand.
What Actually Worked
Step 1: Replace Antigen with Zinit
Antigen is convenient but slow. Zinit is a modern, faster alternative. This was a great suggestion from Claude, and it did a great job of converting my setup in a couple of minutes with only a few back-and-forths to fix edge cases.
# Zinit installation (faster than antigen)
ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git"
[ ! -d $ZINIT_HOME ] && mkdir -p "$(dirname $ZINIT_HOME)"
[ ! -d $ZINIT_HOME/.git ] && git clone https://github.com/zdharma-continuum/zinit.git "$ZINIT_HOME"
source "${ZINIT_HOME}/zinit.zsh"Savings: ~200ms
Step 2: Load Only What You Need from Oh-My-Zsh
Instead of loading the entire Oh-My-Zsh framework, I cherry-picked just the libraries and plugins I actually use. I had a list of plugins at the top of my ~/.zshrc that I wanted to keep, so I made sure to include those. I wasn’t aware that I had added a call in the prompt that caused the entire framework to load.
# Load OMZ libs we need (not the whole thing)
zinit snippet OMZL::git.zsh
zinit snippet OMZL::theme-and-appearance.zsh
zinit snippet OMZL::async_prompt.zsh # Required by git plugin
# Load OMZ plugins
zinit snippet OMZP::git
zinit snippet OMZP::brew
zinit snippet OMZP::direnv
zinit snippet OMZP::eza
# Removed OMZP::fzf - using fzf-tab insteadThis eliminated the overhead of loading unused OMZ plugins and prevented the double compinit call.
Savings: ~150ms
Step 3: Loading 3rd Party Plugins
# Fast syntax highlighting and autosuggestions
zinit light zsh-users/zsh-syntax-highlighting
zinit light zsh-users/zsh-autosuggestions
# fzf-tab for better tab completion
zinit light Aloxaf/fzf-tab
# Don't load fzf-tab-source - it has bugs and we don't need itKey learning: The OMZP::fzf plugin was causing harmless but annoying ZLE warnings. Since we’re using the superior fzf-tab plugin, we don’t need it.
Savings: ~50ms + eliminated warnings
Step 4: Load Completions Once
Instead of letting multiple plugins call compinit, do it once explicitly:
# Load completions efficiently
autoload -Uz compinit
compinitSavings: ~180ms (from eliminating duplicate call)
Step 5: Initialize Tools Properly
Re-enable zoxide (it wasn’t the problem after all!):
# Initialize tools
eval "$(starship init zsh)"
eval "$(fnm env --use-on-cd --shell zsh --version-file-strategy=recursive)"
eval "$(zoxide init zsh)"The Results
New startup time: 120ms 🚀
| Metric | Before | After | Improvement |
|---|---|---|---|
| Plugin manager | Antigen (slow) | Zinit (fast) | ✅ Upgraded |
| ZLE Warnings | Multiple | None | ✅ Fixed |
compinit calls | 2 | 1 | ✅ Fixed |
| Startup time | 640ms | 120ms | 81% faster |
New zprof breakdown:
num calls time self name
-----------------------------------------------------------------------------------
1) 508 16.88 0.03 19.72% 14.13 0.03 16.50% :zinit-tmp-subst-alias
2) 2 11.05 5.53 12.91% 11.05 5.53 12.91% compaudit
3) 7 12.58 1.80 14.70% 9.23 1.32 10.78% .zinit-load-snippet
4) 1 19.04 19.04 22.24% 7.99 7.99 9.33% compinit
5) 4 43.43 10.86 50.73% 6.23 1.56 7.28% .zinit-load-plugin
6) 1 5.07 5.07 5.92% 5.07 5.07 5.92% _fnm_autoload_hookCleanup: Removing Old Caches
After migrating to Zinit, I cleaned up the old Antigen cache. If you’re setting up a new Mac and want to start fresh with this faster setup, check out my Mac setup guide for the complete list of tools to install.
#!/bin/bash
# Remove old antigen cache
rm -rf ~/.antigen
# Clean old completion dumps
rm -f ~/.zcompdump*
# Regenerate fresh cache
zsh -i -c "autoload -Uz compinit && compinit -C"The Complete Configuration
Here’s the final ~/.zshrc:
# used for debugging and profiling - uncomment if needed
# zmodload zsh/zprof
# Zinit installation (faster than antigen)
ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git"
[ ! -d $ZINIT_HOME ] && mkdir -p "$(dirname $ZINIT_HOME)"
[ ! -d $ZINIT_HOME/.git ] && git clone https://github.com/zdharma-continuum/zinit.git "$ZINIT_HOME"
source "${ZINIT_HOME}/zinit.zsh"
# Load OMZ libs we need (not the whole thing)
zinit snippet OMZL::git.zsh
zinit snippet OMZL::theme-and-appearance.zsh
zinit snippet OMZL::async_prompt.zsh
# eza config - must be set BEFORE loading the plugin
zstyle ':omz:plugins:eza' 'dirs-first' yes
zstyle ':omz:plugins:eza' 'git-status' yes
zstyle ':omz:plugins:eza' 'header' yes
zstyle ':omz:plugins:eza' 'icons' yes
# Load OMZ plugins
zinit snippet OMZP::git
zinit snippet OMZP::brew
zinit snippet OMZP::direnv
zinit snippet OMZP::eza
# Fast syntax highlighting and autosuggestions
zinit light zsh-users/zsh-syntax-highlighting
zinit light zsh-users/zsh-autosuggestions
# fzf-tab
zinit light Aloxaf/fzf-tab
# Load completions efficiently (after prompt is ready)
autoload -Uz compinit
compinit
# Initialize tools - these need to load synchronously
eval "$(starship init zsh)"
eval "$(fnm env --use-on-cd --shell zsh --version-file-strategy=recursive)"
eval "$(zoxide init zsh)"
export HOMEBREW_AUTO_UPDATE_SECS="86400"
# history setup
HISTFILE=$HOME/.zhistory
SAVEHIST=1000
HISTSIZE=999
setopt share_history
setopt hist_expire_dups_first
setopt hist_ignore_dups
setopt hist_verify
# Aliases
alias code=code-insiders
alias d=dotnet
alias db="dotnet build"
alias dw="dotnet watch"
alias dr="dotnet run"
alias dt="dotnet test"
alias gprune='git branch --merged origin/$(git_main_branch) | xargs git branch -d'
alias zshconfig="code ~/.zshrc"
# pnpm
export PNPM_HOME="/Users/matt/Library/pnpm"
case ":$PATH:" in
*":$PNPM_HOME:"*) ;;
*) export PATH="$PNPM_HOME:$PATH" ;;
esac
# RVM
export PATH="$PATH:$HOME/.rvm/bin"
# Aspire
export PATH="$HOME/.aspire/bin:$PATH"
# vscode integration
[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"
# used for profiling only
# zprofTry It Yourself
1. First, profile your current setup:
# Add to top of ~/.zshrc
zmodload zsh/zprof
...
# Add to bottom of ~/.zshrc
zprof2. Measure your startup time:
for i in {1..5}; do /usr/bin/time zsh -i -c exit 2>&1 | grep real; done- Review the
zprofoutput to identify your actual bottlenecks (don’t guess like I did!) - Optimize based on what the data shows
- Measure again to verify your improvements
Wrapping Up
Your terminal is where you spend a huge chunk of your dev time. If yours feels sluggish, don’t guess: profile it first. My intuition was completely wrong, and two minutes with zprof saved me from going down the wrong rabbit hole.
Data > Intuition. Always.
Update: The setup has evolved since this post. I’ve since added Starship, fnm, zoxide, Atuin, and a few other tools. See my current Zsh setup for the full config.
Happy profiling! ⚡
