Background#
I’ve been a zsh user for a long time, both on Linux (where it’s not the default) and OS X macOS (where it now is the default).
The first thing I used to do on new machines is port all of my .oh-my-zsh stuff over.
Oh My Zsh is not bad! This is not meant to slam the project at all. It has 187,000 GitHub stars, 300+ plugins, 140+ themes, and 2,500+ contributors for a reason. It democratized shell customization at a scale nothing else has matched.
But when I upgraded to a Mac Mini M4 back in January 2025, I wondered if there was something good enough without being quite so large. Oh My Zsh’s startup time had been bothering me for years. That half-second pause every time I opened a terminal. I’d just accepted it as the cost of doing business.
A year and a half later, I’ve got a much clearer picture. The answers aren’t what I expected.
What’s Out There#
Antidote#
Antidote is still my daily driver, now at version 1.10.0. It’s a pure Zsh implementation that picks up where Antibody left off. The core idea is simple: you list your plugins in a text file, Antidote builds a static load script from them, and your .zshrc just sources the result.
The static file approach means Antidote’s parsing overhead is paid once (at build time), not on every shell start. This is the same insight that made zgen fast years ago, but Antidote executes it with better concurrency and modern Zsh practices.
Here’s my current config, which has been stable for over a year:
~/.zshrc
autoload -Uz compinit
compinit
. "$HOME/.cargo/env"
zstyle :omz:plugins:iterm2 shell-integration yes
ZSH_THEME="kphoen"
function is-macos() {
[[ $OSTYPE == darwin* ]]
}
source ~/.antidote/antidote.zsh
antidote load
~/.zsh_plugins.txt
# use-omz needs to be FIRST
getantidote/use-omz
ohmyzsh/ohmyzsh path:lib
ohmyzsh/ohmyzsh path:plugins/colored-man-pages
ohmyzsh/ohmyzsh path:plugins/docker-compose
ohmyzsh/ohmyzsh path:plugins/gh
ohmyzsh/ohmyzsh path:plugins/git
ohmyzsh/ohmyzsh path:plugins/git-prompt
ohmyzsh/ohmyzsh path:plugins/rsync
ohmyzsh/ohmyzsh path:plugins/ssh
ohmyzsh/ohmyzsh path:plugins/brew conditional:is-macos
ohmyzsh/ohmyzsh path:plugins/macos conditional:is-macos
ohmyzsh/ohmyzsh path:plugins/iterm2 conditional:is-macos
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
It works. It’s been working for 16 months. I have no complaints.
But Antidote is not the only game in town, and depending on what you value, it may not even be the best fit for you.
Sheldon#
Sheldon (v0.8.5) is the Rust entry in the plugin manager space, written by Ross MacArthur. Instead of a plugins.txt file, you use a TOML config:
shell = "zsh"
[plugins]
git = { github = "ohmyzsh/ohmyzsh", dir = "plugins/git", use = "git.plugin.zsh" }
zsh-autosuggestions = { github = "zsh-users/zsh-autosuggestions" }
Your .zshrc then just needs eval "$(sheldon source)" and a sheldon lock to pre-build.
Sheldon’s advantage is that it’s a compiled binary with no Zsh overhead. The parsing and lock file generation happen in Rust, not in your shell. On the rossmacarthur/zsh-plugin-manager-benchmark (yes, Ross benchmarks his own project fairly), Sheldon lands in the top tier for both install and load time alongside Antidote and Zim.
You do need to install a binary. On macOS that’s brew install sheldon, which is painless. On Linux you’re either grabbing a release tarball or building from source. If you already manage your dotfiles with a structured approach, Sheldon’s TOML config will feel natural. If you just want to source a thing and move on, Antidote is simpler.
Zinit#
Zinit (v3.14.0, formerly zplugin) takes a fundamentally different approach. Instead of building a static file, it uses a concept called “turbo mode” that loads plugins after the prompt appears, in the background.
The selling point is dramatic. Your shell prompt shows up instantly, and plugins trickle in asynchronously. On paper this is the ideal solution. Your terminal is usable immediately, and the heavy stuff arrives a fraction of a second later.
In practice, turbo mode has sharp edges. Roman Perepelitsa (author of zsh-bench and powerlevel10k) explicitly does not recommend it. The problem is that many plugins expect to be loaded synchronously. They set environment variables, register keybindings, or define completions that later commands depend on. If your git plugin hasn’t finished loading when you type git status, you get silence instead of output.
Zinit is also the most complex manager to configure. Its ice-modifier syntax is powerful but dense:
zinit ice depth"1" atload"!_zsh_git_prompt"
zinit light ohmyzsh/ohmyzsh path:plugins/git
This is not the kind of config you write from memory. You’ll be keeping the README open.
That said, if you’re willing to invest in learning it, Zinit’s turbo mode genuinely works for reducing perceived latency. The zsh-bench results show Zinit achieves the same 10% first-prompt lag as Antidote and DIY setups (excellent results), though its first-command lag is higher (78% vs 46%), reflecting the overhead of its tracking and reporting features.
Zim#
Zim (v1.16.0) is a framework, not a pure plugin manager. It’s closer in philosophy to Oh My Zsh but engineered for speed from the start. It ships with a curated set of modules (environment, git, input, completion, etc.) and uses heavy caching and bytecode compilation to keep startup fast.
Our Docker benchmarks showed Zim’s warm load time at just 0.009s, essentially indistinguishable from bare Zsh. That’s because Zim’s init.zsh compiles itself and its modules to wordcode (.zwc files) on first load, so subsequent starts are just loading pre-compiled bytecode.
The tradeoff is a smaller ecosystem than OMZ. You get a well-designed set of defaults, but if you want an obscure OMZ plugin, you’re either adapting it manually or adding it as a Zim module via zmodule. Most people won’t run into this. Zim’s core modules cover the essentials (syntax highlighting, autosuggestions, completion, history, git) with better performance than OMZ equivalents.
Others Worth Mentioning#
Zgenom is the maintained fork of zgen, providing static loading with OMZ and Prezto support. Solid, simple, unsexy. If Antidote didn’t exist, this would be my recommendation for the “just works” category.
Zcomet is a minimalist manager from the author of the zsh-unplugged approach. Gets excellent zsh-bench scores (10% first-prompt lag, 44% first-command lag). Tiny codebase worth reading if you want to understand what a plugin manager actually does under the hood.
The Benchmarks#
When I wrote the original version of this page, I didn’t have real data. I had vibes. “Oh My Zsh feels slow” is not a benchmark. Let me fix that.
There are two distinct ways to measure shell startup, and confusing them has led to a lot of bad advice online.
The Wrong Way: time zsh -i -c exit#
This is what most blog posts use. You run time zsh -i -c exit, get a number like 0.072s, and declare victory. The problem is that this measures the total time for Zsh to start, load your config, run exit, and shut down. It does not measure how long you wait for a usable prompt. Which is the actual user experience.
I ran this benchmark in a Docker container on Ubuntu 22.04 with a standard set of 7 OMZ plugins plus zsh-autosuggestions and zsh-syntax-highlighting, averaged over 5 warm runs:
Wait. Antidote and Zinit look slower than OMZ by this metric? That seems backwards.
Here’s the catch: time zsh -i -c exit includes the full process lifecycle, which disproportionately penalizes managers that do more careful initialization (compinit, static file generation, tracking). OMZ’s aggressive caching makes its exit-to-exit time fast, but that doesn’t mean the shell feels faster. It means the timer starts and stops in the wrong places.
This metric is essentially misleading for comparing plugin managers. It’s useful for measuring gross framework overhead (bare Zsh vs OMZ vs Zim) but tells you almost nothing about perceived responsiveness.
The Right Way: zsh-bench#
zsh-bench by Roman Perepelitsa measures what actually matters: the time between pressing Enter and seeing a usable prompt. It creates a virtual terminal, sends keystrokes, and measures four specific latencies:
Any value below the threshold is indistinguishable from zero in blind testing. Roman ran a self-study to establish these thresholds using human-bench, his companion tool. These are not arbitrary numbers.
The results for plugin managers are worth looking at:
| Config | 1st Prompt | 1st Cmd | Cmd Lag | Input Lag |
|---|---|---|---|---|
| DIY (baseline) | 10% 🟢 | 42% 🟢 | 24% 🟢 | 64% 🟡 |
| Antidote | 10% 🟢 | 46% 🟢 | 24% 🟢 | 63% 🟡 |
| Zcomet | 10% 🟢 | 44% 🟢 | 25% 🟢 | 64% 🟡 |
| Zinit | 10% 🟢 | 78% 🟡 | 24% 🟢 | 64% 🟡 |
| Zplug | 108% 🟠 | 100% 🟡 | 24% 🟢 | 64% 🟡 |
| Oh My Zsh | 187% 🟠 | 64% 🟡 | 366% 🔴 | 2% 🟢 |
Key takeaways:
Antidote, Zcomet, and Zinit all achieve 10% first-prompt lag. Imperceptible. The plugin manager is not the bottleneck for prompt speed.
OMZ’s 366% command lag is the real cost. Nearly 4 times the perception threshold. And this is with the
robbyrusselltheme. Theagnostertheme hits 244%, andstarshiphits 354%. The theme, not the framework itself, is responsible for most of the perceived slowness.Zinit’s 78% first-command lag is the only notable manager-specific overhead, coming from its tracking and reporting infrastructure. You pay for those features even if you don’t use them.
Input lag is identical across all managers (63-64%). This is a Zsh-itself tax from syntax highlighting, not a manager concern.
The practical takeaway: the metric most blog posts use (time zsh -i -c exit) is not the right one. I should know. My plugin manager comparison blog posts on this very page were using the wrong numbers. I’ve updated accordingly.
Our full benchmark scripts and Docker configuration are available on Forgejo, including both the time zsh -i -c exit approach (for reproducibility with other blog posts) and links to the zsh-bench methodology that supersedes it.
Where the Real Bloat Lives#
After a year and a half of running Antidote, here’s what I’ve learned about shell startup performance.
The plugin manager is not your problem. The plugins are your problem.
Look at the numbers. brew --prefix nvm takes ~600ms on a cold run. thefuck --alias takes ~120ms. pyenv init takes ~300ms. A single invocation of git in your prompt (which the robbyrussell and agnoster themes do on every prompt) adds latency proportional to the size of your repository. The laggardkernel analysis covers this in detail in this gist.
Compare that to the overhead of your plugin manager, which is measured in single-digit milliseconds when configured properly. The rossmacarthur benchmark shows that Antidote, Sheldon, and Zim all load a full complement of plugins in roughly the same time, and that time is dominated by source-ing the plugin files, not by the manager itself.
The hierarchy of shell slowness:
If you want a faster shell, in order of impact: switch to powerlevel10k (which uses gitstatus for async, non-blocking git info), lazy-load your language version managers, drop starship if you’re using it, and then worry about which plugin manager you’re using.
The gist by laggardkernel covers this in far more detail than I can here. The key insight: Zinit’s turbo mode is the only approach that directly addresses the real bottleneck (time-consuming plugins) by deferring them past the prompt. Every other optimization (static loading, bytecode compilation, concurrent install) optimizes the cheap part of the equation.
16 Months Later#
So after all this, what am I actually running?
Still Antidote. It hasn’t let me down. The config above is what I use every day, and I haven’t touched it in months. It works, it’s fast enough by any practical measure, and I don’t think about my shell configuration. Which is exactly the point.
If I were starting fresh today and wanted the absolute fastest possible shell with minimal configuration effort, I’d probably go with Zim. Its warm load times are genuinely impressive, and the curated module set covers everything I actually use.
If I wanted to maximize control and was willing to invest the time to learn one complex tool, I’d pick Zinit. Turbo mode is the only approach that solves the real bottleneck, even if it requires careful configuration.
And if I needed something consistent across multiple Linux machines with a binary-distributed tool, I’d use Sheldon.
But here’s the dirty secret. It barely matters which one you pick. The difference between the top-tier managers is single-digit milliseconds. The difference between a good theme and a bad one is hundreds of milliseconds. The difference between lazy-loading your language version manager and not doing so is half a second.
Pick one that fits your mental model, set it up, and stop worrying about your shell config. There are better things to optimize.
Benchmark scripts, Docker configuration, and full raw results are available at git.brandyapple.com/magnus/zsh-plugin-manager-benchmarks. All performance data references are linked inline. I’ve tried to make every claim in this page reproducible.
