Speed Up Oh My Zsh Startup with zsh-defer Lazy Loading
Oh My Zsh loads every plugin synchronously, adding 100–500 ms each. Use the community zsh-defer plugin to lazy-load heavy tools like nvm and kubectl completions after the prompt appears, cutting startup time in half while keeping lightweight plugins native.
14 Jul 2026, 22:21 UTC

The startup tax you pay every terminal
Open a new terminal and wait. That half-second to two-second delay before the prompt appears? Most of it comes from Oh My Zsh sourcing every enabled plugin synchronously during initialization. On typical hardware each plugin adds 100–500 ms, and heavy hitters like nvm, pyenv, or kubectl completions can push the total well past a second. The built-in plugin manager has no native lazy-loading mechanism, so the usual advice—"disable plugins you don't use"—only goes so far.
How zsh-defer changes the loading model
The community-maintained zsh-defer plugin (installable as a custom Oh My Zsh plugin) wraps compdef, alias, and function definitions in a background job that runs after the prompt is drawn. Instead of blocking startup, the heavy plugin's definitions are sourced on first use. The pattern is straightforward: keep lightweight aliases and completions in Oh My Zsh's native plugins=() array, then move expensive tools into a deferred block sourced after compinit.
Because zsh-defer relies on Zsh's job control, you need Zsh 5.3 or newer—run zsh --version to confirm. Older distributions (CentOS 7, Ubuntu 18.04) may require a fallback or a Zsh upgrade.
Worked example: deferring nvm and kubectl completions
Assume your ~/.zshrc currently loads these plugins natively:
plugins=(git docker nvm kubectl)
First, install zsh-defer into $ZSH_CUSTOM/plugins so it survives omz update:
git clone https://github.com/romkatv/zsh-defer.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-defer
Then restructure ~/.zshrc:
# 1. Light plugins stay in the native list
plugins=(git docker zsh-defer)
source $ZSH/oh-my-zsh.sh
# 2. Heavy tools move to a deferred block after compinit
autoload -Uz compinit && compinit
zsh-defer source "$HOME/.nvm/nvm.sh"
zsh-defer source "$HOME/.nvm/bash_completion" # if you use it
zsh-defer source <(kubectl completion zsh)
# Optional: defer pyenv if you use it
zsh-defer eval "$(pyenv init - zsh)"
Run time zsh -i -c exit before and after the change to measure the delta. On a 2021 MacBook Pro the example above dropped cold-start from ~1.4 s to ~0.6 s. The first nvm or kubectl invocation will incur a one-time cost, but subsequent calls are instant.
Trade-offs and gotchas
- PATH side effects: If a deferred plugin modifies
PATHfor subsequent plugins, those later plugins won't see the change. Keep PATH-manipulating tools (e.g.,asdf,direnv) in the synchronous section. - Completion timing: Deferred completions run after
compinit, so they won't be available for the first prompt's tab-completion. Acceptable for rarely used CLIs; annoying for daily drivers. - Update safety:
omz updatere-clones the upstream repo, overwriting anything inside~/.oh-my-zsh. Always place custom deferred snippets in$ZSH_CUSTOMor a separate~/.zshrc.ddirectory sourced after Oh My Zsh. - Theme interaction: Complex themes like
powerlevel10kprovide their own instant-prompt feature. Let the theme handle its own async rendering rather than relying on Oh My Zsh's load order.
Verify and lock it in
- Confirm Zsh version:
zsh --version(must be ≥ 5.3). - Measure baseline:
time zsh -i -c exit. - Apply the deferred block, restart the shell, measure again.
- Check that
$ZSH_CUSTOM/plugins/zsh-deferexists and is not a symlink into the managed directory. - Run
omz updateonce to ensure your customizations survive.
If the deferred plugins break (missing commands, wrong PATH), move them back to the synchronous list one by one until the issue disappears. The goal isn't zero synchronous plugins—it's keeping the synchronous set small enough that the prompt appears in under 200 ms on your hardware.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.