Skip to content

Startup Performance

A well-structured zsh config should start in under 100ms on modern hardware. The primary sources of latency are: (1) compinit cache rebuilds, (2) eval subshells for tool initialization, and (3) unconditional source calls to large files. The architecture and shell integration strategy pages address these directly. This page covers profiling and the remaining optimization surface.

mise activation cost vs. proto

mise activate is comparable to proto activate in cost — both are single eval calls that register a precmd hook. What differs is downstream: mise's hook also evaluates project env vars and task definitions on cd, where proto required a separate direnv hook for the same job. Net effect on a typical project cd: about the same. Net effect on cold startup: slightly faster, because mise replaces both proto and direnv in one eval rather than two.

Profiling with zprof

zsh ships a built-in profiler (zprof). Enable it at the top of .zshrc, run a shell, then read the output. The profiler measures wall time per function call including time spent in child calls.

# Temporarily add to the TOP of $ZDOTDIR/.zshrc (remove after profiling):
zmodload zsh/zprof

# ... rest of .zshrc (conf.d sourcing) ...

# Temporarily add to the BOTTOM of $ZDOTDIR/.zshrc:
zprof

For external timing:

# Run 10 shells and average:
for i in $(seq 10); do /usr/bin/time zsh -lic exit; done 2>&1

# Or use hyperfine if available:
hyperfine --warmup 3 "zsh -lic exit"

Lazy eval: deferred tool initialization

Some tools' init scripts are slow because they do work beyond registering shell hooks — they may spawn subshells, read config files, or call external binaries. For tools used infrequently, initialization can be deferred to first use with a stub function pattern:

# conf.d/70-tools.zsh addition: lazy zoxide initialization
# Instead of: eval "$(zoxide init zsh)"
# Use a stub that initializes on first call:
_zoxide_lazy_init() {
  unfunction _zoxide_lazy_init cd z    # Remove stubs
  eval "$(zoxide init zsh --cmd cd)"    # Real init
  cd "$@"                                # Execute the original call
}

# Only use lazy init if zoxide adds measurable latency (>20ms).
# For most systems, direct eval is fast enough — measure first.
if command -v zoxide &>/dev/null; then
  functions[cd]=_zoxide_lazy_init
  functions[z]=_zoxide_lazy_init
fi

The version-hashed cache in shell integration strategy is the framework's preferred approach for most tools. Lazy init is the fallback for tools that genuinely need runtime state and can't be cached.

Performance benchmarks by component

Target timings for each configuration section on a 2023+ machine:

Component Target Red Flag Primary Optimization
compinit (cache hit) < 5ms > 30ms 24h cache + -C flag
compinit (cache miss) < 150ms > 300ms Delete stale .zcompdump
mise activate eval < 20ms > 60ms Guard with command -v
rv shell eval < 10ms > 40ms Guard with command -v
direnv hook eval < 5ms > 20ms Guard with command -v
starship init eval < 15ms > 50ms Version-hashed cache
fzf --zsh eval < 5ms > 20ms Version-hashed cache
conf.d sourcing (all) < 20ms > 80ms Remove unused fragments
Total interactive startup < 80ms > 200ms Profile with zprof

When a component lands in the "red flag" column, the diagnostic path is: zprof to confirm which function is slow, then check whether the tool can be moved from Tier 1 (live eval) to Tier 2 (version-hashed cache) in the shell integration strategy.