Skip to content

perf(ui): cache search keyboard navigation targets#2492

Open
trivikr wants to merge 5 commits intonpmx-dev:mainfrom
trivikr:get-focusable-elements
Open

perf(ui): cache search keyboard navigation targets#2492
trivikr wants to merge 5 commits intonpmx-dev:mainfrom
trivikr:get-focusable-elements

Conversation

@trivikr
Copy link
Copy Markdown
Contributor

@trivikr trivikr commented Apr 12, 2026

🔗 Linked issue

N/A

🧭 Context

The search page supports keyboard navigation across suggestions and package results with ArrowUp, ArrowDown, and Enter.

That navigation path was doing repeated DOM queries and per-key sorting to rebuild the list of selectable elements on every key press. On larger result sets, that adds unnecessary work to a hot interaction path and can make keyboard movement through results feel sluggish.

While optimizing that path, a follow-up issue showed up: the cached focus targets were being populated on the next animation frame, which left a brief window where results were visible but keyboard navigation had no cached targets yet. In practice, if a user pressed an arrow key immediately after results rendered, nothing could happen. There was also a teardown gap where a pending animation frame could refill the cache after observation had stopped.

📚 Description

This change updates search keyboard navigation to cache focusable result elements instead of querying and sorting the DOM on every key press.

It introduces a scoped results container ref, maintains the cached focus target list with a MutationObserver, and refreshes that cache when relevant search UI state changes. This keeps navigation work lightweight while preserving the existing DOM-order behavior of suggestions first, then package results.

trivikr and others added 2 commits April 12, 2026 11:14
…sly on start (#7)

Co-authored-by: trivikr <16024985+trivikr@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 12, 2026 7:43pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 12, 2026 7:43pm
npmx-lunaria Ignored Ignored Apr 12, 2026 7:43pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1ccf647b-75da-4860-b9db-066722806388

📥 Commits

Reviewing files that changed from the base of the PR and between c21e533 and edf1725.

📒 Files selected for processing (1)
  • app/pages/search.vue

📝 Walkthrough

Summary by CodeRabbit

  • Chores
    • Improved keyboard navigation in search results for snappier response and smoother focus movement.
    • Results focus list is now maintained efficiently and updated automatically as results change, preventing missed or stale focus targets.
    • Lifecycle and observation handling tightened so navigation and focus remain reliable when the results view appears, updates or is removed.

Walkthrough

Replaced per-keystroke global DOM queries with a component-scoped cached list of focusable result elements backed by a MutationObserver (RAF-batched refresh). Keydown navigation now uses the cached list; observer lifecycle is started when the results container ref is available and cleaned up on unmount.

Changes

Cohort / File(s) Summary
Search page — keyboard/focus handling
app/pages/search.vue
Replaced dynamic per-keypress getFocusableElements() (global queries + filtering) with a resultsContainerRef-scoped cached focusableElements. Added a MutationObserver to watch subtree/attribute/visibility changes and refresh the cache via requestAnimationFrame (with initial synchronous population). Wired observer start/stop on ref availability, mount/unmount and reactive changes; updated handleResultsKeydown to use the cached list and added ref="resultsContainerRef" to the results <section>.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (keyboard)
    participant Comp as SearchComponent
    participant Results as ResultsContainer\r`<section ref="resultsContainerRef">`
    participant Observer as MutationObserver
    participant DOM as DOM (focusable elements)

    Note over Comp,Results: Mount / init
    Comp->>Results: querySelectorAll([data-result-index],[data-suggestion-index])
    Comp->>Comp: populate cached focusableElements
    Comp->>Observer: observe Results (subtree, attributes, childList)
    Note over User,Comp: Keyboard navigation
    User->>Comp: ArrowUp/ArrowDown keydown
    Comp->>Comp: use cached focusableElements to move focus
    Note over Results,Observer: Results update / visibility change
    Results->>Observer: mutation events
    Observer->>Comp: schedule RAF to refresh cache
    Comp->>Results: re-query focusable elements -> update cached focusableElements
Loading

Possibly related PRs

Suggested reviewers

  • danielroe
  • whitep4nth3r
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf(ui): cache search keyboard navigation targets' directly summarises the main change: caching keyboard navigation targets for performance.
Description check ✅ Passed The description comprehensively explains the optimisation, including context about the previous repeated DOM queries, the caching solution, and the timing/teardown fixes implemented.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 12, 2026

Codecov Report

❌ Patch coverage is 0% with 39 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/pages/search.vue 0.00% 32 Missing and 7 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/search.vue`:
- Around line 519-536: The deferred refresh can run after the results section
has been torn down; inside the watcher callback that currently calls nextTick(()
=> scheduleFocusableElementsRefresh()), guard before scheduling by checking the
relevant DOM/component ref (e.g., the results container ref or root ref used by
scheduleFocusableElementsRefresh) is still non-null/not unmounted; if the ref is
null, bail and do not call scheduleFocusableElementsRefresh. Update the watcher
closure (the block referencing nextTick and scheduleFocusableElementsRefresh) to
perform this null-check so the rAF is never queued for a removed element.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fc324e9e-b4ce-4cfd-b1e0-a11af619e087

📥 Commits

Reviewing files that changed from the base of the PR and between f70a11b and 3dafbd7.

📒 Files selected for processing (1)
  • app/pages/search.vue

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/pages/search.vue (2)

741-746: Redundant RAF cancellation after stopObservingFocusableElements().

Lines 743-746 cancel refreshFocusableElementsFrame and null it, but stopObservingFocusableElements() on line 742 already does this (see lines 446-449). This duplication can be removed.

Suggested cleanup
 onBeforeUnmount(() => {
   stopObservingFocusableElements()
-  if (refreshFocusableElementsFrame != null) {
-    window.cancelAnimationFrame(refreshFocusableElementsFrame)
-    refreshFocusableElementsFrame = null
-  }
   updateLiveRegionMobile.cancel()
   updateLiveRegionDesktop.cancel()
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/search.vue` around lines 741 - 746, The onBeforeUnmount hook
contains redundant cancellation of refreshFocusableElementsFrame after calling
stopObservingFocusableElements(); remove the explicit
window.cancelAnimationFrame(refreshFocusableElementsFrame) and the
refreshFocusableElementsFrame = null lines in the onBeforeUnmount block since
stopObservingFocusableElements() already handles cancelling and clearing
refreshFocusableElementsFrame (refer to stopObservingFocusableElements and
refreshFocusableElementsFrame in the same file and the onBeforeUnmount hook).

470-473: Consider removing the redundant scheduled refresh.

After the synchronous refreshFocusableElements() call on line 472, the immediate scheduleFocusableElementsRefresh() on line 473 appears unnecessary. The cache is already populated, and the MutationObserver will handle any subsequent DOM changes. The extra RAF will effectively no-op, but removing it would simplify the logic.

Suggested simplification
   // Perform an initial synchronous refresh so focusableElements is populated
   // before any immediate key handling (ArrowUp/ArrowDown) occurs.
   refreshFocusableElements()
-  scheduleFocusableElementsRefresh()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/search.vue` around lines 470 - 473, The initial synchronous call to
refreshFocusableElements() already populates the focusableElements cache, so
remove the redundant scheduled refresh by deleting the call to
scheduleFocusableElementsRefresh() immediately after refreshFocusableElements();
rely on the existing MutationObserver and scheduleFocusableElementsRefresh()
when DOM changes occur rather than invoking it synchronously here (adjust any
comments accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/pages/search.vue`:
- Around line 741-746: The onBeforeUnmount hook contains redundant cancellation
of refreshFocusableElementsFrame after calling stopObservingFocusableElements();
remove the explicit window.cancelAnimationFrame(refreshFocusableElementsFrame)
and the refreshFocusableElementsFrame = null lines in the onBeforeUnmount block
since stopObservingFocusableElements() already handles cancelling and clearing
refreshFocusableElementsFrame (refer to stopObservingFocusableElements and
refreshFocusableElementsFrame in the same file and the onBeforeUnmount hook).
- Around line 470-473: The initial synchronous call to
refreshFocusableElements() already populates the focusableElements cache, so
remove the redundant scheduled refresh by deleting the call to
scheduleFocusableElementsRefresh() immediately after refreshFocusableElements();
rely on the existing MutationObserver and scheduleFocusableElementsRefresh()
when DOM changes occur rather than invoking it synchronously here (adjust any
comments accordingly).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ace559c-15a4-4342-9712-943bb4675beb

📥 Commits

Reviewing files that changed from the base of the PR and between 3dafbd7 and c21e533.

📒 Files selected for processing (1)
  • app/pages/search.vue

@serhalp serhalp added needs review This PR is waiting for a review from a maintainer perf npmx.dev app performance labels Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer perf npmx.dev app performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants