Skip to content

perf: search improvements#1431

Merged
ghostdevv merged 4 commits intonpmx-dev:mainfrom
alexdln:perf/search-improvements
Feb 12, 2026
Merged

perf: search improvements#1431
ghostdevv merged 4 commits intonpmx-dev:mainfrom
alexdln:perf/search-improvements

Conversation

@alexdln
Copy link
Member

@alexdln alexdln commented Feb 12, 2026

Completely separated the search process from routing and created a single manager for working with global search, which is responsible for the query and updates the URL every 250ms.

Since there is no longer a binding from query to input, I made a leading debounce, which will allow the user to immediately direct to the search, rather than waiting 80-250ms

Also fixed a few navigation issues - keywords in the package weren't working and the global search field was being updated unnecessarily on the compare page.

@vercel
Copy link

vercel bot commented Feb 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 Feb 12, 2026 4:32pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 12, 2026 4:32pm
npmx-lunaria Ignored Ignored Feb 12, 2026 4:32pm

Request Review

@codecov
Copy link

codecov bot commented Feb 12, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/composables/useGlobalSearch.ts 37.50% 19 Missing and 6 partials ⚠️
app/pages/index.vue 0.00% 2 Missing ⚠️
app/components/Header/SearchBox.vue 50.00% 1 Missing ⚠️
app/components/Package/Keywords.vue 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@alexdln alexdln marked this pull request as draft February 12, 2026 14:01
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces a centralized useGlobalSearch composable that consolidates global search state, provider resolution, and URL synchronization (with debounced update and a flush helper). It removes the previous useGlobalSearchQuery composable and replaces per-component URL-sync logic across SearchBox.vue, Keywords.vue, index.vue and search.vue to consume the new composable (model and provider). SearchBox.vue now exposes a focus() method and wires an input ref; Keywords.vue sets the global model when a keyword is clicked. useStructuredFilters gains an optional searchQueryModel Ref to keep external query state in sync.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly relates to the changeset, detailing search refactoring, debounce improvements, and keyword navigation fixes that align with all file modifications.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link
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: 2

Comment on lines +31 to +35
const updateUrlQueryImpl = (value: string, provider: 'npm' | 'algolia') => {
const isSameQuery = route.query.q === value && route.query.p === provider
// Don't navigate away from pages that use ?q for local filtering
if (pagesWithLocalFilter.has(route.name as string) || isSameQuery) {
return
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve route-level provider overrides when updating the URL.

Right now the setter passes searchProvider.value (settings) into URL updates. If a user arrives with p=npm while their settings default to Algolia, the first keystroke drops p and flips providers. Use the effective provider (route + settings) and normalise current route values for isSameQuery.

💡 Suggested fix
-  const isSameQuery = route.query.q === value && route.query.p === provider
+  const currentQuery = normalizeSearchParam(route.query.q)
+  const currentProvider = normalizeSearchParam(route.query.p) === 'npm' ? 'npm' : 'algolia'
+  const isSameQuery = currentQuery === value && currentProvider === provider
@@
-      if (!updateUrlQuery.isPending()) {
-        updateUrlQueryImpl(value, searchProvider.value)
-      }
-      updateUrlQuery(value, searchProvider.value)
+      const effectiveProvider = searchProviderValue.value
+      if (!updateUrlQuery.isPending()) {
+        updateUrlQueryImpl(value, effectiveProvider)
+      }
+      updateUrlQuery(value, effectiveProvider)

Also applies to: 64-71

Comment on lines +58 to +60
function flushUpdateUrlQuery() {
updateUrlQuery.flush()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Submitting a stable query can no‑op.

flushUpdateUrlQuery() only flushes pending debounced updates. If the user submits an already‑settled query (for example, the prefilled header search), nothing navigates. Consider falling back to an immediate update when no debounce is pending.

💡 Suggested fix
 function flushUpdateUrlQuery() {
-  updateUrlQuery.flush()
+  if (updateUrlQuery.isPending()) {
+    updateUrlQuery.flush()
+    return
+  }
+  updateUrlQueryImpl(searchQuery.value, searchProviderValue.value)
 }

Copy link
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: 2

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

580-586: Remove inline focus-visible utility on button element.

Per project guidelines, focus-visible styling for buttons is applied globally via main.css. The inline focus-visible:outline-accent/70 class here should be removed to maintain consistency.

♻️ Proposed fix
             <button
               type="button"
-              class="shrink-0 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md motion-safe:transition-colors motion-safe:duration-200 hover:bg-fg/90 focus-visible:outline-accent/70"
+              class="shrink-0 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md motion-safe:transition-colors motion-safe:duration-200 hover:bg-fg/90"
               `@click`="claimPackageModalRef?.open()"
             >

Based on learnings: "In the npmx.dev project, ensure that focus-visible styling for button and select elements is implemented globally in app/assets/main.css… Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements in Vue templates."


693-699: Remove inline focus-visible utility on button element.

Same as above—rely on the global button:focus-visible rule rather than the inline utility class.

♻️ Proposed fix
                 <button
                   type="button"
-                  class="px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-accent/70"
+                  class="px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90"
                   `@click`="claimPackageModalRef?.open()"
                 >

Based on learnings: focus-visible styling for buttons should use the global CSS rule, not inline utility classes.

Copy link
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/composables/useStructuredFilters.ts (1)

401-417: ⚠️ Potential issue | 🟠 Major

Avoid dropping in-flight query text when adding/removing keywords.
With debounced URL updates, searchQuery.value can lag behind the active input. Building newQ from it can overwrite recent typing. Prefer the external model when provided and keep the local ref in sync.

Proposed fix
   function addKeyword(keyword: string) {
     if (!filters.value.keywords.includes(keyword)) {
       filters.value.keywords = [...filters.value.keywords, keyword]
-      const newQ = searchQuery.value
-        ? `${searchQuery.value.trim()} keyword:${keyword}`
-        : `keyword:${keyword}`
+      const baseQuery = (searchQueryModel?.value ?? searchQuery.value).trim()
+      const newQ = baseQuery ? `${baseQuery} keyword:${keyword}` : `keyword:${keyword}`
       router.replace({ query: { ...route.query, q: newQ } })
 
       if (searchQueryModel) searchQueryModel.value = newQ
+      searchQuery.value = newQ
     }
   }
 
   function removeKeyword(keyword: string) {
     filters.value.keywords = filters.value.keywords.filter(k => k !== keyword)
-    const newQ = searchQuery.value.replace(new RegExp(`keyword:${keyword}($| )`, 'g'), '').trim()
+    const baseQuery = searchQueryModel?.value ?? searchQuery.value
+    const newQ = baseQuery.replace(new RegExp(`keyword:${keyword}($| )`, 'g'), '').trim()
     router.replace({ query: { ...route.query, q: newQ || undefined } })
     if (searchQueryModel) searchQueryModel.value = newQ
+    searchQuery.value = newQ
   }

@ghostdevv ghostdevv added this pull request to the merge queue Feb 12, 2026
Merged via the queue into npmx-dev:main with commit fd3a597 Feb 12, 2026
17 checks passed
@alexdln alexdln deleted the perf/search-improvements branch February 12, 2026 18:47
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