# Comparison of ZSH frameworks and plugin managers ## Changelog - update 1: add a FAQ section - update 2: benchmark chart and feature comparison table - update 3: - improve the table with missing features for antigen - new `zplg times` result ## TLDR [Speed comparison of ZSH plugin mangers with 10 plugins loaded](https://github.com/vintersnow/zsh_plugin_manager_speed) made by vintersnow@github, detail is covered in the post [最速のZsh プラグインマネージャーを求めて](https://qiita.com/vintersnow/items/cef72aebb2d55a82f212). ![Result of speed comparison](https://raw.githubusercontent.com/vintersnow/zsh_plugin_manager_speed/master/result.png) Comparison of Features and Optimizations made by me. | Name | Manager | Plugins | Frameworks Supported | Other Features | Speed with 0 plugin | | --- | --- | --- | --- | --- | --- | | antigen | bytecode compiling, static bundle loading | - | oh-my-zsh | - | 60 ms | | zgen | static loading | - | oh-my-zsh, prezto | - | 50 ms | | antibody | executed in Go, or static loading | - | oh-my-zsh | - | 50 ms | | zplug | cache mechanism | parallel loading | oh-my-zsh, prezto | managing scripts | 160 ms | | zplugin | bytecode compiling | bytecode compiling, Turbo Mode (background loading) | oh-my-zsh, prezto | managing scripts, completions, plugin report | 50 ms | ## Intro I've been trying different plugin mangers for months to find the fastest one. The content below is what I got. **For the record, the conclusion is very subjective. But it's the truth for me. I would appreciate hearing your thoughts on this.** Here it begins. Display what I got after this long journey. (BTW, I has an SSD on my machine, the data should be much different on HDDs.) ```zsh ❯ export TIMEFMT='%U user %S system %P cpu %*E total' ❯ for i ({1..10}) time zsh -ilc echo &>/dev/null || true 0.07s user 0.04s system 97% cpu 0.115 total 0.07s user 0.04s system 97% cpu 0.110 total 0.06s user 0.04s system 97% cpu 0.109 total 0.07s user 0.04s system 97% cpu 0.111 total 0.07s user 0.04s system 97% cpu 0.111 total 0.07s user 0.04s system 97% cpu 0.111 total 0.07s user 0.04s system 97% cpu 0.111 total 0.07s user 0.04s system 97% cpu 0.112 total 0.07s user 0.04s system 97% cpu 0.113 total 0.07s user 0.04s system 97% cpu 0.114 total ❯ zplg times Plugin loading times: 0.001 sec - dircolors-solarized 0.001 sec - base16-fzf 0.001 sec - PZT::modules/environment 0.001 sec - PZT::modules/history 0.001 sec - PZT::modules/directory 0.001 sec - PZT::modules/helper 0.003 sec - PZT::modules/spectrum 0.011 sec - PZT::modules/utility 0.001 sec - PZT::modules/osx 0.001 sec - OMZ::plugins/fancy-ctrl-z 0.004 sec - OMZ::plugins/fzf 0.005 sec - mafredri/zsh-async 0.005 sec - laggardkernel/zsh-fuzzy-search-and-edit 0.001 sec - wfxr/forgit 0.002 sec - laggardkernel/git-ignore 0.001 sec - ytet5uy4/pctl 0.001 sec - getColorCode 0.001 sec - PZT::modules/completion # only use the conf part 0.001 sec - zpm-zsh/ssh 0.001 sec - zsh-users/zsh-completions 0.001 sec - zdharma/history-search-multi-word 0.002 sec - PZT::modules/autosuggestions 0.006 sec - laggardkernel/spaceship-prompt 0.089 sec - romkatv/gitstatus # turbo mode started 0.017 sec - _local/init0 # init of pyenv, nodenv, rbenv 0.036 sec - zdharma/fast-syntax-highlighting 0.002 sec - PZT::modules/history-substring-search 0.022 sec - softmoth/zsh-vim-mode 0.017 sec - PZT::modules/fasd # compinit here 0.077 sec - _local/init1 0.011 sec - urbainvaes/fzf-marks 0.027 sec - hlissner/zsh-autopair 0.027 sec - MichaelAquilina/zsh-you-should-use 0.018 sec - marzocchi/zsh-notify 0.005 sec - OMZ::plugins/urltools Total: 0.401 sec ❯ count=$(zplg times|wc -l) && echo $((count - 2)) # calc number of plugins 35 ``` ## My experience with ZSH frameworks and plugin managers: There're basically three kinds of startup time for ZSH. 1. time taken by framework, or by plugin manger itself to parse its dialect 2. time cost in loading plugins 3. time taken by scripts written by ourselves The results here are based on the first two kinds of time. I should mention it here that the most of startup time is taken by some time-consuming plugins, such as - `*vm`, `*env` initialization - `nvm`, `rvm` - `rbenv`, `pyenv`, `nodenv` - init code generation (not `source`, or `eval` those codes) - `thefuck`, 120ms for init generation, 1ms for `eval` 👍 - `fasd`, 30~40ms for init generation - time-consuming commands - `brew --prefix nvm`, 600ms 🌚 - `brew command command-not-found-init` ### Frameworks #### [Oh-My-ZSH](https://github.com/robbyrussell/oh-my-zsh) The most famous framework with most plugins built-in. For example, `autojump`, `z`, `fasd` are all included, which let users choose whatever they want. #### [Prezto](https://github.com/sorin-ionescu/prezto) The only framework does optimizations in plugins with sophisticated coding skill, which makes me realize I'm ignorant about ZSH: - [Refreshing `.zcompdump` every 20h](https://github.com/sorin-ionescu/prezto/blob/4abbc5572149baa6a5e7e38393a4b2006f01024f/modules/completion/init.zsh#L31-L41) - [Compiling `.zcompdump` as bytecode in the background](https://github.com/sorin-ionescu/prezto/blob/4abbc5572149baa6a5e7e38393a4b2006f01024f/runcoms/zlogin#L9-L15) - [Caching init script for fasd](https://github.com/sorin-ionescu/prezto/blob/4abbc5572149baa6a5e7e38393a4b2006f01024f/modules/fasd/init.zsh#L22-L36) - Saving `*env` startup time with [`init - --no-rehash` for `rbenv`, `pyenv`, `nodenv`](https://github.com/sorin-ionescu/prezto/blob/4abbc5572149baa6a5e7e38393a4b2006f01024f/modules/python/init.zsh#L22) - [Removing the horribly time-consuming `brew command` from `command-not-found`](https://github.com/sorin-ionescu/prezto/blob/4abbc5572149baa6a5e7e38393a4b2006f01024f/modules/command-not-found/init.zsh) Prezto modules are loaded by its custom function called `pmodload`. `source` the script directly won't work as expected. And it has a different style compared with oh-my-zsh: - Avoiding environment variable pollution with `zstyle` - Bundling related plugins together - module `python` in Prezto is composed of four parts: `pyenv`, `conda`, `virtualenv`, `virtualenvwrapper` #### [ZIM](https://github.com/zimfw/zimfw) (I have not tested this. But bytecode compiling should help it be one of the fast frameworks.) Zsh IMproved FrameWork. According to issue #284: - bytecode compiling - coding style focused on efficiency Drawback: much less plugins are included compared to oh-my-zsh and Prezto. ### Plugin Managers #### [Antigen](https://github.com/zsh-users/antigen) The de facto official plugin manager. Antigen supports oh-my-zsh, plugins from github repos. No prezto module support. Startup time of a clean installation is about 60ms. Startup time using conf example from the [`README.md`](https://github.com/zsh-users/antigen#usage) is about 150ms. (Exclude the time-consuming `command-not-found` plugin for macOS, 6 plugins are loaded, maybe with `compinit`.) **Not bad, but not excellent either.** Optimizations: - bytecode compiling for itself to reduce the 1st kind of time - No optimization for the loading of plugins #### [Zgen](https://github.com/tarjoilija/zgen) Zgen hitchhikes on [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) and [prezto](https://github.com/sorin-ionescu/prezto). It clones those two repos, and loads their plugins/modules using methods from them. Zgen also supports plugins from github repos. What makes zgen excellent is its **static loading**. It generates a **static init script** consisting of `source` statements for plugins and `pmodload` statements for prezto modules. When ZSH starts up, zgen will source the init script directly without parsing its dialect every time, which also means you need to regenerate the init script after updating ur conf. A clean startup time is 50ms. Optimizations: - static init, removes the 1st kind of time **completely** - No optimization for the loading of plugins #### [Antibody](https://github.com/getantibody/antibody) Antigen in Go. It supports oh-my-zsh plugins, plugins from github repos. No support for Prezto modules. **Note**: For anyone think the plugin manager written in Go is superior to plugin manger written in ZSH. Let's make it clear, Antibody loads plugins using **ZSH statement `source`**, which means antibody ONLY reduces the 1st kind of startup time. The born of antibody may be related to the slowness of old version antigen. Antibody tries to use Go to reduces the startup time taken by plugin manger itself, cause using Go to parse and execute its dialect `antibody bundle < ~/.zsh_plugins.txt` is faster. (The slowness of antigen should only be its skeleton in the closet now.) It adds a new loading method called **static loading** later. Obviously, parsing the dialect into `source` statements to cache it directly later, is faster than parsing the dialect every time ZSH starts up. You may guess it, this is basically what [zgen](https://github.com/tarjoilija/zgen) is doing using ZSH. The clean startup time of both managers are very close, nearly 50ms. I kind of feel sorry for the author of antibody, who uses a more complicated, powerful tool to solve the problem, but later found himself defeated by an simple idea. Optimizations: - Reduce the 1st kind of time with Go, or - remove the 1st kind of time **completely** using static loading - No optimization for the loading of plugins #### A Detailed Comparison Between Antibody and Zgen Clean startup time without any plugin loaded: ```zsh # antibody, source <(antibody init) for i ({1..10}) time zsh -ilc echo &>/dev/null zsh 0.03s user 0.02s system 118% cpu 0.047 total zsh 0.03s user 0.02s system 103% cpu 0.049 total zsh 0.03s user 0.02s system 107% cpu 0.051 total zsh 0.03s user 0.02s system 114% cpu 0.045 total zsh 0.03s user 0.02s system 102% cpu 0.049 total zsh 0.03s user 0.02s system 105% cpu 0.047 total zsh 0.03s user 0.02s system 110% cpu 0.046 total zsh 0.03s user 0.02s system 107% cpu 0.050 total zsh 0.03s user 0.02s system 106% cpu 0.049 total zsh 0.03s user 0.02s system 107% cpu 0.048 total # zgen, source "${HOME}/.zgen/zgen.zsh" for i ({1..10}) time zsh -ilc echo &>/dev/null zsh 0.03s user 0.02s system 110% cpu 0.046 total zsh 0.03s user 0.02s system 106% cpu 0.046 total zsh 0.03s user 0.02s system 106% cpu 0.048 total zsh 0.03s user 0.02s system 109% cpu 0.051 total zsh 0.03s user 0.02s system 105% cpu 0.048 total zsh 0.03s user 0.02s system 102% cpu 0.051 total zsh 0.03s user 0.02s system 106% cpu 0.047 total zsh 0.03s user 0.02s system 107% cpu 0.047 total zsh 0.03s user 0.02s system 111% cpu 0.049 total zsh 0.03s user 0.02s system 106% cpu 0.047 total ``` Tests using example config from [getantibody.github.io](https://getantibody.github.io/) ```zsh # example conf for antibody dynamic loading antibody bundle << EOF caarlos0/jvm djui/alias-tips # comments are supported like this caarlos0/zsh-mkc zsh-users/zsh-completions caarlos0/zsh-open-github-pr # empty lines are skipped # remove plugin below because of time-consuming `brew` command # robbyrussell/oh-my-zsh path:plugins/aws zsh-users/zsh-syntax-highlighting zsh-users/zsh-history-substring-search EOF ``` Result: ```zsh # antibody dynamic loading for i ({1..10}) time zsh -ilc echo &>/dev/null zsh 0.05s user 0.03s system 105% cpu 0.083 total zsh 0.06s user 0.03s system 105% cpu 0.083 total zsh 0.05s user 0.03s system 104% cpu 0.082 total zsh 0.05s user 0.03s system 107% cpu 0.081 total zsh 0.05s user 0.03s system 104% cpu 0.082 total zsh 0.05s user 0.03s system 103% cpu 0.081 total zsh 0.06s user 0.03s system 107% cpu 0.083 total zsh 0.05s user 0.03s system 105% cpu 0.082 total zsh 0.05s user 0.03s system 102% cpu 0.082 total zsh 0.06s user 0.03s system 106% cpu 0.082 total # antibody static loading, source .zsh_plugins.sh directly. No auto `compint`. for i ({1..10}) time zsh -ilc echo &>/dev/null zsh 0.05s user 0.02s system 105% cpu 0.069 total zsh 0.05s user 0.03s system 107% cpu 0.071 total zsh 0.05s user 0.03s system 106% cpu 0.071 total zsh 0.05s user 0.03s system 104% cpu 0.070 total zsh 0.05s user 0.03s system 104% cpu 0.070 total zsh 0.05s user 0.03s system 108% cpu 0.072 total zsh 0.05s user 0.03s system 105% cpu 0.070 total zsh 0.05s user 0.03s system 105% cpu 0.070 total zsh 0.05s user 0.03s system 104% cpu 0.071 total zsh 0.05s user 0.03s system 106% cpu 0.072 total # zgen static loading, without `compinit`, ZGEN_AUTOLOAD_COMPINIT=0 for i ({1..10}) time zsh -ilc echo &>/dev/null zsh 0.05s user 0.03s system 105% cpu 0.075 total zsh 0.05s user 0.03s system 104% cpu 0.077 total zsh 0.05s user 0.03s system 106% cpu 0.074 total zsh 0.05s user 0.03s system 104% cpu 0.074 total zsh 0.05s user 0.03s system 105% cpu 0.076 total zsh 0.05s user 0.03s system 103% cpu 0.074 total zsh 0.05s user 0.03s system 108% cpu 0.076 total zsh 0.05s user 0.03s system 104% cpu 0.075 total zsh 0.05s user 0.03s system 107% cpu 0.077 total zsh 0.05s user 0.03s system 102% cpu 0.081 total # zgen source "${HOME}/.zgen/init.zsh" directly # without `compinit`, ZGEN_AUTOLOAD_COMPINIT=0 for i ({1..10}) time zsh -ilc echo &>/dev/null zsh 0.05s user 0.03s system 107% cpu 0.070 total zsh 0.05s user 0.03s system 106% cpu 0.073 total zsh 0.05s user 0.02s system 103% cpu 0.070 total zsh 0.05s user 0.03s system 104% cpu 0.070 total zsh 0.05s user 0.03s system 109% cpu 0.071 total zsh 0.05s user 0.03s system 105% cpu 0.070 total zsh 0.04s user 0.02s system 106% cpu 0.064 total zsh 0.04s user 0.02s system 107% cpu 0.062 total zsh 0.04s user 0.02s system 107% cpu 0.061 total zsh 0.04s user 0.02s system 107% cpu 0.063 total ``` We used 7 plugins here. The result is clear, static loading saves your time. The only difference between static loading in Antibody and Zgen is, Zgen the plugin manager itself needs to be `source`d, the `source` is run by Zgen itself, and Zgen does `compint` for you by default. (Disable auto `compinit` in Zgen with `ZGEN_AUTOLOAD_COMPINIT=0`) Everything else are the same. What they do in static loading is sourcing `*plugin.zsh` into current shell and adding plugin folders into `fpath`. Same formula, different packagings. Zgen implements static loading with only 500 lines ZSH script, with additional Prezto support. #### Interlude Now we have examined plugin mangers trying to reduce the startup time taken by themselves. But the time taken by plugin manger itself is only a fraction of the total startup time. Just as what I wrote in the beginning, a simple `brew --prefix nvm` cost you 600ms, `thefuck --alias` cost you 120ms. No mater how efficient your plugin manger is, you still get that kind of startup delay. Next, I'll examine some other plugin managers trying to **reduce the 2nd kind of time taken by plugins**. Keep tuned. ![To be continued](https://i.imgur.com/5KbbDcd.png) #### [Zulu](https://github.com/zulu-zsh/zulu) Zulu, a plugin manager uses bytecode compiling both for the plugin manager itself and all plugins being loaded. It only supports built-in plugins, no support oh-my-zsh or prezto plugins. **What kind of plugin manger could call itself a plugin manger if it only uses builtin plugins?** Let's be practical, zulu is **just a framework**. A clean startup (without adding any plugins) time is 150ms. Ok, it's **BAD**. Compared with Antigen, zulu has more optimizations but a longer startup time, which means the plugin manager itself cost too much time and the implementation is not efficient. Optimizations: - bytecode compiling for plugin manger itself. Offset by its low efficient coding. - bytecode compiling for plugins #### [Zplug](https://github.com/zplug/zplug) Zplug uses parallel installation/updating, hooks, cache mechanism. It also supports managing scripts and binary from Github release for you. Oh-my-zsh, Prezto and plugins from Github repos are all supported. It's very full-fledged and powerful. The problem is, it's a plugin manger begins with good ideas, like parallel and cache, results in a very, very **BAD implementation**. A clean startup time is 160ms. (Just `source` the plugin manger itself.) According to [issue #368](https://github.com/zplug/zplug/issues/368), [issue #364](https://github.com/zplug/zplug/issues/364), thread from reddit/r/zsh [Zplug is slow?](https://www.reddit.com/r/zsh/comments/6pmr15/zplug_is_slow/): Zplug is slower than Oh-my-zsh, Prezto, Zgen. Antigen. Tests made by myself also confirmed the conclusion, it's slower compared with the prezto, zgen with the same plugins loaded. **The time chart on zplug's Github repo is unreliable**. I'll quote what I said on issue [issue #368](https://github.com/zplug/zplug/issues/368) here: > At first, I thought it as a compatibility problem of prezto modules. But after commenting all the prezto modules, I got a startup time of some 0.6 second in total, which is basically the same startup time when i used prezto. If I enable the prezto modules I use, startup time is about 1 second. > > This is ridiculous because when I used the perzto framework, it loaded more plugins and had a time-consuming initialization of pyenv. Sorry, I didn't mean to be so rude. What irritates me is, the content from zpulg's `README.md` makes you hold your breath, but what I experienced is **totally different** from what they tout. Another drawback brought by the parallel loading is that you have **no definite control of the loading order** of your plugins. Even the plugins loaded with same `defer` value are loaded parallelly. Optimizations: - Negative optimization brought by terrible implementation for plugin manager itself. - Parallel loading, cache mechanism for loading of plugins #### [Zplugin](https://github.com/zdharma/zplugin) Zplugin, another full-fledge plugin manger with script, binary management, reports, completion management, turbo mode, services. - Reports - `zplugin time` list time taken by each plugin - `zplugin report` reports what's going on in the plugins - Scripts management: download and update scripts easily from remote repos - Completion management: enable, disable specific completion easily - **Async Turbo Mode**: the real **killer for time-consuming plugins** Zplugin supports plugins from oh-my-zsh, Prezto, Github repos. At first, it didn't catch my eye because it's less popular compared to zplug, which offers most of the same features. Anyway, I already explained my relationship with zplug. A clean startup time is 50ms. Bytecode compiling is made for manger itself and all plugins. This helps zplugin get a slower startup time increase compared to plugin mangers without bytecode compiling for plugins. > Async Turbo Mode, allows you to postpone loading of a plugin to the moment when processing of `.zshrc` is finished and prompt is being shown. Besides, there are no drawbacks of this approach – no lags, freezes, etc. From the data I offered at the beginning at the post. Zplugin loads my 37 plugins with a startup time of 160ms. The real time taken by all plugins is 1.040s, which means 880ms is reduced in Turbo Mode. Optimizations: - bytecode compiling for plugin manger itself - bytecode compiling for plugins - Amazing Turbo Mode kills the time-consuming plugins like `nvm`, `thefuck`, `pyenv`, `nodenv`, etc. ## FAQ ### Why don't you make a benchmark? Sorry, I won't do it, because I think it's unnecessary. According to my analytical method, any plugin manger not trying to reduce the time taken by plugins, are **not qualified in the finals**. Just as what I said, the startup time taken by plugin manager itself only takes a fraction of the total time. **Time-consuming plugins are the main causes of a slow startup of ZSH**. Among all the plugin managers I've tested, Zplug and Zplugin are the only two trying to solve the hardest part, which brings them to the final round. Although Zplug's implementation is bad, I still hold the opinion that its **parallel mechanism is more innovative** than static loading and bytecode compiling. I'd like to apologize to any plugin manager developer who feels hurt by my word. I do respect their works and they are the men who make the community of ZSH bigger than any other shell. (The community here, is based on the number of plugins and plugin mangers of a interactive shell. [awesome-zsh-plugins](https://github.com/unixorn/awesome-zsh-plugins)) While, I'm just a ZSH user like most other guys, and always want the best. Users don't care which one is better among those mediocre products. Competition is cruel.