skip to content

In zsh, pressing Tab after a command that must query a slow source — a package index or a remote inventory — freezes the prompt for a second or two each time. How would you find out where the time goes, and what does zsh's completion system offer to fix it?

level: seniorimportance: should knowfreq 26%

answer

  1. separate startup cost from per-Tab cost
  2. time the underlying query by hand
  3. the system has an opt-in cache
  4. one style says where cached data lives
  5. another decides when it is stale

basics

~20 s

Time the completion by running the underlying query by hand, then check whether the completion function supports zsh's completion cache. Turning on the use-cache style, with cache-path pointing at a writable directory, makes such functions store and re-read candidate lists instead of re-querying on every Tab.

solid answer

~50 s

First separate the two possible costs: the shell's startup and registration work, and the per-Tab work of the completion function itself. If only that one command is slow, it is the function, and the usual culprit is a subprocess that enumerates candidates on every keystroke — run that query by hand and time it to confirm. compsys has a built-in answer for exactly this: functions written to use `_store_cache` and `_retrieve_cache` will persist their candidate lists when you enable them with `zstyle ':completion:*' use-cache on` and give them somewhere to live with `zstyle ':completion:*' cache-path`. Staleness is governed by the `cache-policy` style, which names a function returning true when the cache should be rebuilt, so you can decide it expires after a day rather than never. If the function does not support caching, your options are scoping the completer differently, replacing the function, or accepting the cost.

code

bash · 9 lines
bash
zstyle ':completion:*' use-cache on
zstyle ':completion:*' cache-path "${XDG_CACHE_HOME:-$HOME/.cache}/zsh/zcompcache"

_my_cache_policy() {
  local -a oldp
  oldp=( "$1"(Nm+1) )
  (( $#oldp ))
}
zstyle ':completion:*' cache-policy _my_cache_policy

go deeper

for a junior

Know that zsh can cache completion results, and that turning it on means setting the use-cache style and giving it a cache-path.

for a middle

Explain that caching is cooperative — the completion function has to use the cache helpers — and that the cache-policy style decides when stored candidates are considered stale.

for a senior

Diagnose before configuring: establish whether the delay follows the command or the shell, time the candidate-producing query directly, and know the fallbacks when the function has no cache support, including a timeout so the prompt cannot hang.

for a principal

Weigh interactive latency against staleness for the whole team: how fresh candidate data really needs to be, whether a scheduled refresh into a local file beats live queries, and what a slow prompt costs across everyone's day.

## Split the problem before you fix it "zsh is slow" collapses two very different costs, and the fix for one does nothing for the other. - **Per-shell cost** is paid once at startup, when `compinit` scans `fpath` and rebuilds its dump file. It shows up as a slow *new terminal*, not a slow Tab. - **Per-completion cost** is paid every time you press Tab against a particular command. It shows up as a frozen prompt on that command and nowhere else. The question above is squarely the second. The diagnostic is simple: does the delay follow the *command* or the *shell*? If Tab is instant for `cd` and slow only for the one tool, the completion function for that tool is doing work. ## Confirming where the time goes Completion functions generate candidates by calling something — usually a subprocess. Run that thing by hand and time it: ```bash time mytool list-remote-things >/dev/null ``` If that alone takes a second, the completion cannot be faster than a second, and no amount of zstyle tuning will change it — the fix has to be caching or a cheaper source. You can also see which function is responsible: `echo $_comps[mytool]` names it, and reading its body shows what it shells out to. For per-call timing inside the shell, running the candidate-producing command under `time` is more honest than guessing from feel. ## The completion cache compsys ships a cache for precisely this situation. It is opt-in on both sides: - The **completion function** must be written to use it, calling `_cache_invalid` to ask whether the stored data is stale, `_retrieve_cache` to load it and `_store_cache` to save it. Many shipped completions for package managers and similar slow sources already do. - The **user** must enable it: ```bash zstyle ':completion:*' use-cache on zstyle ':completion:*' cache-path "${XDG_CACHE_HOME:-$HOME/.cache}/zsh/zcompcache" ``` `use-cache` switches the mechanism on; `cache-path` says where the files go, and setting it explicitly keeps a pile of cache files out of your home directory. Once enabled, the first Tab pays the full query cost and writes the result; subsequent completions read the file. ## Controlling staleness A cache that never expires eventually offers candidates that no longer exist, which is worse than being slow. The `cache-policy` style names a function that zsh calls to decide whether to rebuild: ```bash _my_cache_policy() { local -a oldp oldp=( "$1"(Nm+1) ) (( $#oldp )) } zstyle ':completion:*' cache-policy _my_cache_policy ``` The policy function receives the cache file's path as `$1` and returns success when the cache should be regenerated. The glob qualifier `(Nm+1)` matches the file only if it was modified more than one day ago — `N` makes a non-match expand to nothing rather than erroring — so the array is non-empty exactly when the cache is a day old. That is the idiomatic zsh way to write "expire after a day", and the policy is the piece you almost always want to set yourself, because the sensible expiry is a property of your data, not of the completion function. ## When caching is not available If the function does not implement the cache protocol, you have three honest options. Scope the expensive completer away from where you do not need it, using a narrower zstyle context so it never runs for arguments where the candidates do not matter. Replace the function with your own that queries a cheap local source — a file your tooling refreshes on a schedule — instead of the live one. Or accept the cost, and make sure the query itself has a short timeout so a network hiccup does not hang the prompt indefinitely, which is the failure mode people actually complain about. ## The related startup cost While you are there, it is worth knowing the other side: a very large `fpath` makes `compinit` slower on every new shell, because it stats every candidate file. `compinit -C` skips the check for new or changed completion functions and reuses the dump as-is, trading discovery of newly installed completions for startup speed. That is a real trade — after installing anything that ships completions you must remove the dump and re-run `compinit` yourself — but on a machine where new tools appear rarely and terminals are opened constantly, it is the right one.

  • Why does enabling `use-cache` sometimes have no effect at all?
    Because caching is cooperative. The style only permits caching; the completion function must actually call `_retrieve_cache` and `_store_cache` around its expensive query. A function that shells out unconditionally ignores the style completely, so you have to read the function or replace it rather than assume the style did something.
  • How would you make a completion cache expire after a day instead of persisting forever?
    Set the `cache-policy` style to a function of your own. zsh passes it the cache file's path and rebuilds when it returns success, so a one-line body using a glob qualifier that matches only files modified more than a day ago gives you daily expiry. The sensible interval depends on how fast the underlying data changes.
  • A colleague reports that new terminals are slow, but Tab is instant. Is the completion cache relevant?
    No — that is the startup cost, not the per-completion cost. It comes from compinit scanning a large fpath and rebuilding its dump file. The levers there are trimming fpath, or `compinit -C` to reuse the dump without checking for new completion functions, at the price of having to clear the dump by hand after installing tools.

saying these in an interview costs you the question

  • Blames shell startup for a slow single command
  • Enables use-cache and assumes every function honours it
  • Sets a cache with no expiry policy
  • Tunes zstyle without ever timing the underlying query
  • Thinks compinit -C speeds up individual completions

context