Diagnosing Slow Zsh Startup with zprof: From Symptom to Fix
A new terminal tab that takes a second or more to show a prompt usually means your zsh startup files, not zsh itself. Use the built-in zprof profiler to find the culprit, then apply targeted fixes: compinit caching, lazy-loading version managers, and PATH deduplication.
19 Oct 2025, 19:58 UTC

The recognizable condition
You open a new terminal tab and wait. One second, maybe two, before the prompt appears. A bare shell starts instantly, so something in your startup files is burning that time — but ~/.zshrc has grown organically for years and guessing wastes more time than measuring.
The useful takeaway: zsh ships with a built-in profiler, zsh/zprof, that reports function-level time and call counts for everything your startup runs. Combined with one timing command and three targeted fixes (compinit caching, plugin lazy-loading, PATH deduplication), most slow startups drop from seconds to well under 300 ms.
The examples below assume zsh 5.x on macOS or Linux. Startup file names vary slightly by OS packaging; the verification section shows how to confirm which files your zsh actually reads.
Cause and diagnostic table
| Symptom in profile | Likely cause | Typical fix |
|---|---|---|
compinit near the top of zprof output | Completion dump rebuilt on every start | Cache the dump; run the security check once per day |
| Large self time in plugin or version-manager functions (nvm, pyenv, rbenv) | Heavy scripts sourced synchronously | Drop unused plugins; lazy-load version managers |
| Nothing hot in zprof, but shell still slow | Time spent outside functions, or in system files / terminal | Trace with zsh -xv; check terminal and filesystem |
| Startup fine, but subshells/scripts also slow | Heavy code in ~/.zshenv (runs for every shell) | Move interactive-only code to ~/.zshrc |
| PATH grows each time you re-source | Duplicate PATH entries, sometimes from macOS path_helper | typeset -U path |
Step 1: Quantify the problem
Run these in your current shell. No special permissions are needed; nothing here changes state.
time zsh -i -c exit
time zsh -f -i -c exitThe first command starts a full interactive shell and exits immediately; the second does the same with -f, which skips all startup files. If -f is fast (tens of milliseconds) but the normal shell is slow, the problem is in your startup files, not zsh itself. Record both numbers — you will re-run the first command after every change.
Step 2: Profile with zprof
Edit ~/.zshrc and add this as the very first line:
zmodload zsh/zprofOpen a new shell, then run:
zprof | head -20The output lists functions by call count, total time, and self time (time excluding calls to other functions). Self time is the column that points at the actual culprit. A typical finding is compinit consuming several hundred milliseconds, or a plugin-manager function dominating the list.
One limitation to know before you trust the numbers: zprof attributes time at function granularity. Code written at the top level of a file, outside any function, may show up under an anonymous entry or be hard to attribute. If a suspect block of your ~/.zshrc does not appear clearly, wrap it in a named function and call it, e.g.:
my_setup() {
# suspect lines here
}
my_setupIf zprof shows nothing expensive but the shell is still slow, fall back to bisection: comment out the second half of ~/.zshrc, time a new shell, and narrow down. Also inspect /etc/zshrc, /etc/zprofile, and ~/.zshenv — the last one runs for every shell, including non-interactive scripts, so it should stay nearly empty.
Step 3: Apply the fix that matches the finding
compinit rebuilds the dump every start
compinit sets up tab completion and, by default, re-scans all completion directories and rebuilds its cache file (~/.zcompdump) on every shell. The standard fix is to reuse the cached dump and only do the full check when it is stale:
autoload -Uz compinit
if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then
compinit
else
compinit -C
fiThis runs the full compinit (including its security audit of directory permissions) only when the dump is more than 24 hours old, and otherwise loads the cache with -C. Note the trade-off: plain compinit -C on every start skips the insecure-directory check entirely, which is acceptable on a single-user laptop but worth avoiding on shared systems. Verify the cache is being reused by checking that the modification time of ~/.zcompdump no longer updates on every new shell:
ls -l ~/.zcompdumpA plugin or version manager dominates
Version-manager shims are the classic offender: initializing nvm, pyenv, or rbenv eagerly can each add hundreds of milliseconds because they shell out to detect versions and rewrite PATH. Options, in order of preference:
- Remove plugins you no longer use — zprof makes the cost of each one visible, so this becomes an evidence-based decision.
- Lazy-load the version manager: instead of running its init at startup, define a stub function that loads it on first use. Many community lazy-load wrappers exist, or write a small one.
Be aware of the side effect: lazy-loading means the tool's directory is not on PATH until the stub fires. Non-interactive scripts that call node or python directly may break. Keep eager loading in ~/.zprofile or CI environments if that matters to you.
PATH keeps growing
Check for duplicates (run in any interactive shell):
echo $PATH | tr ':' '\n' | sort | uniq -dAny output means entries are duplicated — often from re-sourcing ~/.zshrc or from macOS path_helper in /etc/zprofile reordering and re-adding system paths. Zsh has a clean built-in fix; add near the top of ~/.zshrc:
typeset -U path PATHThe -U flag keeps only the first occurrence of each entry in the tied path array / PATH string. Re-run the uniq -d check in a fresh shell; it should print nothing.
Escalation criteria
Stop tuning your dotfiles and look elsewhere when:
zsh -f -i -c exitis also slow — the problem is the terminal emulator, a slow or network-mounted home directory, or security software scanning processes, not your configuration.- zprof points into
/etc/zsh*system files you do not control — check what your OS or IT-managed tooling injects there. - A single third-party plugin accounts for most of the time even after lazy-loading — capture the zprof output and report it to the plugin's maintainer rather than working around it.
To confirm exactly which files your zsh reads and in what order, trace a startup: zsh -xv -i -c exit 2>&1 | less. The trace is verbose, but it settles any doubt about packaging differences between macOS and Linux distributions.
Verifying the result
After each change, re-run time zsh -i -c exit and compare against your Step 1 baseline. A realistic target for a well-tuned setup with a moderate plugin set is under 200–300 ms; instant is not the goal, measurably better is. Once you are satisfied, you can leave zmodload zsh/zprof in place — its overhead is negligible — or remove it and keep the line commented for the next time startup creeps up.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.