Skip to content

Instantly share code, notes, and snippets.

@komori
Forked from laggardkernel/startup-time-of-zsh.md
Created June 23, 2023 17:49
Show Gist options
  • Save komori/3755aab055ae1d4de19cda46e4e2c892 to your computer and use it in GitHub Desktop.
Save komori/3755aab055ae1d4de19cda46e4e2c892 to your computer and use it in GitHub Desktop.
Comparison of ZSH frameworks and plugin managers

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 made by vintersnow@github, detail is covered in the post 最速のZsh プラグインマネージャーを求めて.

Result of speed comparison

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.)

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

The most famous framework with most plugins built-in. For example, autojump, z, fasd are all included, which let users choose whatever they want.

The only framework does optimizations in plugins with sophisticated coding skill, which makes me realize I'm ignorant about 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

(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

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 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 hitchhikes on oh-my-zsh and 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

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 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:

# 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

# 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:

# 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 sourced, 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

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 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, issue #364, thread from reddit/r/zsh 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 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, 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)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment