dev-tools

My Terminal Takes 6 Seconds to Open. Here's What I Did About It.

A 5-minute profiling session revealed 5 seconds of wasted shell startup time. Three targeted fixes - caching, lazy-loading, and deferred init - brought it down to ~1 second.

AuthorAlex Raihelgaus
Date
My Terminal Takes 6 Seconds to Open. Here's What I Did About It.

The Problem

Opening a new terminal took ~6 seconds before the prompt appeared. Not catastrophic, but annoying enough to investigate.

Profiling

Zsh has a built-in $SECONDS variable with fractional precision. Drop this at the top of your .zshrc:

typeset -F SECONDS=0
_zsh_timer_last=$SECONDS
_zt() {
  local now=$SECONDS
  printf '⏱ %-40s %6.0fms\n' "$1" "$(( (now - _zsh_timer_last) * 1000 ))"
  _zsh_timer_last=$now
}

Then after each heavy operation (every eval, source, tool init), add _zt "label". At the bottom of .zshrc:

printf '⏱ %-40s %6.0fms\n' "TOTAL" "$(( SECONDS * 1000 ))"
unfunction _zt

Open a new terminal. You'll see exactly where your time goes.

My Results

⏱ oh-my-posh                                  511ms
⏱ oh-my-zsh                                   235ms
⏱ gcloud                                        2ms
⏱ opam                                          3ms
⏱ thefuck                                     673ms
⏱ sdkman                                       93ms
⏱ atuin                                        18ms
⏱ nvm                                         523ms
⏱ asdf                                          1ms
⏱ 1password                                   186ms
⏱ openclaw                                   3744ms
⏱ TOTAL                                      5989ms

Three things jumped out immediately:

Offender Time What it does on every shell open
openclaw 3744ms Boots Node.js to generate static shell completions
thefuck 673ms Boots Python to generate a shell alias
nvm 523ms Loads the entire nvm environment

That's 4940ms spent on things I don't need at shell startup. Almost 5 seconds of pure waste.

The Fixes

1. Cache openclaw completions with auto-invalidation (3744ms saved)

The openclaw CLI generates shell completions dynamically, but the output is static - it only changes when the CLI itself is updated. Cache it, and use zsh's -nt (newer-than) test to auto-regenerate when the binary changes:

# Before (3744ms every terminal open)
source <(openclaw completion --shell zsh)

# After (~1ms - reads a file, auto-regenerates on update)
_openclaw_cache=~/.openclaw-completion.zsh
if [[ ! -f "$_openclaw_cache" ]] || [[ "$(which openclaw)" -nt "$_openclaw_cache" ]]; then
  openclaw completion --shell zsh > "$_openclaw_cache"
fi
source "$_openclaw_cache"
unset _openclaw_cache

The -nt check compares file modification times. Any reinstall or upgrade touches the binary, triggering a one-time regeneration. Zero maintenance. I wrote a deeper dive on the openclaw issue specifically.

2. Lazy-load thefuck (673ms saved)

thefuck boots a full Python process on every shell init just to register an alias. You only need it when you actually type fuck:

# Before: runs Python on every terminal open
eval $(thefuck --alias)

# After: defers Python until first use
fuck() {
  unfunction fuck
  eval $(thefuck --alias)
  fuck "$@"
}

The first time you type fuck, it initializes (one-time 673ms delay). Every subsequent call is instant. Every terminal that never uses it pays nothing.

3. Lazy-load nvm (523ms saved)

NVM's init script is notoriously slow. Defer it until you actually call nvm, node, npm, or npx:

# Before: loads nvm on every terminal open
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# After: defers until first use
export NVM_DIR="$HOME/.nvm"
_nvm_lazy_load() {
  unfunction nvm node npm npx
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
}
nvm() { _nvm_lazy_load; nvm "$@"; }
node() { _nvm_lazy_load; node "$@"; }
npm() { _nvm_lazy_load; npm "$@"; }
npx() { _nvm_lazy_load; npx "$@"; }

Same pattern as thefuck - placeholder functions that replace themselves on first call.

Results

Fix Time saved
openclaw cache + auto-invalidation 3744ms
thefuck lazy-load 673ms
nvm lazy-load 523ms
Total saved ~4940ms

Shell startup went from ~6 seconds to ~1 second.

The General Pattern

Most shell startup bloat comes from two things:

  1. Dynamic generation of static output - CLIs that run source <(some-tool completion --shell zsh). The output doesn't change between runs, but you pay the full runtime boot cost every time. Cache it.

  2. Eager initialization of lazy-use tools - Tools you use occasionally (thefuck, nvm) that run heavyweight init on every shell open. Wrap them in placeholder functions that self-replace on first call.

If your terminal feels sluggish, profile it. The investigation takes 5 minutes. The fixes take another 5. The payoff compounds on every terminal you open for the rest of time.

Tags

#shell#zsh#performance#profiling#developer-tools#terminal#optimization