perf(pwsh): lazy-load PSFzf off the shell-render path#77
Conversation
`Import-Module PSFzf` costs ~260ms (measured) — the single biggest slice of 10-tools.ps1 — but it's dead weight until Ctrl+T/Ctrl+R is pressed. Defer it: keep FZF_DEFAULT_OPTS/COMMAND eager (cheap, child-inherited), bind lightweight stub key handlers, and pay the import + install PSFzf's real handlers on the first press of either chord via Invoke-DotLoadPSFzf. Set-PsFzfOption overwrites the stubs, so later presses skip the loader. The stub scriptblock runs in the session runspace on keypress, so the deferred Import-Module lands where the interactive shell sees it (an OnIdle event action would import into the eventing runspace instead). Thread the deferral through the atuin Ctrl+R handoff: after atuin's init seizes Ctrl+R, re-assert the lazy stub (normal path), or hand Ctrl+R to PSFzf if already loaded, or fall back to ReverseSearchHistory when PSFzf isn't installed. Audited Get-InitCache while here: the starship/zoxide/mise/atuin caches resolve stale=False (mtime + generator-hash both pass), so the cache is healthy and the residual cold-start cost is genuine per-process init, not regeneration. No cache change needed. Full Pester suite: 556 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes PowerShell profile startup by deferring the expensive Import-Module PSFzf work (~260ms) out of the synchronous shell-render path, loading PSFzf only on first use of the relevant key chords.
Changes:
- Adds lazy-loading stubs for PSFzf keybindings (Ctrl+T / Ctrl+R) and a one-shot loader function that imports PSFzf on first use.
- Keeps
FZF_DEFAULT_OPTS/FZF_DEFAULT_COMMANDeager to preserve cheap, child-pane-inherited environment configuration. - Adjusts the atuin Ctrl+R handoff logic to re-assert the lazy stub (or fall back to
ReverseSearchHistory) after atuin initialization.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # swallowed. Handler names verified against PSFzf 2.7.10 (Invoke-FzfPsReadlineHandler*). | ||
| function global:Invoke-DotLoadPSFzf { | ||
| param([ValidateSet('Provider','History')][string]$Action) | ||
| Import-Module PSFzf | ||
| Set-PsFzfOption -PSReadlineChordProvider 'Ctrl+t' -PSReadlineChordReverseHistory 'Ctrl+r' | ||
| switch ($Action) { | ||
| 'Provider' { Invoke-FzfPsReadlineHandlerProvider } | ||
| 'History' { Invoke-FzfPsReadlineHandlerHistory } | ||
| } | ||
| } | ||
| # Cheap stubs bound now; the ~260ms import is deferred to first use, off the render path. | ||
| Set-PSReadLineKeyHandler -Chord 'Ctrl+t' -BriefDescription 'PSFzf file picker (lazy load)' -ScriptBlock { Invoke-DotLoadPSFzf -Action Provider } | ||
| Set-PSReadLineKeyHandler -Chord 'Ctrl+r' -BriefDescription 'PSFzf history (lazy load)' -ScriptBlock { Invoke-DotLoadPSFzf -Action History } |
There was a problem hiding this comment.
Thanks — good catch on the hard-coding risk. I verified it empirically: downloaded PSFzf 2.4.0 (the exact baseline pin in packages/modules.ps1:27) and both Invoke-FzfPsReadlineHandlerProvider and Invoke-FzfPsReadlineHandlerHistory are present there, so the names are stable 2.4.0 → 2.7.10 and the first Ctrl+T/Ctrl+R press is safe on a fresh box before the maintenance runner upgrades anything.
I did take the robustness half of your suggestion (788538f): the loader now wraps Import-Module PSFzf -ErrorAction Stop + Set-PsFzfOption in try/catch and warns via Write-DotWarn on failure (matching the starship/atuin/CompletionPredictor init guards), returning with the stub still bound so a later press retries. The handler invocation stays outside the try so an Esc-cancelled fzf isn't misreported as an init failure.
Address PR review: wrap the deferred Import-Module PSFzf + Set-PsFzfOption in try/catch so a broken/removed PSFzf surfaces as a friendly Write-DotWarn instead of a raw error at the prompt (mirrors the starship/atuin/ CompletionPredictor init guards). On failure the stub stays bound, so a later keypress retries. The real handler is invoked outside the try, so an Esc-cancelled fzf isn't misreported as an init failure. The reviewer also worried the Invoke-FzfPsReadlineHandler* names might be absent on the pinned baseline PSFzf 2.4.0 — verified by downloading 2.4.0: both Invoke-FzfPsReadlineHandlerProvider and ...History are present, so the first press is safe on a fresh box. Comment updated to cite the baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Defers the
Import-Module PSFzfcost (~260ms, measured — the single biggest slice ofcore/10-tools.ps1) out of the synchronous startup path. It only matters when you press Ctrl+T / Ctrl+R, so it's now paid once, on first use.How
FZF_DEFAULT_OPTS/FZF_DEFAULT_COMMAND) — cheap and inherited by child panes.Invoke-DotLoadPSFzfimports PSFzf, callsSet-PsFzfOption(which overwrites the stubs with PSFzf's real handlers), then fires the requested action so the first keystroke isn't swallowed. Later presses skip the loader entirely.OnIdle— anOnIdleevent action would import into the eventing runspace where the interactive shell can't see it.ReverseSearchHistorywhen PSFzf isn't installed.Cache audit (no change needed)
The original ask assumed the
Get-InitCachecache was invalidating every launch. Audited it: the starship/zoxide/mise/atuin caches all resolvestale=False(mtime + generator-hash both pass), so the cache is healthy and the residual cold-start cost is genuine per-process init, not regeneration. No cache change made.Verification
Fzf Provider Select/Fzf Reverse History Select).🤖 Generated with Claude Code