Opening a new terminal tab takes roughly two seconds before the zsh prompt appears. How would you find out what part of the startup is slow, and what typically turns out to be responsible?
answer
- measure before you guess
- compare against a zsh -f baseline
- a module that times functions
- zmodload zsh/zprof
- version managers and eager plugins
basics
~20 sMeasure first: time zsh -i -c exit against a zsh -f baseline to confirm the cost is your own configuration, then load the zsh/zprof module at the top of ~/.zshrc and call zprof at the bottom to see which functions consume the time.
solid answer
~50 sStart by measuring rather than guessing. `time zsh -i -c exit`, repeated a few times, gives the real number; `time zsh -f -i -c exit` skips the startup files and gives the floor, and the gap between them is what you own. Then profile: `zmodload zsh/zprof` as the first line of `~/.zshrc` and a bare `zprof` as the last line makes the next interactive shell print a table of shell functions sorted by time, with self time separated from time spent in callees. Its blind spot is straight-line code that is not inside a function, so pair it with bisection — comment out half the file, re-measure, repeat. The usual culprits are the same everywhere: language version managers whose init lines are `eval`-ed subshells, plugin managers that source every plugin eagerly, and any `$(...)` in the file, each of which forks a process on every shell you open.
code
bash · 7 lines# first line of ~/.zshrc
zmodload zsh/zprof
# ... the rest of your configuration ...
# last line of ~/.zshrc
zprofgo deeper
Know that shell startup slowness usually comes from your own ~/.zshrc, and that starting a shell with zsh -f skips it so you can compare.
Explain how to load the zsh/zprof module and read its per-function timings, and name the usual suspects such as version-manager init lines and eagerly sourced plugins.
Work from measurement to fix: establish a baseline, bisect the file, separate one-off startup cost from per-prompt cost, and prefer deferring or deleting over shuffling lines about.
Treat startup latency as a budget the team owns, and decide whether the answer is discipline in a shared configuration or a different loading strategy altogether.
## Measure before you touch anything Shell startup is the one latency everybody feels and almost nobody measures. Get a number first. ```zsh for i in 1 2 3; do time zsh -i -c exit; done ``` Run it several times — the first run pays cold-cache costs that are not representative. Then establish the floor: ```zsh time zsh -f -i -c exit ``` `-f` skips the startup files, so this is zsh itself with nothing of yours. If the two numbers are close, the slowness is not your configuration at all and you should be looking elsewhere — a slow terminal emulator, a network home directory, a machine-wide file under `/etc`. If the gap is 1.9 of your 2 seconds, keep going. ## zprof zsh ships a profiler as a loadable module. Wire it around the file you suspect: ```zsh # first line of ~/.zshrc zmodload zsh/zprof # ...everything else... # last line of ~/.zshrc zprof ``` Open a new interactive shell (`zsh -i -c exit` will do) and you get a table: number of calls, total time, average, self time, and the function name, sorted with the expensive things at the top. Two columns matter. **Total** time includes everything a function called; **self** time is what the function burned itself. A wrapper with large total and tiny self time is innocent — follow the callee. The important limitation: `zprof` profiles **shell functions**. Straight-line code at the top level of your rc file, and much of what a plain `source` of someone else's script does, is not attributed to any function and can hide from the report. If `zprof` shows a total far below your measured wall time, the missing time is exactly that kind of code, and bisection is how you find it. ## Bisection, the technique that always works Comment out the bottom half of `~/.zshrc`, re-measure, and keep halving. It feels crude next to a profiler and it is strictly more reliable, because it measures the thing you actually care about — wall-clock time to prompt — rather than a model of it. Ten minutes of halving localises almost any startup problem to a single line. ## What it usually turns out to be Across real configurations the same offenders recur: - **Version-manager initialisation.** Lines of the form `eval "$(sometool init -)"` fork a process, wait for it to print shell code, and then evaluate that code — on every shell you open. Some managers additionally source a large shell library. This is very often the single biggest item in the report. - **Eagerly loaded plugins.** A plugin manager that sources twenty repositories at startup pays for twenty repositories at startup, whether or not you use them today. - **Any `$(...)` in the file.** Each one is a fork. Querying a tool for a path you could have written literally is a per-shell tax for a value that changes twice a year. - **The completion system's initialisation**, when it is made to rebuild rather than use its cache. - **Anything that touches the network.** A version check, a remote lookup, a status query. On a good day it is 200 ms; on a bad day your terminal hangs. ## Fixing, in order of preference 1. **Delete it.** A surprising share of a slow rc file is configuration for tools the person no longer uses. 2. **Make it literal.** Replace a `$(...)` that computes a stable path with the path. 3. **Defer it.** Load the expensive thing the first time it is actually needed rather than at startup. Plugin managers offer this directly: zinit's `wait` ice (its turbo mode) schedules a plugin to load shortly after the prompt appears, and antidote takes a different route by generating one static file from your plugin list which the rc file simply sources. 4. **Move it.** Anything only login shells need belongs in `~/.zprofile`, not in the file every tab reads. ## Hiding it is not fixing it Some prompt frameworks offer an "instant prompt" that draws a usable prompt immediately and finishes initialising behind it. That genuinely improves the experience, and it is worth having — but the work still happens, it still delays the first command in a scripted context, and it can mask a configuration that has quietly grown to three seconds. Measure with the feature disabled at least once so you know the real number you are hiding. ## Set a budget The practical bar is that a new tab should feel instant — under about 100 ms of your own configuration. Treat that as a budget and re-measure after adding anything to the file. Startup time is like a spending habit: it never gets slow in one commit, it gets slow in forty.
- zprof reports far less total time than your wall-clock measurement. What does that tell you?That the missing time is not inside shell functions. `zprof` attributes time to functions, so straight-line code at the top level of the rc file — a plain `source` of a large script, an `eval` of a subshell's output — can be invisible to it. Fall back to bisecting the file, which measures the thing you actually care about.
- How do you decide between deleting, deferring and moving a slow line?Delete if the tool is not used any more — most slow rc files carry dead configuration. Move it to `~/.zprofile` if only login shells need it. Defer it only when it is genuinely needed interactively and genuinely expensive, since deferral adds a mechanism someone has to understand later.
- Does an instant-prompt feature solve a slow startup?No, it relocates the pain. The prompt appears immediately while initialisation continues behind it, which is a real usability win, but the work still happens and the first command can still wait on it. Measure at least once with it off so you know the number you are hiding.
- What is a reasonable startup budget to hold yourself to?Roughly 100 ms of your own configuration for an interactive shell — the gap between `zsh -i -c exit` and `zsh -f -i -c exit`. Past that a new tab stops feeling instant. Re-measure whenever you add to the file; startup never gets slow in one commit.
saying these in an interview costs you the question
- Guesses at the culprit without measuring anything
- Blames the terminal emulator before checking zsh -f
- Thinks zprof reports every line of the rc file
- Deletes plugins at random until it feels faster
- Treats an instant prompt as having fixed the latency