Skip to content

perf(renderer): cache icons and virtualize launcher surfaces#533

Open
JustYannicc wants to merge 11 commits into
SuperCmdLabs:mainfrom
JustYannicc:codex/consolidate-rendering-icons-emoji
Open

perf(renderer): cache icons and virtualize launcher surfaces#533
JustYannicc wants to merge 11 commits into
SuperCmdLabs:mainfrom
JustYannicc:codex/consolidate-rendering-icons-emoji

Conversation

@JustYannicc

@JustYannicc JustYannicc commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Consolidated renderer hot-path performance work for icon resolution, file icon IPC, local asset checks, emoji/list/grid virtualization, launcher command-list windowing, and stable list item registration.
  • Added bounded caches/coalescing for Phosphor icon resolution, Raycast file icon data URL requests, and local asset existence checks while preserving retry behavior for misses/failures.
  • Virtualized large emoji grids, Raycast grids, and launcher command-list surfaces so large result sets render only the visible window plus overscan/selection context.
  • Added focused regression and measurement scripts for icon, emoji, list, grid, launcher, and native emoji caret-session paths.

Why

These paths are all hit while rendering large launcher/Raycast result surfaces. Before this consolidation, repeated icon resolution could rescan Phosphor exports, concurrent identical file icons could fan out into duplicate IPC calls, large emoji/grid/list surfaces eagerly rendered thousands of cells/rows, and list registration could churn during selection-driven renders.

Compatibility impact

Rendering semantics are intended to stay unchanged. Icons still resolve through the same local/remote/Phosphor/file-icon paths, file icon failures remain retryable, and local asset misses are not permanently cached so late-created files can resolve. List/grid/emoji/launcher surfaces preserve keyboard selection, scrolling, context actions, section layout options, detail rendering, and existing fallback behavior while reducing rendered DOM work.

How tested

  • node --test scripts/test-icon-asset-cache.mjs scripts/test-icon-runtime-phosphor-cache.mjs scripts/test-icon-runtime-file-icon-cache.mjs scripts/test-emoji-grid-virtualization.mjs scripts/test-grid-virtualization.mjs scripts/test-list-registry-stable-order.mjs scripts/test-launcher-command-list-windowing.mjs scripts/test-emoji-caret-session.mjs -> 26 passing tests.
  • pnpm test -> 50 passing tests, 1 skipped live Electron test.
  • git diff --check -> pass.
  • Codex LSP diagnostics -> no errors for LauncherCommandList.tsx, list-runtime.tsx, grid-runtime.tsx, icon-runtime-phosphor.tsx, icon-runtime-render.tsx, and grid-runtime-virtualization.ts.
  • pnpm exec tsc -p tsconfig.renderer.json --noEmit --pretty false on this standalone origin/main branch is still blocked by existing renderer typing debt outside this consolidation, including App.tsx, CameraExtension.tsx, LiquidGlassSurface.tsx, launcher browser source typing, Raycast API shims, settings, snippets, and quicklink icon types.

Performance evidence

Focused before/after measurements on a temporary origin/main baseline versus this branch:

Probe Before After
Phosphor exact/repeated, 20,000 calls 26,845.470ms 3.586ms
Phosphor unknown-fuzzy/repeated, 1,500 calls 3,219.935ms 3.656ms
Launcher scenario sequence, 5k/3k rows 23,000 row renders / 390.968ms 122 row renders / 4.823ms
Launcher root initial, 5k rows 5,000 row renders / 81.757ms 18 row renders / 1.184ms

Focused tests also report emoji grid virtualization renders 112 visible emoji cells out of 4,096, grid virtualization renders 30 visible cells out of 5,000, local icon asset checks reduce 10,000 repeated positive lookups to one statSync, and 100 concurrent identical file icons coalesce to one IPC request.

Stack validation

Because standalone renderer typecheck is blocked by existing origin/main typing debt, I also validated on a temporary stack based on upstream PR #532 (fix(validation): restore project checks, JustYannicc:codex/consolidate-validation-checks at d4c6a9f) plus this consolidation series. Two stack-only conflicts were resolved by keeping the validation branch type cleanups together with the list/emoji virtualization changes.

Stack results:

  • pnpm exec tsc -p tsconfig.renderer.json --noEmit --pretty false -> pass.
  • pnpm test -> 51 passing tests, 1 skipped live Electron test.

Replaces

@shobhit99

Copy link
Copy Markdown
Contributor

Review: perf(renderer): cache icons and virtualize launcher surfaces

Solid, well-tested consolidation. I traced the virtualization windowing, the caches, and the launcher/grid/list selection paths; behavior looks preserved in the paths that matter, with a couple of minor things worth a look.

Strengths

  • Layout math is extracted into pure, unit-tested helpers (grid-runtime-virtualization.ts, list-runtime-hooks.ts additions). Nice separation.
  • All three virtualizers keep the selected entry mounted even when scrolled out of the window (getVirtualizedEntries unions a selected-range; grid uses getScrollTopForItemIndex; list maps item→row). So keyboard nav to off-screen items and its itemRefs registration still work — the main correctness risk in windowing is handled.
  • Icon-resolution caches are keyed purely on the input name; tint/color/theme are applied later in renderPhosphorIcon, not during resolution, so the cache keys are correct and theme/tint-safe. File-icon requests coalesce via an in-flight map, and negative results are not permanently cached (IPC errors + local-asset misses stay retryable) as claimed. Caches are bounded (clear-on-overflow / LRU).
  • Launcher virtualization is gated behind a 120-entry threshold, so the common small-list case still renders through the original path (now fragment-wrapped, but DOM-equivalent under space-y-0.5).
  • List registry order stabilized via a mount-time orderRef (matches the existing grid pattern) — removes per-render re-registration churn. Grid column clamp (1–8) and itemSize→columns mapping now match Raycast semantics; GridInset value change (smallsm…) is absorbed by normalizeGridInset, which accepts both forms.

Concerns / potential bugs

  • Redundant/competing scroll-into-view in the launcher (medium). App.tsx scrollToSelected (lines 1634–1652) still calls selectedElement.scrollIntoView({ behavior: "smooth" }) on every selectedIndex change, and the new LauncherCommandList selected-entry effect also calls element.scrollTo({ behavior: "smooth" }) on the same container whenever shouldVirtualize. Both fire per keypress and target the same element/position; they mostly agree, but two smooth scrolls per keystroke are redundant and can visibly fight/stutter. Not a correctness break (the selected node is always rendered), but consider having one owner — e.g. gate the in-component effect off, or skip App`s scroll when the list virtualizes.
  • Launcher virtual row heights are hardcoded estimates (low). COMMAND_ROW_HEIGHT=38 / SECTION_HEADER_HEIGHT=26 / CALCULATOR_CARD_HEIGHT=124 drive absolute positioning. Rows are px-3 py-2 around a 20px icon; if real height diverges (e.g. a wrapping subtitle) absolutely-positioned rows can overlap or leave gaps. Selection stays correct because Apps scroll uses real getBoundingClientRect, but visuals could drift. The list/emoji-grid paths pin exact heights inline so they are drift-free — only the launchers absolute layout relies on estimates. Worth confirming the constants against the real DOM.
  • Positive asset-existence cache never invalidates on deletion (low). positiveLocalPathExistsCache (icon-runtime-assets.tsx) caches exists=true until a size-based flush. Misses are correctly not cached (late-created files resolve), but a deleted extension asset keeps returning true → a broken sc-asset:// reference. Low real-world risk since extension asset paths are stable; noting for completeness.

Notes

  • itemSize="small" grids with no explicit columns now render 8 columns instead of the old fixed 5 (intentional, matches Raycast). Fine, just a visible change for those extensions.
  • No user-facing strings added/removed, so no i18n locale work needed. Imports/types for the touched files check out (ListItemAccessory imported, grid-runtime hooks imported, data-idx querySelector consumer replaced by index math).

🟡 Approve with minor comments — no behavior-breaking bugs found; the scroll-effect redundancy is the one thing I would resolve before merge.

🤖 Automated review by Claude Code (Opus 4.8).

@JustYannicc

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in 513f43c.

Fixes:

  • Removed the virtualized launcher list selected-entry scrollTo({ behavior: "smooth" }) effect, leaving selected-row smooth scrolling owned by App.tsx so there is no competing double scroll per keypress.
  • Pinned virtualized launcher row/header/calculator rendered heights to the same constants reserved by the absolute layout, and extended the launcher windowing measurement/test coverage to assert those fixed heights plus the no-child-smooth-scroll contract.
  • Changed the positive extension asset existence cache to refresh after a TTL, so deleted local assets stop resolving instead of staying cached until a size flush. Added a deletion/TTL regression test.

Verification run locally:

  • node --test scripts/test-launcher-command-list-windowing.mjs
  • node --test scripts/test-icon-asset-cache.mjs
  • node --test scripts/test-icon-runtime-phosphor-cache.mjs
  • node --test scripts/test-icon-runtime-file-icon-cache.mjs
  • node --test scripts/test-grid-virtualization.mjs
  • node --test scripts/test-emoji-grid-virtualization.mjs
  • node --test scripts/test-list-registry-stable-order.mjs
  • ./node_modules/.bin/vite build

Also confirmed via GraphQL that PR #533 has no inline review threads; the actionable feedback was the top-level review comment.

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.

2 participants