Skip to content

perf: cut fish and zsh shell startup time - #133

Open
chianyungcode wants to merge 3 commits into
mainfrom
perf/shell-startup
Open

chianyungcode wants to merge 3 commits into
mainfrom
perf/shell-startup

Conversation

@chianyungcode

Copy link
Copy Markdown
Owner

Closes CHE-21

Why

Every new shell ran atuin, starship, direnv, jw and zoxide just to print their setup scripts. zsh also set up completions twice (once in nix-darwin's /etc/zshrc, once in .zshrc), and it loaded oh-my-zsh plugins that start extra processes before the first prompt.

Result (hyperfine, <shell> -i -c exit):

Shell Before After
fish 79 ms 29 ms
zsh 98 ms 34.5 ms

Summary

fish (89f7c41)

  • Add __cached_source, which saves each tool's setup script under ~/.cache/fish/init/ and only re-runs the tool when its binary changes.
  • Load the direnv hook from the cache, via a conf.d/direnv.fish that replaces Homebrew's copy.
  • Make the atuin session ID in fish instead of running atuin uuid.
  • Stop loading jw completions at startup; completions/jw.fish already covers them.
  • Only load the herdr hook inside a herdr pane.

zsh (6b25d5a)

  • The same caching (000-cached-init.zsh) for starship, jw, atuin and zoxide. Starship's PROMPT2 is cached too, and cleared when starship.toml changes.
  • Skip nix-darwin's /etc/zshrc (NOSYSZSHRC=1).
  • Run compinit after conf.d, holding earlier compdef calls and replaying them. Fix the daily full check, and compile the dump in the background.
  • Load the oh-my-zsh brew, git and npm plugins just after the first prompt.
  • The same jw and herdr changes as fish.

zsh bun aliases (97fb088)

  • The deferred brew plugin now loads after bun.sh, so its ba, bi and bu replaced the bun aliases (ba ran brew autoremove). Rename the bun ones to bna, bni and bnu. Fish keeps ba, bi and bu.

Tradeoffs

  • The startup numbers leave out work that now runs just after the prompt. zsh -i -c shells never get the git, brew or npm plugin aliases.
  • New completions can take up to a day to appear (rm ~/.cache/zsh/zcompdump-* to refresh).
  • The cache only notices binary upgrades. After changing a tool's arguments, a wrapper function or zoxide's _ZO_* variables, run rm -rf ~/.cache/{zsh,fish}/init.
  • If many shells start at once right after an upgrade, one may load a half-written script for that launch. A failed starship generation in zsh can stay cached until starship's next upgrade.
  • Skipping /etc/zshrc drops HIST_FCNTL_LOCK and any zsh settings added in nix-darwin later.
  • The atuin session ID is fully random, where atuin's own ID starts with a timestamp.

Test plan

  • Startup times above measured with hyperfine
  • Live zsh after the deferred plugins load: bna/bni/bnu run bun, ba/bi/bu run brew
  • CI passes
  • Open new fish and zsh shells: prompt, atuin (Up/Ctrl-R), zoxide, direnv and completions work

🤖 Generated with Claude Code

chianyungcode and others added 3 commits November 4, 2026 11:38
Startup spawned atuin, starship, direnv, jw and zoxide on every launch
to generate their init scripts. Cache the output under
$XDG_CACHE_HOME/fish/init, keyed on each binary's resolved path and
mtime so it rebuilds after upgrades.

- add __cached_source helper (01_cached-init.fish)
- shadow the vendor direnv.fish hook with a cached one
- generate ATUIN_SESSION in fish instead of spawning `atuin uuid`
- drop jw's eager completion sourcing; completions/jw.fish covers it
- only source the herdr hook inside a herdr pane

fish -i -c exit: 79 ms -> 29 ms (hyperfine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Startup ran a second, uncached compinit from nix-darwin's /etc/zshrc,
spawned starship and jw to generate their init scripts, and eagerly
loaded oh-my-zsh plugins that fork on load.

- skip nix-darwin's /etc/zshrc via NOSYSZSHRC (duplicate compinit with
  compaudit, promptinit, bashcompinit)
- add __cached_source helper (000-cached-init.zsh), keyed on the
  binary's resolved path and mtime, for starship, jw, atuin and zoxide
- bake starship's PROMPT2 into the cache; drop it when starship.toml
  changes
- generate ATUIN_SESSION in zsh instead of spawning `atuin uuid`
- drop jw's eager completion eval; completions/_jw covers it
- defer the oh-my-zsh brew/git/npm plugins (`git version`, `rm` forks)
- run compinit after conf.d so plugin fpath entries land in the dump,
  queueing earlier compdef calls; make the daily full check actually
  work and zcompile the dump in the background
- only source the herdr hook inside a herdr pane

zsh -i -c exit: 98 ms -> 34.5 ms (hyperfine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The oh-my-zsh brew plugin is now deferred, so it loads after bun.sh and
its ba/bi/bu aliases (brew autoremove/install/update) replaced the bun
ones. Rename the bun aliases to bna/bni/bnu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Nov 4, 2026

Copy link
Copy Markdown

CHE-21

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant