Skip to content

perf(router-core): guard default search parser against throws - #7665

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/default-search-parse-guard
Closed

perf(router-core): guard default search parser against throws#7665
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/default-search-parse-guard

Conversation

@anonrig

@anonrig anonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Stop the default search parser from throwing a SyntaxError for every plain-string param. In packages/router-core/src/searchParams.ts, defaultParseSearch ran JSON.parse on every leftover string value:

-export const defaultParseSearch = parseSearchWith(JSON.parse)
+export const defaultParseSearch = parseSearchWith(parseMaybeJson)

parseMaybeJson first checks, from the first non-whitespace character, whether the value could be JSON and only then calls JSON.parse. The check is a superset of JSON's value-start grammar ({ [ " - 0-9 t f n, skipping leading whitespace), so any string JSON.parse would accept still gets parsed — we only skip the doomed parse of plain strings.

Why

qss.decode already coerces numbers/booleans, so JSON.parse only ever runs on the remaining string values — which on real apps are mostly plain text (?q=hello&f=live&src=typed_query). Each one throws a SyntaxError that parseSearchWith catches to keep the raw string, and constructing + throwing that error (with stack capture) is the dominant cost.

Search params are parsed on every SSR request and every client navigation. In-situ before/after on the real defaultParseSearch (Vitest bench):

Query shape old (JSON.parse) new (guarded) Speedup
mixed X-like (?q=…&f=live&src=…&pf=1) 317K ops/s 996K ops/s 3.1×
all plain strings (?q=from:elon since:2024&f=live&…) 107K ops/s 176K ops/s 1.7×

Correctness

Behavior is identical. The guard never skips a value JSON.parse would accept, and for skipped values it returns the raw string — exactly what parseSearchWith does today when JSON.parse throws.

Verified by:

  • The full existing searchParams suite (isomorphism round-trips + "alien deserialization" of human-typed params).
  • A 5000+ case fuzz confirming couldBeJson is a correct superset of JSON.parse's accept set.
  • Four added tests: plain-string params stay strings, leading-whitespace JSON values still parse (guard skips whitespace), JSON-looking-but-invalid values fall back to the raw string, and t/f/n-prefixed non-literals (tweet, false_alarm, null_island) stay strings.
nx run @tanstack/router-core:test:unit   -> 1182 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types  -> no errors

Notes

  • Only the default parse path changes. defaultStringifySearch uses the same JSON.parse-as-throw-probe pattern when building URLs (also hot via <Link>), but its logic relies on throw-vs-no-throw as the "is this JSON?" signal, so making it allocation-free cleanly is a slightly larger change — happy to do it as a follow-up.
  • Includes a changeset (@tanstack/router-core patch).
  • Pre-existing unused-imports/no-unused-vars warnings on the existing catch (_err) blocks are untouched by this change.

Summary by CodeRabbit

  • Performance
    • Optimized query parameter parsing to reduce processing overhead, improving performance for server-side rendering and client-side navigation.

Loading
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