Skip to main content

Benchmarking Shell Startup Latency

Context

I spend a lot of time in the terminal. It's a scratchpad for me. At the time of writing, I have 57 tmux panes spread across 14 sessions. I start and stop A LOT of shells.

After installing the kiro-cli, my shell felt rather sluggish: new tabs were not immediately ready to use, and the delay showed up in both normal terminal windows and tmux.

What I measured

Before any changes, zsh -i -c exit was roughly 0.29s warm and 0.43s cold. zsh -l -i -c exit sat around 0.31s.

What happened?

Kiro injected shell hooks into both .zshrc and .zprofile.

After gating those hooks, login-shell startup dropped to about 0.04-0.07s from 0.29-0.43s.

What I changed

  • Removed Kiro hooks from shell startup.
  • Moved Kiro integration behind an on-demand kiro-cli() shell wrapper.
  • Swapped the prompt to starship. Unnecessary in hindsight, but whatever. I've been wanting to try something other than omz.

After

Current warm timings are roughly 0.04s-0.05s for both interactive and login shells, with the first shell in a batch sometimes landing around 0.09s.

Benchmark timeline

Step Timing
Baseline ~0.36s
Remove OMZ 0.21s faster for normal, non-login shells
Move Kiro hooks out of startup faster by 0.02s-0.04s for normal shells, 0.24s on login shells

compinit was first optimized by using compinit -C with ~/.zcompdump and compiling that cache with zcompile, but I removed that change later because it was not worth the extra maintenance.

I also tried lazy-loading Bun and terraform completion, but I removed that too for the same reason.

Takeaway

Interesting reminder about .zprofile and zsh hooks.


Relevant diffs

typeset -g _dotfiles_kiro_integration_loaded=0

_dotfiles_load_kiro_integration() {
  (( _dotfiles_kiro_integration_loaded )) && return 0
  _dotfiles_kiro_integration_loaded=1

  [[ -f "${HOME}/Library/Application Support/kiro-cli/shell/zprofile.pre.zsh" ]] && builtin source "${HOME}/Library/Application Support/kiro-cli/shell/zprofile.pre.zsh"
  [[ -f "${HOME}/Library/Application Support/kiro-cli/shell/zshrc.pre.zsh" ]] && builtin source "${HOME}/Library/Application Support/kiro-cli/shell/zshrc.pre.zsh"
  [[ -f "${HOME}/Library/Application Support/kiro-cli/shell/zprofile.post.zsh" ]] && builtin source "${HOME}/Library/Application Support/kiro-cli/shell/zprofile.post.zsh"
  [[ -f "${HOME}/Library/Application Support/kiro-cli/shell/zshrc.post.zsh" ]] && builtin source "${HOME}/Library/Application Support/kiro-cli/shell/zshrc.post.zsh"
}

kiro-cli() {
  _dotfiles_load_kiro_integration
  command kiro-cli "$@"
}

autoload -Uz compinit
if [[ -f "$HOME/.zcompdump" ]]; then
  compinit -C
else
  compinit
fi

if [[ -f "$HOME/.zcompdump" && ( ! -f "$HOME/.zcompdump.zwc" || "$HOME/.zcompdump" -nt "$HOME/.zcompdump.zwc" ) ]]; then
  zcompile "$HOME/.zcompdump"
fi

typeset -g creates a global flag in zsh, and the (( ... )) && return 0 check makes the loader a one-time guard. That means the Kiro integration is sourced only the first time I run kiro-cli in a shell, not on every prompt.

The builtin source calls pull in Kiro’s generated shell hooks without going through my wrapper function again.

Sidenote: compinit cache

compinit reads a generated dump file, ~/.zcompdump, so zsh does not have to rediscover every completion definition on every shell launch.

I also compile that dump to ~/.zcompdump.zwc, which just makes loading the generated cache cheaper.

If the cache ever gets out of date, it is safe to delete ~/.zcompdump*. Zsh will rebuild it the next time compinit runs.

Quick find

Search the garden