{"id":13587579,"url":"https://github.com/romkatv/zsh-bench","last_synced_at":"2025-05-15T08:09:18.890Z","repository":{"id":42034712,"uuid":"414479289","full_name":"romkatv/zsh-bench","owner":"romkatv","description":"Benchmark for interactive Zsh","archived":false,"fork":false,"pushed_at":"2025-03-16T09:39:31.000Z","size":316,"stargazers_count":734,"open_issues_count":2,"forks_count":26,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-04-07T04:04:29.078Z","etag":null,"topics":["performance","zsh"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/romkatv.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-07T06:00:35.000Z","updated_at":"2025-04-01T17:49:26.000Z","dependencies_parsed_at":"2023-11-13T08:24:53.115Z","dependency_job_id":"b0622e80-4243-49b0-829e-acf15446f009","html_url":"https://github.com/romkatv/zsh-bench","commit_stats":{"total_commits":211,"total_committers":9,"mean_commits":"23.444444444444443","dds":0.06161137440758291,"last_synced_commit":"661fc46c74fd970f00346d285f5ae434130491f0"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkatv%2Fzsh-bench","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkatv%2Fzsh-bench/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkatv%2Fzsh-bench/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkatv%2Fzsh-bench/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/romkatv","download_url":"https://codeload.github.com/romkatv/zsh-bench/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248889910,"owners_count":21178321,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["performance","zsh"],"created_at":"2024-08-01T15:06:16.442Z","updated_at":"2025-04-14T13:41:20.771Z","avatar_url":"https://github.com/romkatv.png","language":"Shell","funding_links":[],"categories":["Other Resources","Shell"],"sub_categories":["ZSH Tools"],"readme":"# zsh-bench\n\nBenchmark for interactive zsh.\n\n- [Summary](#summary)\n- [Install](#install)\n- [Usage](#usage)\n- [How it works](#how-it-works)\n- [What it measures](#what-it-measures)\n- [How fast is fast](#how-fast-is-fast)\n- [Benchmark results](#benchmark-results)\n  - [Basics](#basics)\n  - [Prompt](#prompt)\n  - [Premade configs](#premade-configs)\n  - [Do it yourself](#do-it-yourself)\n  - [Cutting corners](#cutting-corners)\n  - [Powerlevel10k](#powerlevel10k)\n    - [Instant prompt](#instant-prompt)\n  - [Plugin managers](#plugin-managers)\n  - [Deferred initialization](#deferred-initialization)\n  - [How not to benchmark](#how-not-to-benchmark)\n  - [Full benchmark data](#full-benchmark-data)\n  - [Conclusions](#conclusions)\n  - [Responses](#responses)\n  - [Debugging and validation](#debugging-and-validation)\n- [License](#license)\n- [FAQ](#faq)\n\n## Summary\n\n- `zsh-bench` measures user-visible latency of interactive zsh: *input lag*, *command lag*, etc. You\n  can [use it to benchmark your own shell](#usage).\n- `human-bench` measures human perception of latency when using interactive zsh. You can use it to\n  check how it feels to use zsh with specific latencies or to test whether you can tell a\n  difference between 5ms and 0ms lag.\n- I've used `human-bench` to conduct a blind study on myself to find the maximum values of latencies\n  that are indistinguishable from zero. For example, *command lag* below 10ms feels just like 0ms\n  but anything above this value starts feeling sluggish.\n- I've used these threshold values to normalize benchmark results to see what is fast and what is\n  slow.\n- Armed with this set of tools I've optimized two of my zsh projects:\n  [powerlevel10k](https://github.com/romkatv/powerlevel10k) and\n  [zsh4humans](https://github.com/romkatv/zsh4humans). They used to be fairly fast but now they\n  are literally indistinguishable from instantaneous as far as human perception goes.\n- I've benchmarked many zsh techniques, plugins, frameworks and plugin managers and have shared\n  [my observations](#benchmark-results) in this document together with a brief\n  [conclusion](#conclusions).\n\n## Install\n\nClone the repo:\n\n```zsh\ngit clone https://github.com/romkatv/zsh-bench ~/zsh-bench\n```\n\n## Usage\n\n```text\nusage: zsh-bench [OPTION].. [CONFIG]..\n\nOPTIONS\n  -h,--help\n  -i,--iters \u003cNUM\u003e [default=16]\n  -l,--login \u003cyes|no\u003e [default=yes]\n  -g,--git \u003cyes|no|empty\u003e [default=yes]\n  -c,--config-dir \u003cdirectory\u003e [default=\u003czsh-bench-dir\u003e/configs]\n  -d,--scratch-dir \u003cdirectory\u003e\n  -I,--isolation \u003cdocker|user\u003e\n  -s,--standalone\n  -r,--raw\n```\n\n### Benchmark zsh on your machine\n\n```zsh\n~/zsh-bench/zsh-bench\n```\n\nThis requires zsh \u003e= 5.8 and it must be your login shell.\n\nIf your zsh startup files start `tmux`, the benchmark may hang unless your `tmux` has [this fix](\n  https://github.com/tmux/tmux/commit/9b1fdb291ee8e940311a51cf41f97b07930b4688#diff-2dec3ca953f8622e2bc9fe13a2eb464d057905e6f9313682665328c6b67910e6)\nfor [this bug](https://github.com/tmux/tmux/issues/2909).\n\nIf your zsh startup files enable history but don't set `histignorespace`, you might find random\ncommands in your history after running `zsh-bench`.\n\n### Benchmark predefined zsh configs\n\n```zsh\n~/zsh-bench/zsh-bench --isolation docker -- \u003cname\u003e [name]..\n```\n\nThis requires `docker`. Names of predefined zsh configs are directories under\n[configs](https://github.com/romkatv/zsh-bench/tree/master/configs).\n\nWith `--isolation user` benchmarking is done as user `zsh-bench` on the host.\n\n## How it works\n\n`zsh-bench` creates a virtual TTY and starts a login shell in it. It then sends keystrokes to the\nTTY and measures how long it takes for the shell to react. For example, `zsh-bench` can send\n`echo hello` and `echo goodbye` to the TTY twice in quick succession and measure how long it takes\nfor the words \"hello\" and \"goodbye\" to be printed.\n\n## What it measures\n\nSample output of `zsh-bench`:\n\n```text\ncreates_tty=1\nhas_compsys=1\nhas_syntax_highlighting=1\nhas_autosuggestions=1\nhas_git_prompt=1\nfirst_prompt_lag_ms=14.331\nfirst_command_lag_ms=56.500\ncommand_lag_ms=2.518\ninput_lag_ms=5.195\nexit_time_ms=5.886\n```\n\nThe first few fields list detected shell capabilities; the rest are measured latencies.\n\nShell capabilities (0 or 1):\n\n| name | meaning |\n|------------|---------|\n| **creates tty** | the shell creates its own TTY by invoking `tmux` or `screen` |\n| **has compsys** | the shell initializes `compsys`—the \"new\" completion system—by invoking `compinit` |\n| **has syntax highlighting** | user input (the command line) is highlighted by [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting) |\n| **has autosuggestions** | suggestions for command completions are offered automatically by [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) |\n| **has git prompt** | git branch is displayed in prompt |\n\nLatencies (in milliseconds):\n\n| name | what time it measures | if too high |\n|------|-----------------------|-------------|\n| **first prompt lag (ms)** | from the start of the shell to the moment prompt appears on the screen | you get to stare at an empty screen for some time whenever you open a terminal |\n| **first command lag (ms)** | from the start of the shell to the moment the first interactive command starts executing | you get to wait for the output of `ls` if you type it really fast after opening a terminal |\n| **command lag (ms)** | from pressing \u003ckbd\u003eEnter\u003c/kbd\u003e on an empty command line to the moment the next prompt appears; the same as [zsh-prompt-benchmark](https://github.com/romkatv/zsh-prompt-benchmark) (my project) | all commands appear to take longer to execute; the slowdown may happen after you press \u003ckbd\u003eEnter\u003c/kbd\u003e and before the command starts executing, or after the command finishes executing and before the next prompt appears |\n| **input lag (ms)** | from pressing a regular key to the moment the corresponding character appears on the command line; this test is performed when the current command line is already fairly long | keyboard input feels sluggish, as if you are working over an SSH connection with high latency |\n| **exit time (ms)** | how long it takes to execute `zsh -lic \"exit\"`; this value is [meaningless](#how-not-to-benchmark) as far as measuring interactive shell latencies goes | there is no baseline value for this latency, so it cannot be \"too high\" |\n\n## How fast is fast\n\nIs *input lag* of 5ms a lot? What about *first prompt lag* of 100ms? How good/bad would it\nbe if these latencies were halved/doubled? I've written `human-bench` to answer questions like this.\nIt's a small tool that can simulate zsh with latencies of your choice.\n\n```text\nusage: human-bench [OPTION]..\n\nOPTIONS\n  -h,--help\n  -s,--shell-command \u003cSTR\u003e [default=\"zsh\"]\n  -f,--first-prompt-lag-ms \u003cNUM\u003e [default=0]\n  -c,--first-command-lag-ms \u003cNUM\u003e [default=0]\n  -p,--command-lag-ms \u003cNUM\u003e [default=0]\n  -i,--input-lag-ms \u003cNUM\u003e [default=0]\n```\n\nIt turns out that *first prompt lag* of 100ms causes the first prompt to appear with a noticeable\ndelay when starting zsh but *input lag* of 5ms is barely perceptible. Or is it imperceptible?\nWhen I invoke `human-bench --input-lag-ms 5` I expect input to lag and this might affect what I'm\nseeing. To rule out this bias I've extended `human-bench` to accept several values of the same\nlatency:\n\n```zsh\nhuman-bench --input-lag-ms 0 --input-lag-ms 5\n```\n\n`human-bench` picks one of these latencies at random before starting zsh. However, it doesn't\nreveal the choice until I exit the playground. With this tool in hand I conducted a blind study on\nmyself and found out that I cannot distinguish between these two latencies. As far as my senses are\nconcerned, *input lag* of 5ms is as good as zero.\n\nI used this blinding method to find the threshold values of all latencies in my use of zsh. Any\nvalue below the threshold is—to me—indistinguishable from zero. I can distinguish values above the\nthreshold from zero with better than 50% accuracy.\n\n| latency (ms)          | the maximum value indistinguishable from zero |\n|-----------------------|----------------------------------------------:|\n| **first prompt lag**  |                                            50 |\n| **first command lag** |                                           150 |\n| **command lag**       |                                            10 |\n| **input lag**         |                                            20 |\n\nThe first two latencies are related to zsh startup time. I don't start zsh by literally typing `zsh`\nwithin an existing shell. I either open a terminal, create a new tab, or split an existing tab. The\nlatter is the most common, so I rigged `human-bench` to split a tab for the purpose of testing\nstartup latencies. For the other two latencies I typed and executed simple commands.\n\nKeep in mind that these thresholds may have different values for different people, machines,\nterminals, etc. I believe the ballpark should be the same though.\n\n## Benchmark results\n\nI implemented `zsh-bench` in order to optimize [powerlevel10k](https://github.com/romkatv/powerlevel10k)\nand [zsh4humans](https://github.com/romkatv/zsh4humans). In the process I benchmarked many zsh\ntechniques, plugins, frameworks and plugin managers. I'm sharing some of my findings here.\n\nI recommend reading this section top-to-bottom without jumping back and forth. You can also skip\nright to [conclusions](#conclusions).\n\nAll benchmark results in this section have been normalized by the\n[threshold values](#how-fast-is-fast). *first prompt lag* of 25ms becomes 50% and 100ms becomes\n200%. Latencies up to 50%, 100% and 200% are be marked with 🟢, 🟡 and 🟠, respectively. Latencies\nabove 200% get 🔴. Note that 🟡 is actually *really good*. It means the latency is\nindistinguishable from zero. A zsh config with all latencies marked green or yellow performs as if\nthere were no latencies at all. I'm reserving 🟢 for latencies under *half* of this ambitious\nthreshold because it's nice to have a bit of headroom. I might add extra stuff to my zsh configs or\nmaybe I'll run zsh on a slower machine. I wouldn't want this to push my latencies outside of the\nimperceptible range. So, green latencies aren't just imperceptible but also leave enough unused\nlatency budget.\n\n### Basics\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [no-rcs](https://github.com/romkatv/zsh-bench/tree/master/configs/no-rcs) | ❌ | ❌ | ❌ | ❌ | ❌ | 3%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n| [tmux](https://github.com/romkatv/zsh-bench/tree/master/configs/tmux) | ✔️ | ❌ | ❌ | ❌ | ❌ | 8%\u003cbr\u003e🟢 | 3%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n| [compsys](https://github.com/romkatv/zsh-bench/tree/master/configs/compsys) | ❌ | ✔️ | ❌ | ❌ | ❌ | 37%\u003cbr\u003e🟢 | 12%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n| [zsh-syntax-highlighting](https://github.com/romkatv/zsh-bench/tree/master/configs/zsh-syntax-highlighting) | ❌ | ❌ | ✔️ | ❌ | ❌ | 23%\u003cbr\u003e🟢 | 14%\u003cbr\u003e🟢 | 6%\u003cbr\u003e🟢 | 57%\u003cbr\u003e🟡 |\n| [zsh-autosuggestions](https://github.com/romkatv/zsh-bench/tree/master/configs/zsh-autosuggestions) | ❌ | ❌ | ❌ | ✔️ | ❌ | 32%\u003cbr\u003e🟢 | 13%\u003cbr\u003e🟢 | 96%\u003cbr\u003e🟡 | 3%\u003cbr\u003e🟢 |\n| [git-branch](https://github.com/romkatv/zsh-bench/tree/master/configs/git-branch) | ❌ | ❌ | ❌ | ❌ | ✔️ | 32%\u003cbr\u003e🟢 | 11%\u003cbr\u003e🟢 | 50%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n\n**no-rcs** is zsh in its pure form, without any\n[rc files](https://zsh.sourceforge.io/Intro/intro_3.html). It's really fast! Even if it was 10 times\nslower, I wouldn't be able to tell a difference.\n\nThe rest of the entries here are the simplest configs capable of providing each capability. For\nexample, here's `.zshrc` from **zsh-autosuggestions**:\n\n```zsh\nsource ~/zsh-autosuggestions/zsh-autosuggestions.zsh\n```\n\nJust one line. Plain and simple. `~/zsh-autosuggestions` is supposed to be created manually, outside\nof zsh rc files. In the benchmark it's done by a `setup` script like this:\n\n```zsh\ngit clone -q --depth=1 https://github.com/zsh-users/zsh-autosuggestions.git ~/zsh-autosuggestions\n```\n\nThese basic building blocks are composable. We can easily create a config by combining any number\nof them. Later we'll be doing just that. The latencies of a combination are the sums of latencies\nof all its constituents. For example, *first prompt lag* of **tmux+compsys** is *first prompt lag*\nof **tmux** plus *first prompt lag* of **compsys**. You can probably already see that adding\neverything together will push some latencies over the threshold. Our goal is to avoid that while\nstill getting all the goodies. **git-branch** gives us *git prompt* for the price of 48% of our\n*command lag* budget. Let's see if we can do better.\n\n### Prompt\n\nI've benchmarked several different git prompts.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [git-branch](https://github.com/romkatv/zsh-bench/tree/master/configs/git-branch) | ❌ | ❌ | ❌ | ❌ | ✔️ | 32%\u003cbr\u003e🟢 | 11%\u003cbr\u003e🟢 | 50%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n| [agnoster](https://github.com/romkatv/zsh-bench/tree/master/configs/agnoster) | ❌ | ❌ | ❌ | ❌ | ✔️ | 65%\u003cbr\u003e🟡 | 22%\u003cbr\u003e🟢 | 244%\u003cbr\u003e🔴 | 1%\u003cbr\u003e🟢 |\n| [starship](https://github.com/romkatv/zsh-bench/tree/master/configs/starship) | ❌ | ❌ | ❌ | ❌ | ✔️ | 82%\u003cbr\u003e🟡 | 28%\u003cbr\u003e🟢 | 354%\u003cbr\u003e🔴 | 1%\u003cbr\u003e🟢 |\n| [powerlevel10k](https://github.com/romkatv/zsh-bench/tree/master/configs/powerlevel10k) | ❌ | ❌ | ❌ | ❌ | ✔️ | 4%\u003cbr\u003e🟢 | 14%\u003cbr\u003e🟢 | 19%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n\nThe git repo used by the benchmark has 1,000 directories and 10,000 files in it. Not too few,\nnot too many. All benchmarks ran with untracked cache enabled. Wall time of `git status` stood at\n16ms.\n\n**git-branch** only shows the name of the current branch and has the same latency regardless of the\nrepository size.\n\n**agnoster** config uses the classic\n[agnoster zsh theme](https://github.com/agnoster/agnoster-zsh-theme). It scans the whole repo to see\nif there are untracked files, unstaged changes, etc. We can see that this causes lag on every\ncommand pushing latency into the red. The lag is linear in the number of files and directories in\nthe git repo. You wouldn't want to use this theme in a truly large git repo with hundreds of\nthousands or millions of files.\n\n**starship** config uses the cross-shell [starship prompt](https://github.com/starship/starship).\nIt suffers from the same performance problems as agnoster plus some. Starship is implemented as an\nexternal binary, so it has to pay the price of *at least one* additional fork+exec on every command\ncompared to native zsh prompts. One fork+exec cannot account for the high lag starship exhibits, so\nwhat gives? Under the benchmark conditions starship\n[clones](https://man7.org/linux/man-pages/man2/clone.2.html) 158 times! That's costly.\n\n**powerlevel10k** config uses [powerlevel10k zsh theme](https://github.com/romkatv/powerlevel10k)\nthat I've developed. It scans the git repo just like agnoster and starship but it does not invoke\n`git` to do that. Instead, it uses [gitstatus](https://github.com/romkatv/gitstatus) -- another of\nmy projects. This gives powerlevel10k a nice speedup on repositories large and small. In addition,\npowerlevel10k doesn't block zsh prompt while gitstatus is scanning the repo, so *command lag* stays\nconstant even in giant repositories. Powerlevel10k has a few other interesting performance-related\nproperties that we'll [explore](#powerlevel10k) when we start building real zsh configs.\n\n### Premade configs\n\nLet's see what some of the popular premade zsh configs offer out of the box.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [prezto](https://github.com/romkatv/zsh-bench/tree/master/configs/prezto) | ❌ | ✔️ | ❌ | ❌ | ❌ | 97%\u003cbr\u003e🟡 | 35%\u003cbr\u003e🟢 | 13%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n| [ohmyzsh](https://github.com/romkatv/zsh-bench/tree/master/configs/ohmyzsh) | ❌ | ✔️ | ❌ | ❌ | ✔️ | 187%\u003cbr\u003e🟠 | 64%\u003cbr\u003e🟡 | 366%\u003cbr\u003e🔴 | 2%\u003cbr\u003e🟢 |\n| [zim](https://github.com/romkatv/zsh-bench/tree/master/configs/zim) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | 122%\u003cbr\u003e🟠 | 53%\u003cbr\u003e🟡 | 191%\u003cbr\u003e🟠 | 64%\u003cbr\u003e🟡 |\n| [zsh4humans](https://github.com/romkatv/zsh-bench/tree/master/configs/zsh4humans) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 19%\u003cbr\u003e🟢 | 36%\u003cbr\u003e🟢 | 27%\u003cbr\u003e🟢 | 25%\u003cbr\u003e🟢 |\n\nThe names of these configs match the respective public projects from which they were copied:\n[ohmyzsh](https://github.com/ohmyzsh/ohmyzsh), [prezto](https://github.com/sorin-ionescu/prezto),\n[zim](https://github.com/zimfw/zimfw) and [zsh4humans](https://github.com/romkatv/zsh4humans). The\nlatter is my project. All configs were used unmodified.\n\n**prezto** is fast but doesn't provide much out of the box. No syntax highlighting, autosuggestions\nor git prompt. Users who need these features—most do—should enable them explicitly.\n\n**ohmyzsh** and **zim** by default use a theme with similar performance characteristics of\n**agnoster**, so they have high *command lag* in larger git repositories. **zim** is the only config\namong these that doesn't detect untracked files. This appears to have been a conscious decision by\n**zim** developers for performance reasons. We can see that it helps: **zim** has approximately\nhalf the *command lag* of **ohmyzsh**.\n\n**zim** enables syntax highlight and autosuggestions by default, so it naturally has higher *input\nlag* than projects that don't. Fast zsh startup is a major [explicit goal](\n  https://github.com/zimfw/zimfw/wiki/Speed) of zim and we\ncan see that it beats **ohmyzsh** on *first prompt lag* and *first command lag*.\n\n**zsh4humans** ticks all capability checkboxes and has all latencies comfortably in the green zone.\nThis shouldn't be surprising. In the game of optimization, measuring is half the work. I had access\nto `zsh-bench`, so I was able to optimize zsh4humans to score well on it. Prior to creating\n`zsh-bench` I knew that *input lag* and *first command lag* in zsh4humans were sometimes noticeable\nbut it was difficult to evaluate the effectiveness of potential optimizations when I not only\ncouldn't measure these latencies but didn't even have clear concepts for them.\n\nNote that all these projects have extra features that aren't reflected in the capabilities shown in\nthe table. They all enable persistent history, define aliases, set shell options, etc.\n\n### Do it yourself\n\nLet's leave premade configs alone for some time and try to build a zsh config from scratch. Given\nthe availability of high-quality building blocks, this shouldn't be very difficult.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [diy](https://github.com/romkatv/zsh-bench/tree/master/configs/diy) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 118%\u003cbr\u003e🟠 | 47%\u003cbr\u003e🟢 | 155%\u003cbr\u003e🟠 | 61%\u003cbr\u003e🟡 |\n| [diy+](https://github.com/romkatv/zsh-bench/tree/master/configs/diy%2B) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 51%\u003cbr\u003e🟡 | 24%\u003cbr\u003e🟢 | 63%\u003cbr\u003e🟡 |\n| [diy++](https://github.com/romkatv/zsh-bench/tree/master/configs/diy%2B%2B) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 42%\u003cbr\u003e🟢 | 24%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n\n**diy** is the simplest config that provides all capabilities. I've made it by concatenating configs\nof the [basic building blocks](#basics). Here's the whole `.zshrc`:\n\n```zsh\n# If not in tmux, start tmux.\nif [[ -z ${TMUX+X}${ZSH_SCRIPT+X}${ZSH_EXECUTION_STRING+X} ]]; then\n  exec tmux\nfi\n\n# Enable the \"new\" completion system (compsys).\nautoload -Uz compinit \u0026\u0026 compinit\n\n# Configure prompt to show the current working directory and git branch.\nautoload -Uz vcs_info add-zsh-hook\nadd-zsh-hook precmd vcs_info\nPS1='%~ $vcs_info_msg_0_'\nsetopt prompt_subst\n\n# Enable syntax highlighting.\nsource ~/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh\n# Enable autosuggestions.\nsource ~/zsh-autosuggestions/zsh-autosuggestions.zsh\n```\n\nTwo latencies are over the threshold, so some lag can be noticeable. However, before using this\nconfig you'll want to add more stuff to it -- key bindings, environment variables, aliases,\ncompletions options, etc. All these things will make zsh slower. If you aren't careful, a lot\nslower.\n\n**diy+** improves on **diy** by replacing its prompt with [powerlevel10k](#powerlevel10k). This has\ndramatic positive effect on *first prompt lag* and *command lag*. Moreover, *first prompt lag*\nis now constant and won't increase if more stuff is added to the config. This means you'll never\nhave to stare at an empty screen when opening terminal -- prompt will be there right from the start.\nThe additional initialization code will only affect *first command lag*, which has the highest\nthreshold of all latencies and has the least impact on the perception of zsh performance. I've also\nmade `.zshrc` in this config self-bootstrapping to obviate the need to maintain a separate `install`\nor `setup` script for the cloning of zsh plugin repositories. This is primarily to make comparisons\nwith plugin managers in the future sections easier.\n\n**diy++** adds one more optimization -- it compiles large zsh files to wordcode. This reduces\n*first command lag* a little bit. This config performs well and is still\n[relatively simple](https://github.com/romkatv/zsh-bench/blob/master/configs/diy%2B%2B/skel/.zshrc).\n\n### Cutting corners\n\nThere are several optimizations that speed up zsh startup but can easily backfire. **diy++unsafe**\nadds three such optimizations on top of **diy++** to reduce *first command lag* by 5% of the budget.\nI don't recommend them.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [diy++unsafe](https://github.com/romkatv/zsh-bench/tree/master/configs/diy%2B%2Bunsafe) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 9%\u003cbr\u003e🟢 | 37%\u003cbr\u003e🟢 | 24%\u003cbr\u003e🟢 | 63%\u003cbr\u003e🟡 |\n\nThe first optimization is to compile to wordcode `.zshrc` itself. This will cause you a lot of grief\nif you do something like this:\n\n```zsh\n% cp ~/.zshrc ~/.zshrc.bak  # backup .zshrc before messing with it\n% vi ~/.zshrc               # change stuff\n% exec zsh                  # restart zsh to try the new changes\n% mv ~/.zshrc.bak ~/.zshrc  # revert changes\n% exec zsh                  # restart zsh\n```\n\nAt this point you'll be surprised to find that the last command seemingly had no effect. Zsh is\nstill using `.zshrc` that you modified in `vi`. This happens because `mv` preserves file\nmodification time and zsh looks at it to figure out whether the wordcode matches the source code.\n\nCompiling `.zshrc` to wordcode will also prevent you from using aliases in `.zshrc` if they were\ndefined in the same file. For example:\n\n```zsh\nalias ll='ls -l'\nlll() { ll \"$@\" | less; }\n```\n\nIf you put this code in `.zshrc`, you'll notice that `lll` doesn't work if `.zshrc` is compiled.\n\nAnother dangerous optimization is to invoke `compinit` with `-C`. If you do that and install a new\ntool using your favorite package manager, completions for this tool may not appear in zsh even after\nrestart. You'll have to manually delete a cache file. Saving a few milliseconds on zsh startup is\nnot worth it if later you'll have to spend an hour trying to figure out why completions don't work.\n\nThe last over-eager optimization is to print the first prompt before checking whether plugins\nneed to be installed. The section on [Instant Prompt](#instant-prompt) explains why this is a bad\nidea.\n\n### Powerlevel10k\n\nBefore going further let's look at powerlevel10k more closely. This theme can display a lot of\ninformation in prompt: disk usage, public IP address, VPN status, current kubernetes context,\ntaskwarrior task count, etc. The configuration of powerlevel10k affects its latency. All benchmarked\nconfigs that use powerlevel10k employ the same small config that only shows the current working\ndirectory and git status in prompt. In addition to this I've also measured (and optimized -- this\nwas the whole point of working on `zsh-bench`) the performance of powerlevel10k with *everything*\nturned on.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [powerlevel10k](https://github.com/romkatv/zsh-bench/tree/master/configs/powerlevel10k) | ❌ | ❌ | ❌ | ❌ | ✔️ | 4%\u003cbr\u003e🟢 | 14%\u003cbr\u003e🟢 | 19%\u003cbr\u003e🟢 | 1%\u003cbr\u003e🟢 |\n| [powerlevel10k-full](https://github.com/romkatv/zsh-bench/tree/master/configs/powerlevel10k-full) | ❌ | ❌ | ❌ | ❌ | ✔️ | 8%\u003cbr\u003e🟢 | 27%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 | 6%\u003cbr\u003e🟢 |\n\n**powerlevel10k-full** has substantially higher *command lag* but it's still under 100%, meaning\nthat prompt is still indistinguishable from instantaneous. However, there is not much *command lag*\nbudget left for doing extra things on every command. So you would need to be careful with your zsh\nconfig if you were to use powerlevel10k with everything turned on. In practice, no sane person would\nenable *everything*. Here's a ridiculously overwrought prompt:\n\n![Powerlevel10k Extravagant Style](\n  https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/extravagant-style.png)\n\nIt has **18** segments. The full config enables **64**!\n\n**powerlevel10k-full** increases *input lag* by 1.2ms compared to the smaller config.\nPowerlevel10k can dynamically update prompt depending on the current command you are typing. Here's\nan example from powerlevel10k docs where the current kubernetes context and gcloud credentials are\nshown only when they are relevant to the current command.\n\n\u003cdetails\u003e\n  \u003csummary\u003ePowerlevel10k Show On Command\u003c/summary\u003e\n\n  ![Powerlevel10k Show On Command](\n    https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/show-on-command.gif)\n\u003c/details\u003e\n\nThis feature requires parsing the command line as it changes, hence extra *input lag*. The impact\non latency is small, so it shouldn't cause any problems.\n\n#### Instant prompt\n\nI mentioned earlier that powerlevel10k makes *first prompt lag* small and, importantly, independent\nfrom anything else you have in zsh startup files. This feature is called *Instant Prompt* in\npowerlevel10k docs (a more appropriate name would have been *Instant **First** Prompt*) and it's\nworth looking at how it works.\n\nWhen you open the homepage of Google in a web browser, it appears to load almost instantly even\nthough there is a lot of fancy functionality built into it. If we look under the hood, the whole\npage takes a long time to load but most of this loading happens after the UI has been rendered. It\ndoesn't take much time to render an input box for the query and the search button, so it looks\ninstantaneous. The initial UI may look like the real thing but it's only a stub. If you enter a\nquery and click the button quickly enough, search results won't appear. The button doesn't have the\nnecessary logic yet, so it'll just remember that it was clicked. The query will go through once the\npage loads.\n\nThis trick works really well because you can start typing right away. Typing is very slow by machine\nstandards, so by the time you are finished the page has almost always been fully loaded and you\ndon't notice any delay when clicking the button.\n\nPowerlevel10k uses the same trick, only in case of zsh the UI you see on startup is the prompt.\nAs soon as you open a terminal, powerlevel10k prints prompt. This first prompt only has information\nthat can be computed quickly (the current working directory, username, hostname, current time,\npython virtual environment, etc.) but nothing that can require a lot of time, so no git status.\nWhile you are typing the first command, zsh continues to initialize -- loading plugins, setting up\ncompletions, defining aliases, enabling key bindings, retrieving git status, etc. Once zsh is fully\ninitialized, the original limited prompt is replaced with the full prompt and whatever you have\ntyped is replayed in Zsh Line Editor (zle). If you've enabled syntax highlighting, at this point the\ncommand line gets highlighted. Here's how it looks:\n\n\u003cdetails\u003e\n  \u003csummary\u003ePowerlevel10k Instant Prompt\u003c/summary\u003e\n\n  ![Powerlevel10k Instant Prompt](\n    https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/instant-prompt.gif)\n\u003c/details\u003e\n\nIf you press \u003ckbd\u003eEnter\u003c/kbd\u003e before zsh is fully initialized, the command won't execute. It\n*cannot* execute because execution relies on aliases, environment variables and whatnot, but those\nthings haven't been defined yet. The command will execute once zsh finishes initializing. Until then\nit'll look like lag, as if the command takes a long time to start. You'll notice the same \"lag\" if\nyou use a key binding such as \u003ckbd\u003eTab\u003c/kbd\u003e for a completion or \u003ckbd\u003eCtrl+R\u003c/kbd\u003e for interactive\nhistory search.\n\nInitializing zsh while you are typing the first command poses a problem. What if some part of\n`.zshrc` prints to the terminal? The output will appear smack in the middle of what you perceive as\nthe command line. That wouldn't be pretty at all. And what if `.zshrc` asks you a question?\n\n```text\nDisk usage seems kinda high. Delete your home directory? [y/N]\n```\n\nThe buffered input that was intended to go to Zsh Line Editor will be read by the disk cleaner. If\nthe first command you are typing starts with \"y\", say goodbye to your files.\n\nTo avoid these issues, for the duration of zsh initialization powerlevel10k redirects standard input\nto `/dev/null` and standard output with standard error to a temporary file. Once zsh is fully\ninitialized, standard file descriptors are restored and the content of the temporary file is printed\nout. This content appears *above* the first prompt. This is much better than letting the output\ninterleave with the command line but it's still not pretty.\n\nFor best results, when using *Instant Prompt*, `.zshrc` should be structured like so:\n\n1. The first section is for commands that either:\n   - read from standard input or the TTY\n   - write to standard output, standard error or the TTY\n   - occasionally (but rarely) may take unpredictably long time to execute\n   ```zsh\n   # If not in tmux, start tmux: reads from and writes to the TTY.\n   if [[ -z ${TMUX+X}${ZSH_SCRIPT+X}${ZSH_EXECUTION_STRING+X} ]]; then\n     exec tmux\n   fi\n\n   # Clone git repos that don't exist: prints and may take unpredictably long time to execute.\n   if [[ ! -e ~/zsh-autosuggestions ]]; then\n     print -r -- 'installing zsh-autosuggestions ...'\n     git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions.git ~/zsh-autosuggestions\n   fi\n\n   # Prints.\n   print -Pr -- 'Hello, %n. Today is %D{%A}.'\n\n   # ... and so on\n   ```\n2. Activate *Instant Prompt*. This must be done with this exact command.\n   ```zsh\n   # Print the first prompt and redirect standard file descriptors.\n   if [[ -r \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\" ]]; then\n     source \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\"\n   fi\n   ```\n3. The last section is for the bulk of initialization. These commands must not:\n     - read from standard input or the TTY\n     - write to standard output, standard error or the TTY\n     - take unpredictably long time to execute\n   ```zsh\n    autoload -Uz compinit \u0026\u0026 compinit\n    source ~/zsh-autosuggestions/zsh-autosuggestions.zsh\n    # ... and so on\n   ```\n\nCommands in the first section must be very fast to avoid delaying the first prompt.\n\nIt's OK for commands in the last section to print *in case of errors*. The assumption is that you'll\nfix these errors, so in normal operation there won't be any output.\n\nIf you need to use the TTY in the last section, there is `$TTY` for you. Just make sure you aren't\nreading anything from it and only writing \"invisible\" things that don't appear on the screen. This\nis OK:\n\n```zsh\n# Let gpg know what our TTY is.\nexport GPG_TTY=$TTY\n# Change the cursor shape to \"beam\".\nprint -n '\\e[5 q' \u003e$TTY\n# Display the current working directory in the terminal title.\nprintf '\\e]0;%s\\a' ${(V)${(%):-%1~}} \u003e$TTY\n```\n\n### Plugin managers\n\n**diy++** is a solid base for a zsh config that gives us full control over the initialization\nprocess. By using a plugin manager we can give up some of this control for convenience. I've\nbenchmarked several plugin managers and frameworks. All configs here have all core capabilities.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [diy++](https://github.com/romkatv/zsh-bench/tree/master/configs/diy%2B%2B) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 42%\u003cbr\u003e🟢 | 24%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n| [diy++unsafe](https://github.com/romkatv/zsh-bench/tree/master/configs/diy%2B%2Bunsafe) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 9%\u003cbr\u003e🟢 | 37%\u003cbr\u003e🟢 | 24%\u003cbr\u003e🟢 | 63%\u003cbr\u003e🟡 |\n| [zcomet](https://github.com/romkatv/zsh-bench/tree/master/configs/zcomet) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 44%\u003cbr\u003e🟢 | 25%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n| [zinit](https://github.com/romkatv/zsh-bench/tree/master/configs/zinit) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 78%\u003cbr\u003e🟡 | 24%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n| [zplug](https://github.com/romkatv/zsh-bench/tree/master/configs/zplug) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 108%\u003cbr\u003e🟠 | 100%\u003cbr\u003e🟡 | 24%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n| [ohmyzsh+](https://github.com/romkatv/zsh-bench/tree/master/configs/ohmyzsh%2B) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 56%\u003cbr\u003e🟡 | 29%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n| [prezto+](https://github.com/romkatv/zsh-bench/tree/master/configs/prezto%2B) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 47%\u003cbr\u003e🟢 | 34%\u003cbr\u003e🟢 | 68%\u003cbr\u003e🟡 |\n| [zim+](https://github.com/romkatv/zsh-bench/tree/master/configs/zim%2B) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 10%\u003cbr\u003e🟢 | 38%\u003cbr\u003e🟢 | 24%\u003cbr\u003e🟢 | 64%\u003cbr\u003e🟡 |\n| [zsh4humans](https://github.com/romkatv/zsh-bench/tree/master/configs/zsh4humans) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 19%\u003cbr\u003e🟢 | 36%\u003cbr\u003e🟢 | 27%\u003cbr\u003e🟢 | 25%\u003cbr\u003e🟢 |\n\n**diy++** and **diy++unsafe** are listed here to serve as baseline for comparing latency.\n\nThe next three configs use \"pure\" plugin managers: [zcomet](https://github.com/agkozak/zcomet),\n[zinit](https://github.com/zdharma-continuum/zinit) and [zplug](https://github.com/zplug/zplug).\nThese allow you to install and load plugins but don't configure zsh on their own in any way.\n\nConfigs **ohmyzsh+**, **prezto+** and **zim+** are based on the respective standard configs. I've\nenabled only the plugins that are required to tick off all capabilities and disabled the rest.\n\n**zsh4humans** can install and load arbitrary plugins but the default config already enables\neverything we care about. So I'm benchmarking the stock config with no changes here.\n\nMost configs in this list treat `compinit`, [zsh-syntax-highlighting](\n  https://github.com/romkatv/zsh-bench/tree/master/configs/zsh-syntax-highlighting),\n[zsh-autosuggestions](https://github.com/romkatv/zsh-bench/tree/master/configs/zsh-autosuggestions)\nand [powerlevel10k](https://github.com/romkatv/zsh-bench/tree/master/configs/powerlevel10k) as black\nboxes. They cannot beat **diy++unsafe** on benchmarks.\n\n**zplug**, **ohmyzsh** and **prezto** trade usability for performance by invoking `compinit` with\n`-C`. This is not a good choice in my opinion. The small speedup isn't worth it. **zsh4humans** and\n**zim** go the opposite way -- they perform *more* checks than the regular `compinit` in order to\nimprove usability of the completion system. For example, if you add a completion function to an\nexisting file with a `#compdef` directive, none of the configs except for **zsh4humans** and **zim** will\npick up this change until you manually delete `.zcompdump`. Restarting zsh or even rebooting the\nmachine won't help. **zsh4humans** manages to achieve *first command lag* lower than any other\nconfig despite the additional quality of life improvements. This shows that cutting corners isn't\nnecessary for achieving low latency goals.\n\nAll configs have very low *first prompt lag* thanks to powerlevel10k. The only exception is\n**zplug**. **zplug** provides a nice API that plays well with [Instant Prompt](#instant-prompt). It\nhas one function to install plugins and another to load them. Installation of plugins can print\nstatus messages and perform network I/O, so it should be done before the first prompt is printed.\nUnfortunately, this function in **zplug** is rather slow, hence high *first prompt lag*. **zcomet**,\nand **zinit** dodge this bullet by not providing this kind of API in the first place, so\nthese configs are [cutting corners](#cutting-corners). It's not that **zcomet** and **zinit** are\nacting recklessly here but rather their limitations don't allow me to cleanly use *Instant Prompt*.\nWhen using these plugin managers you either have to give up *Instant Prompt* and have\n*first prompt lag* over the threshold or cut corners and get subpar UX.\n\n**zsh4humans** has lower *first command lag* and *input lag* than anything else. It achieves this by\nimplementing tight integration between the core shell features: prompt, syntax highlighting\nand autosuggestions. You can enable *extra* plugins in **zsh4humans** but the core comes as a single\nunit.\n\n**zsh4humans** has *first prompt lag* 9% (4.7ms in absolute terms) higher than **diy++**. A lot of\nfeatures are packed into that chunk of time but this isn't the place to describe them. The resulting\n*first prompt lag* is still just 20% of the threshold of perception, so I'm feeling pretty secure\nthat this latency won't be noticeable. Importantly, when users add extra initialization code to\ntheir zsh startup files, it doesn't increase *first prompt lag*. It increases only\n*first command lag*, which **zsh4humans** has at a lower value than other configs. Overall I'm\nvery happy with where **zsh4humans** stands.\n\nIf you don't care about `tmux`, you can mentally subtract\n[its latencies](https://github.com/romkatv/zsh-bench/blob/master/doc/benchmarks.md) from any row in\nthe table. Given that **zplug** is only slightly over 100% on two metrics, subtracting **tmux** from\nit brings all latencies in the table into the green or yellow territory. Everything is pretty fast!\nUnderstanding the differences in functionality is what really matters for an informed choice. This\ndocument is only about performance though, so I won't go into it.\n\n### Deferred initialization\n\nIt's possible to defer some parts of zsh initialization and perform them when zsh has nothing else\nto do. This can be done with [zinit turbo mode](\n  https://github.com/zdharma-continuum/zinit#turbo-and-lucid) or [zsh-defer](\n    https://github.com/romkatv/zsh-defer). The latter is my project.\n\n| config | tmux | compsys | syntax highlight | auto suggest | git prompt | first prompt lag | first cmd lag | cmd lag | input lag |\n|-|-:|-:|-:|-:|-:|-:|-:|-:|-:|\n| [zinit-turbo](https://github.com/romkatv/zsh-bench/tree/master/configs/zinit-turbo) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | 9%\u003cbr\u003e🟢 | 39%\u003cbr\u003e🟢 | 24%\u003cbr\u003e🟢 | 62%\u003cbr\u003e🟡 |\n| [zsh-defer](https://github.com/romkatv/zsh-bench/tree/master/configs/zsh-defer) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | 10%\u003cbr\u003e🟢 | 22%\u003cbr\u003e🟢 | 28%\u003cbr\u003e🟢 | 65%\u003cbr\u003e🟡 |\n\nIn these configs the initialization of syntax highlighting and autosuggestions was deferred. When\ndeferring initialization of some features, you have to be prepared to use zsh without those features\nfor some time. The benchmark results indicate that the first command of the interactive shell\ndidn't have syntax highlighting or autosuggestions. This makes sense. Zsh was busy processing the\nfirst command in Zsh Line Editor and hasn't reached the state of having nothing to do before the\ncommand has started executing.\n\nInitialization of the vast majority of features is unsafe to defer. Anything that modifies\nenvironment variables, defines commands or changes the behavior of widgets shouldn't be deferred.\n\nThe only feature I know of whose initialization can be safely deferred is syntax highlighting.\nAutosuggestions must be initialized *after* syntax highlighting, so you would have to defer both of\nthem or none. Unfortunately, deferring the initialization of autosuggestions is unsafe because it\nchanges the behavior of some keys, so you cannot use autosuggestions if you defer syntax\nhighlighting.\n\nDeferred initialization runs within the context of Zsh Line Editor (zle). Some plugins don't expect\nto be loaded from zle and may fail to properly initialize. Loading such plugins from zle requires\nworkarounds that often rely on the plugin's implementation details. This exposes the user to much\nhigher risk of breakage when updating plugins.\n\nDeferred initialization can reduce only *first cmd lag*. If done properly, it has no effect on\nother latencies. Given that there are many configs to choose from that are below the perception\nthreshold on *first cmd lag*, deferred initialization doesn't solve any real problems while adding\nquite a few of its own.\n\nSo much for deferred initialization. Cannot recommend.\n\n### How not to benchmark\n\nIf you search online for tips on how to benchmark zsh startup, you'll find the following command or\na variation thereof:\n\n```zsh\ntime zsh -lic \"exit\"\n```\n\nFor the sake of completeness, `zsh-bench` also measures this. This metric is shown as\n*exit_time_ms* in the raw output. Let's look at a couple of raw benchmark results that pertain to\nzsh startup speed.\n\n| config | first prompt lag (ms) | first command lag (ms) | exit time (ms) |\n|-|-:|-:|-:|\n| [agnoster](https://github.com/romkatv/zsh-bench/tree/master/configs/agnoster) | 32 | 33 | 2 |\n| [powerlevel10k](https://github.com/romkatv/zsh-bench/tree/master/configs/powerlevel10k) | 2 | 21 | 6 |\n\nWhen using **agnoster**, `exit` finishes in just 2ms. Yet, when you open a terminal, you'll be\nlooking at an empty screen for 32ms. What exactly happens on the 2ms mark that counts as \"startup\"?\n\nConsider **powerlevel10k** for comparison. With this config `exit` takes longer than with\n**agnoster** but zsh starts faster: when you open a terminal, prompt appears virtually instantly and\nthe first command executes sooner.\n\nMany zsh plugin managers have been optimized for fast `exit` and are presenting it as a meaningful\nmeasure of performance. The widely held belief that **zinit** is the fastest plugin manager is based\non the timing of `exit`. Deferred initialization—pioneered by zinit turbo\nmode—[may not be very useful in practice](#deferred-initialization) but it's extremely effective on\nthis metric. Unsurprisingly, **zinit** has been [optimized for it](\n  https://web.archive.org/web/20201122095227/https://github.com/zdharma/pm-perf-test).\n\nThis doesn't mean developers have been engaging in conscious deception. It was easy to unknowingly\nfall into the trap. The timing of `exit` is very close to *first prompt lag* and\n*first command lag* in zsh configs from the older and simpler times. It *used to be* a proper\nmeasure of zsh startup performance. At some point these latencies have diverged, the benchmark lost\nits meaning, but the old habits remained.\n\n**zsh4humans** clocks at 6ms on `exit`. I'd be overjoyed if I could claim that **zsh4humans**\ninitializes that fast but there is no meaningful definition of initialization for which this claim\nis true.\n\nThe output of `time zsh -lic \"exit\"` tells you how long it takes to execute\n`zsh -lic \"exit\"` and nothing else. If you aren't in the habit of running `zsh -lic \"exit\"`, there\nis no reason for you to care one way or another about this number.\n\nSince the release of zsh-bench several projects have switched from targeting `exit` to meaningful\nperformance metrics. The ones I know of are **zcomet**, **zim** and **antidote**. There is hope that\nthis trend will continue.\n\n### Full benchmark data\n\n- Fast desktop machine (all benchmark results inlined in this document are from this run):\n  - Date: 2023-02-27.\n  - OS: Ubuntu 22.04.\n  - CPU: AMD Ryzen Threadripper 3970x.\n  - Storage: NVMe M.2.\n  - Results: [raw](https://github.com/romkatv/zsh-bench/blob/master/doc/linux-desktop.txt),\n    [normalized](https://github.com/romkatv/zsh-bench/blob/master/doc/linux-desktop.md).\n- MacBook Air (M1, 2020):\n  - Date: 2021-11-09.\n  - OS: macOS Monterey.\n  - Chip: Apple M1.\n  - Storage: SSD.\n  - Power: battery (not plugged into the power outlet).\n  - Results: [raw](https://github.com/romkatv/zsh-bench/blob/master/doc/macos-laptop.txt),\n    [normalized](https://github.com/romkatv/zsh-bench/blob/master/doc/macos-laptop.md).\n- Raspberry Pi 4 Model B:\n  - Date: 2021-11-09.\n  - OS: Raspbian Buster (32 bit).\n  - CPU: ARM Cortex-A72.\n  - Storage: microSD.\n  - Results: [raw](https://github.com/romkatv/zsh-bench/blob/master/doc/linux-raspberrypi.txt),\n    [normalized](https://github.com/romkatv/zsh-bench/blob/master/doc/linux-raspberrypi.md).\n\n### Conclusions\n\n- [powerlevel10k](#powerlevel10k) is an effective tool for reducing startup and per-command lag.\n- [diy++](https://github.com/romkatv/zsh-bench/blob/master/configs/diy%2B%2B/skel/.zshrc) is a\n  performant and relatively simple base for a self-bootstrapping zsh config if you want to build one\n  from scratch.\n- Plugin managers cannot beat\n  [diy++](https://github.com/romkatv/zsh-bench/blob/master/configs/diy%2B%2B/skel/.zshrc) on\n  performance unless they [cut corners](#cutting-corners). A fast plugin manager is one that doesn't\n  slow things down much. The value provided by a plugin manager is convenience, not speed.\n- All plugin managers and frameworks have good performance when configured properly. This includes\n  [ohmyzsh](https://github.com/romkatv/zsh-bench/blob/master/configs/ohmyzsh%2B/skel/.zshrc),\n  despite a commonly held opinion that it's slow.\n- From the \"pure\" plugin managers I've tested\n  [zplug](https://github.com/romkatv/zsh-bench/blob/master/configs/zplug/skel/.zshrc) has the best\n  API but it's also the slowest. The slowdown is small enough that it won't matter to most users.\n- Not all plugin managers can cleanly use [Instant Prompt](#instant-prompt) -- the closest thing to\n  a silver bullet in the battle for startup speed.\n- [zsh4humans](https://github.com/romkatv/zsh4humans) is faster than anything else with comparable\n  features. It beats even\n  [diy++](https://github.com/romkatv/zsh-bench/blob/master/configs/diy%2B%2B/skel/.zshrc) on\n  benchmarks thanks to tight integration of core features which cannot be replaced with third party\n  plugins.\n- Deferring zsh initialization with\n  [zinit turbo mode](https://github.com/zdharma-continuum/zinit#turbo-and-lucid) or\n  [zsh-defer](https://github.com/romkatv/zsh-defer) is not worth it.\n- The output of `time zsh -lic \"exit\"` does not tell you anything about the performance of\n  interactive zsh.\n\n### Responses\n\nThis section lists public responses from the developers of analyzed projects.\n\n#### zcomet\n\nFrom [README.md](\n  https://github.com/agkozak/zcomet/blob/52f2eaa7e7c09fb32e31e10ce1d45cb719065be4/README.md#news):\n\n\u003e - October 13, 2021\n\u003e     + I have adopted [@romkatv](https://github.com/romkatv)'s\n\u003e       [zsh-bench](https://github.com/romkatv/zsh-bench) benchmarks as a standard for measuring\n\u003e       performance.\n\u003e     + `zcomet` no longer `zcompiles` rc files, and the default behavior of `zcomet compinit` is\n\u003e       merely to run `compinit` while specifying a sensibly named cache file (again, props to\n\u003e       **@romkatv** for suggesting these changes).\n\nSimilarly on [reddit](\n  https://www.reddit.com/r/zsh/comments/q5e2du/ann_zshbench_benchmark_for_interactive_zsh/hgjnryv/):\n\n\u003e I've taken your advice: `zcomet` no longer compiles rc files, and `zcomet compinit` is no longer\n\u003e `compinit -C` by default.\n\u003e\n\u003e Next I'll look into the issue of compatibility with your *Instant Prompt*.\n\n#### zim\n\nThis [summary](https://github.com/romkatv/zsh-bench/issues/5#issuecomment-1029519185):\n\n\u003e Thanks to [@romkatv](https://github.com/romkatv)'s input and to his thoughts in\n\u003e [zsh-bench](https://github.com/romkatv/zsh-bench), this is what we've improved in\n\u003e [Zim](https://github.com/zimfw/zimfw):\n\u003e\n\u003e * We changed [our benchmarks](https://github.com/zimfw/zsh-framework-benchmark) to measure the\n\u003e   time to the first prompt appearance (instead of the time to run `exit`). We're using a different\n\u003e   tool ([expect](https://linux.die.net/man/1/expect)) than zsh-bench\n\u003e   ([script](https://linux.die.net/man/1/script)) to measure that time, and\n\u003e   [@romkatv](https://github.com/romkatv) helped us make use the times between our different\n\u003e   approaches match.\n\u003e * We don't compile Zsh startup scripts, and we don't compile any scripts in the background\n\u003e   anymore. The former is considered\n\u003e   [\"cutting corners\"](https://github.com/romkatv/zsh-bench/blob/3ed27aa21c13cba81b0c0065a54df5cfd50ef79c/README.md#cutting-corners)\n\u003e   in zsh-bench, and the latter\n\u003e   [unreliable](https://github.com/romkatv/zsh-bench/pull/11#issuecomment-994979683). In fact, now\n\u003e   Zim does not run anything in the background during your shell experience.\n\u003e * We're using the same improved completion dumpfile check that zsh4humans uses during startup in\n\u003e   our [completion](https://github.com/zimfw/completion) module, since using `compinit -C` alone is\n\u003e   also considered\n\u003e   [\"cutting corners\"](https://github.com/romkatv/zsh-bench/blob/3ed27aa21c13cba81b0c0065a54df5cfd50ef79c/README.md#cutting-corners)\n\u003e   in zsh-bench.\n\u003e * We've also updated our templates to set `ZSH_AUTOSUGGEST_MANUAL_REBIND=1` and source the\n\u003e   zsh-users/zsh-autosuggestions module last.\n\n### Debugging and validation\n\nSeveral tools are included in zsh-bench to aid in debugging and validation of benchmark results.\n\nPerform one iteration of login shell benchmark and retain temporary benchmark data:\n\n```zsh\n~/zsh-bench/zsh-bench --iters 1 --scratch-dir /tmp/zsh-bench\n```\n\nReplay the screen of the TTY that `zsh-bench` was acting on during benchmarking:\n\n```zsh\n~/zsh-bench/dbg/replay --scratch-dir /tmp/zsh-bench\n```\n\nReplay at 10% speed and at most 1s delay between TTY updates:\n\n```zsh\n~/zsh-bench/dbg/replay --scratch-dir /tmp/zsh-bench --delay-multiplier 10 --max-delay-ms 1000\n```\n\nDon't resize your terminal after running `zsh-bench` so that everything replays correctly.\n\nYou'll see that at first `zsh-bench` issues a short command that sources a script. This command gets\nsent to the TTY right away without waiting for first prompt. The sourced file prints, and the result\nof that printing signifies execution of the first command.\n\nThen `zsh-bench` starts measuring input latency. It prints a fairly long command composed of a bunch\nof escaped `abc` tokens, types one more character and waits for that character to be processed by\n[zle](https://zsh.sourceforge.io/Doc/Release/Zsh-Line-Editor.html). This is performed several times\nand then the command line is cleared without execution.\n\nThen `zsh-bench` measures command lag by spamming \u003ckbd\u003eEnter\u003c/kbd\u003e and seeing how many prompts it\nwould get.\n\nPrint a TAB-separated table of timestamped raw writes to the TTY:\n\n```zsh\n~/zsh-bench/dbg/timeline --scratch-dir /tmp/zsh-bench\n```\n\nSee what happened at the specific timestamp:\n\n```zsh\n~/zsh-bench/dbg/replay --scratch-dir /tmp/zsh-bench --pause-at-ms 10.149\n```\n\nThe last argument is a timestamp in milliseconds. The replay will pause right before and right after\nit. If you pass the value of `first_prompt_lag_ms` or `first_command_lag_ms` reported by `zsh-bench`\nas the timestamp, you'll see what `zsh-bench` considered *first prompt* and the output of\n*first command* respectively. If `zsh-bench` did its job correctly, the screen before\n`first_prompt_lag_ms` shouldn't have prompt but afterwards it should. Similarly, the screen before\n`first_command_lag_ms` shouldn't have `ZB*-msg` but afterwards it should (the first command prints\n`ZB*-msg` where `*` is a random number). Capability `has_git_prompt` should be set if and only if\n`ZB*-branch` appears before `ZB*-msg`. In other words, `has_git_prompt` signifies that the first\nprompt shows git branch.\n\n## License\n\n[MIT](https://github.com/romkatv/zsh-bench/blob/master/LICENSE).\n\n## FAQ\n\n### Why is it important to benchmark my shell? Is it for enthusiasts?\n\nIt's not important to benchmark your shell. It is important for *me* to benchmark zsh plugins and\nconfigs that I publish so that users of my code have fast shell. Shell users—or anyone for that\nmatter—prefer fast software over slow whether they are enthusiasts or not.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromkatv%2Fzsh-bench","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fromkatv%2Fzsh-bench","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromkatv%2Fzsh-bench/lists"}