Skip to content

perf(renderer): consolidate Raycast UI hot paths - #621

Open
JustYannicc wants to merge 11 commits into
SuperCmdLabs:mainfrom
JustYannicc:codex/consolidate-wave5-renderer-raycast
Open

perf(renderer): consolidate Raycast UI hot paths#621
JustYannicc wants to merge 11 commits into
SuperCmdLabs:mainfrom
JustYannicc:codex/consolidate-wave5-renderer-raycast

Conversation

@JustYannicc

@JustYannicc JustYannicc commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Consolidated renderer/Raycast/menubar wave5 performance work into one branch from current upstream origin/main.
  • Reduced selected-scroll churn for Raycast Grid and launcher list selected-row correction by using instant scroll behavior.
  • Reduced Raycast action churn with visible-field action signatures and stable global keydown listeners; kept List.Dropdown flattening cached for stable children without relying on it as a broad rerender win.
  • Stabilized Raycast hook argument signatures for usePromise, useCachedPromise, and useExec.
  • Reduced List and emoji-grid render churn with memoized renderers and virtual-row helpers.
  • Reduced MenuBarExtra title tick churn by reusing serialized renderer menu items and skipping unchanged native tray image updates for stable file-backed icons.

Why

The replaced micro PRs target the same renderer/Raycast UI hot paths and were hard to review independently because several changes overlap in list rendering, action registry updates, hook signatures, and menubar title tick behavior. This PR keeps those changes together while avoiding unrelated runtime, command, search, and validation stack changes.

Compatibility impact

Raycast API shapes and exports are preserved. Selection semantics, action execution, dropdown initial onChange, shortcut handling, menu item actions, tray title fallback behavior, file-backed icon refresh on mtime/size changes, and native menu serialization remain compatible.

How tested

  • PASS: dependency hydration with real Homebrew npm from committed package-lock.json: /opt/homebrew/Cellar/node@22/22.22.3/bin/node /opt/homebrew/lib/node_modules/npm/bin/npm-cli.js ci --ignore-scripts --force; no lockfile churn committed.
  • PASS: standard npm test path with real npm: /opt/homebrew/Cellar/node@22/22.22.3/bin/node /opt/homebrew/lib/node_modules/npm/bin/npm-cli.js test (node --test 'scripts/test-*.mjs'), 38 tests total, 37 passed, 1 live Electron test skipped.
  • Covered by standard npm test path (node --test 'scripts/test-*.mjs'): scripts/test-virtual-selection-scroll-behavior.mjs, scripts/test-action-registry-listener-churn.mjs, scripts/test-list-dropdown-memoization.mjs, scripts/test-raycast-hook-signatures.mjs, scripts/test-list-renderer-memoization.mjs, scripts/test-list-emoji-render-churn.mjs, scripts/test-menubar-native-update-cache.mjs, and scripts/test-menubar-renderer-serialization-cache.mjs.
  • Manual performance evidence harnesses, intentionally outside the test-*.mjs glob: scripts/measure-raycast-hook-signatures.mjs and scripts/measure-raycast-list-render-churn.mjs.
  • PASS: /opt/homebrew/Cellar/node@22/22.22.3/bin/node /opt/homebrew/lib/node_modules/npm/bin/npm-cli.js run build:main.
  • PASS: /opt/homebrew/Cellar/node@22/22.22.3/bin/node /opt/homebrew/lib/node_modules/npm/bin/npm-cli.js run build:renderer.
  • PASS: git diff --check.
  • PASS: node --check over all added/touched .mjs scripts.
  • PASS: Codex LSP diagnostics on every touched TS/TSX file found no errors; remaining diagnostics were hints only.

Performance evidence

  • Manual list structural measurement: node scripts/measure-raycast-list-render-churn.mjs --iterations=2 --warmups=1 --selection-steps=200 --json produced selectionSteps=200, listItemRenderCount=400, emojiGridItemRenderCount=400, median about 0.0014ms, p95 about 0.0014ms.
  • Manual hook signature measurement: node scripts/measure-raycast-hook-signatures.mjs --iterations=2 --warmups=1 --args=500 --json produced cached stable-args speedup about 14020x versus repeated direct JSON.stringify, and handled cyclic args.
  • Source micro PR evidence retained: perf(raycast): reduce action and dropdown churn #611 reported action registry execute-only publishes 12 -> 0 and global keydown listener adds 12 -> 1; its dropdown harness still covers stable-children flattening 40 -> 5, but that case is intentionally scoped.
  • Source micro PR evidence retained: perf(hooks): stabilize Raycast hook signatures #613 reported cached stable hook args avoiding repeated large JSON.stringify work and handling cyclic args without throwing.
  • Source micro PR evidence retained: perf(menubar): skip unchanged file icon image updates #610 reported stable file-backed title ticks reducing native tray icon resolves 60 -> 1 and setImage calls 59 -> 0.

Stack validation

Replaces

@shobhit99

Copy link
Copy Markdown
Contributor

Reviewed the full diff (main.ts tray caching, App.tsx, action registry, detail/form/list/grid runtimes, the hook stable-args extraction, the list virtual-row helpers + emoji-grid virtualization, and the menubar serialization cache). Focused on behavior/API preservation for the Raycast UI runtime. Net: solid, mostly a correctness-neutral-or-better perf pass; one low-severity behavioral nuance in emoji-grid scrolling.

Strengths

  • Memoized row renderers are safe. ListItemRenderer/ListEmojiGridItemRenderer are wrapped in React.memo with explicit comparators that track visible props (title/subtitle/icon/accessories/isSelected/dataIdx/assetsPath) and deliberately ignore handler identity. The reason this doesn't break click/activate is that onActivate now reads selectedIdxRef.current/primaryActionRef.current (both refreshed every render), so a memoized-stale closure still executes against the current selection/action. key={item.id} means the comparator only ever runs for the same item, so id doesn't need comparing. Correct.
  • Action registry publish gate is a superset of the old one. buildActionVisibleSignature includes icon/shortcut/style/order in addition to title/sectionTitle, so it publishes in strictly more cases than the previous id:title:sectionTitle snapshot (the old code actually missed icon/shortcut/style changes). execute is always mutated in place, so the newest callback fires even when no re-publish happens. unregister always publishes. Verified against ActionRegistration in action-runtime-types.ts — all Pick'd fields exist.
  • Install-once global keydown effects (list/form/detail) with [] deps reading from a per-render-updated ref eliminate add/removeEventListener churn with no stale closures. The list ref omits primaryAction correctly (its global handler only does metaK + shortcut iteration; Enter is handled in the React onKeyDown).
  • useExec fix: passing real execArgs instead of [] fixes a latent staleness bug where useExec never re-ran on command/args/options change. Shared useStableArgs/getStableArgsKey is circular-ref-safe and behavior-equivalent to the removed private copy (confirmed no other importer of the old one).
  • main.ts tray caching is sound. Refresh is gated on a composite icon key + a statSync mtime/size file-identity key, which correctly covers the "icon file became available with the same path" case (|missing|mtimeMs=…|bytes=… flips the key). lastResolvedTrayIconOk is restored/persisted across skipped refreshes so the title-fallback state is preserved; state is cleaned up on menubar-remove.
  • Menubar serialization cache is consistent. Keyed on (registryVersion, assetsPath); since every register/unregister bumps registryVersion, the cache can never diverge from registryRef, and action closures read runtimeCtxRef.current at call time, so no stale-callback risk. Good test coverage added throughout.

Concerns / potential bugs

  • (Low) Emoji-grid scroll-into-view can silently no-op on large selection jumpslist-runtime.tsx, the emoji branch of the selection scroll effect (if (shouldUseEmojiGridValue) { el.querySelector('[data-idx="…"]')?.scrollIntoView(...) }). Now that the emoji grid is virtualized, only rows within the overscan window are in the DOM. Incremental arrow nav is fine (target stays within overscan). But a jump beyond the window — e.g. selectedIdx clamped to filteredItems.length - 1 when a filter shrinks the list, or a controlled selection far off-screen — finds no element and doesn't scroll, leaving the selection off-viewport until manual scroll. Not strictly a regression (base applied 36px linear-row math to the grid layout, which was also wrong), but the linear-list path already computes the offset from rowMetrics/itemIdxToRowIdx and calls el.scrollTo regardless of render window; the emoji path could use the same math for consistency instead of querySelector.

Suggestions

🟡 Approve with minor comments

🤖 Automated review by Claude Code (Opus 4.8).

@JustYannicc

JustYannicc commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in commit 8ec499c.

What changed:

  • Fixed the virtualized emoji-grid selection scroll path in list-runtime.tsx: selection scroll now uses the existing virtual row map/row offsets and scrollTo(...), so large jumps no longer depend on the selected emoji cell already being mounted in the DOM.
  • Extended test-virtual-selection-scroll-behavior.mjs to guard that the Raycast list selection path uses offset-based instant scroll and does not fall back to scrollIntoView/mounted-cell lookup.
  • Dropped the incidental App.tsx [PIN-TOGGLE] debug logs and removed the off-theme setter casts; fixed useAiChat's return type so the real state setter type flows without casts.
  • Softened the PR body's List.Dropdown memoization wording to scope the evidence to stable children instead of presenting it as a broad rerender win.

Verification:

  • node --test scripts/test-action-registry-listener-churn.mjs scripts/test-list-dropdown-memoization.mjs scripts/test-list-renderer-memoization.mjs scripts/test-raycast-hook-signatures.mjs scripts/test-virtual-selection-scroll-behavior.mjs ✅ 8 passed
  • git diff --check

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