Skip to content

perf(router-core): skip JSON parsing for plain search strings#7022

Open
Sheraff wants to merge 2 commits intomainfrom
flo/search-params-json-parse-guard
Open

perf(router-core): skip JSON parsing for plain search strings#7022
Sheraff wants to merge 2 commits intomainfrom
flo/search-params-json-parse-guard

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Mar 23, 2026

Summary

  • add a cheap lexical guard before calling the JSON-like search parser so plain strings avoid exception-driven parse work
  • apply the same fast path in both parseSearchWith and stringifySearchWith while preserving existing behavior for JSON-like values
  • cover the new guard with tests that assert plain strings skip parser calls and JSON-like strings still parse normally

Benchmark Notes

  • Local @benchmarks/client-nav: React 29.6119ms -> 28.6312ms (~3.3% faster)
  • Local @benchmarks/client-nav: Solid 24.7548ms -> 24.2348ms (~2.1% faster)
  • Local @benchmarks/client-nav: Vue 22.5473ms -> 21.7420ms (~3.6% faster)
  • Local @benchmarks/bundle-size: router scenario gzip deltas ranged from +71B to +82B

Testing

  • CI=1 NX_DAEMON=false pnpm nx run @tanstack/router-core:test:unit --outputStyle=stream --skipRemoteCache
  • CI=1 NX_DAEMON=false pnpm nx run @tanstack/router-core:test:types --outputStyle=stream --skipRemoteCache
  • CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf --outputStyle=stream --skipRemoteCache
  • CI=1 NX_DAEMON=false pnpm nx run @benchmarks/bundle-size:build --outputStyle=stream --skipRemoteCache

Summary by CodeRabbit

  • Refactor

    • Improved search-parameter handling to avoid unnecessary JSON parsing for clearly non-JSON values, reducing overhead while preserving existing behavior for other parsers.
  • Tests

    • Added tests ensuring correct behavior and round-trip encoding for JSON-like and non-JSON query values, and verifying custom parsers are still applied when provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Added a pre-parse validator and updated parse/stringify flows so JSON.parse is only attempted for strings that look like JSON; custom parsers are still applied unconditionally. Tests updated to cover both JSON-like and non-JSON-like query values and round-trip behavior.

Changes

Cohort / File(s) Summary
Router core — search param parsing
packages/router-core/src/searchParams.ts
Added canStringBeJsonParsed() and modified parseSearchWith() / stringifySearchWith() to avoid calling JSON.parse on obviously non-JSON strings while preserving behavior for non-JSON parsers.
Tests — search param behavior
packages/router-core/tests/searchParams.test.ts
Updated imports and added tests verifying: custom parsers run for non-JSON-looking strings; JSON.parse is skipped for plain strings; JSON-like values are parsed/stringified as expected and round-trip correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniff the string, and if it's plain,
I skip the parse and save some strain.
If braces, quotes, or numbers gleam,
I call the parser on the dream.
Hop, encode, decode — a tidy scheme!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main performance optimization: skipping JSON parsing for plain search strings using a lexical guard.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch flo/search-params-json-parse-guard

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

@nx-cloud
Copy link

nx-cloud bot commented Mar 23, 2026

View your CI Pipeline Execution ↗ for commit 7a5b555

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 1m 7s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-23 18:18:12 UTC

@github-actions
Copy link
Contributor

github-actions bot commented Mar 23, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 23, 2026

Bundle Size Benchmarks

  • Commit: bb3b8e4b069a
  • Measured at: 2026-03-23T17:50:24.102Z
  • Baseline source: history:6077120efa59
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 88.24 KiB +85 B (+0.09%) 278.58 KiB 76.55 KiB ▁██████▅▅▅▅▆
react-router.full 91.47 KiB +94 B (+0.10%) 289.57 KiB 79.35 KiB ▁██████▅▅▆▆▆
solid-router.minimal 35.89 KiB +92 B (+0.25%) 108.48 KiB 32.20 KiB █▃▃▃▃▃▃▁▁▁▁▁
solid-router.full 40.31 KiB +99 B (+0.24%) 121.88 KiB 36.10 KiB █▃▃▃▃▃▃▁▁▁▁▂
vue-router.minimal 53.86 KiB +83 B (+0.15%) 154.71 KiB 48.34 KiB ▁██████▆▆▆▆▆
vue-router.full 58.74 KiB +98 B (+0.16%) 170.18 KiB 52.56 KiB ▁██████▆▆▇▇▇
react-start.minimal 102.67 KiB +115 B (+0.11%) 326.60 KiB 88.69 KiB ▁██████▄▄▅▅▆
react-start.full 106.02 KiB +75 B (+0.07%) 336.91 KiB 91.56 KiB ▁██████▅▅▅▅▆
solid-start.minimal 49.95 KiB +84 B (+0.16%) 154.66 KiB 44.04 KiB █▃▃▃▃▃▃▁▁▁▁▁
solid-start.full 55.45 KiB +96 B (+0.17%) 170.76 KiB 48.71 KiB █▄▄▄▄▄▄▁▁▂▂▂

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 23, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7022

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7022

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7022

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7022

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7022

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7022

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7022

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7022

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7022

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7022

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7022

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7022

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7022

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7022

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7022

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7022

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7022

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7022

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7022

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7022

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7022

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7022

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7022

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7022

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7022

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7022

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7022

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7022

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7022

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7022

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7022

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7022

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7022

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7022

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7022

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7022

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7022

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7022

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7022

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7022

commit: 7a5b555

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ad29de715

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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: 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 `@packages/router-core/src/searchParams.ts`:
- Around line 23-31: The helper canStringBeJsonParsed rejects JSON literals if
they have surrounding whitespace; update canStringBeJsonParsed to trim the input
before checking literal values so inputs like " true " or "null\t" are
recognized the same as "true"/"false"/"null" (e.g. use a trimmedValue =
value.trim() and test trimmedValue === 'true' || trimmedValue === 'false' ||
trimmedValue === 'null'); ensure the existing firstCharCode checks still operate
on the original string start for objects/arrays/numbers or, if you prefer, use
trimmedValue consistently for both literal checks and the numeric firstCharCode
branch to preserve JSON.parse behavior with leading whitespace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b379670e-aa36-4d1f-ae03-3a9a221cf7bc

📥 Commits

Reviewing files that changed from the base of the PR and between d0516af and 1ad29de.

📒 Files selected for processing (2)
  • packages/router-core/src/searchParams.ts
  • packages/router-core/tests/searchParams.test.ts

Comment on lines +23 to +31
return (
firstCharCode === 34 ||
firstCharCode === 45 ||
firstCharCode === 91 ||
firstCharCode === 123 ||
(firstCharCode >= 48 && firstCharCode <= 57) ||
value === 'true' ||
value === 'false' ||
value === 'null'
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 | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does JavaScript JSON.parse accept trailing whitespace for literals like "true ", "false ", and "null "?

💡 Result:

Yes. JSON.parse accepts trailing (and leading) JSON whitespace around any valid JSON value, including the literals true, false, and null—so "true ", "false\t\n", and " null\r" all parse successfully.

That’s because JSON defines a complete JSON text as ws value ws (so whitespace is allowed after the value) [2], and ECMAScript’s JSON.parse requires the input to be a valid JSON text per ECMA-404 [3]. ECMA-404 also states that insignificant whitespace is allowed before or after any token [1].

Only JSON whitespace is allowed (space, tab, LF, CR). Any other trailing non-whitespace character (e.g. "true x") must throw a SyntaxError.


Fix behavioral regression for JSON literals with surrounding whitespace.

JSON.parse accepts valid JSON text with leading/trailing whitespace (e.g., JSON.parse("true ") succeeds), but the canStringBeJsonParsed helper only accepts exact 'true' | 'false' | 'null' without whitespace. This causes inputs like true (from URL parameters with + encoding) to skip parsing and remain strings, regressing the original behavior.

Suggested fix
 function canStringBeJsonParsed(value: string) {
   if (!value) {
     return false
   }

-  const firstCharCode = value.charCodeAt(0)
+  const normalized = value.trim()
+  if (!normalized) {
+    return false
+  }
+
+  const firstCharCode = normalized.charCodeAt(0)

   if (firstCharCode <= 32) {
     return true
   }

   return (
     firstCharCode === 34 ||
     firstCharCode === 45 ||
     firstCharCode === 91 ||
     firstCharCode === 123 ||
     (firstCharCode >= 48 && firstCharCode <= 57) ||
-    value === 'true' ||
-    value === 'false' ||
-    value === 'null'
+    normalized === 'true' ||
+    normalized === 'false' ||
+    normalized === 'null'
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/searchParams.ts` around lines 23 - 31, The helper
canStringBeJsonParsed rejects JSON literals if they have surrounding whitespace;
update canStringBeJsonParsed to trim the input before checking literal values so
inputs like " true " or "null\t" are recognized the same as
"true"/"false"/"null" (e.g. use a trimmedValue = value.trim() and test
trimmedValue === 'true' || trimmedValue === 'false' || trimmedValue === 'null');
ensure the existing firstCharCode checks still operate on the original string
start for objects/arrays/numbers or, if you prefer, use trimmedValue
consistently for both literal checks and the numeric firstCharCode branch to
preserve JSON.parse behavior with leading whitespace.

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 23, 2026

Merging this PR will improve performance by 8.72%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
✅ 2 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
ssr request loop (vue) 416.1 ms 401.4 ms +3.67%
ssr request loop (solid) 153.1 ms 140.8 ms +8.72%
ssr request loop (react) 328.1 ms 308.1 ms +6.49%
client-side navigation loop (react) 61.4 ms 58 ms +5.74%

Comparing flo/search-params-json-parse-guard (7a5b555) with main (6077120)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (bb3b8e4) during the generation of this report, so 6077120 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Only apply the JSON fast path to JSON.parse so documented custom serializers still round-trip, and add regression coverage for non-JSON parsers.
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.

♻️ Duplicate comments (1)
packages/router-core/src/searchParams.ts (1)

17-31: ⚠️ Potential issue | 🟠 Major

Fix JSON-literal detection for trailing whitespace.

At Line 29–Line 31, the literal checks use raw value, so inputs like "true " / "null\t" (valid for JSON.parse) fail the guard and stay strings. That regresses prior behavior and can break parse/stringify symmetry for literal-like query values.

Suggested patch
 function canStringBeJsonParsed(value: string) {
   if (!value) {
     return false
   }

-  const firstCharCode = value.charCodeAt(0)
-
-  if (firstCharCode <= 32) {
-    return true
-  }
+  const normalized = value.trim()
+  if (!normalized) {
+    return false
+  }
+
+  const firstCharCode = normalized.charCodeAt(0)

   return (
     firstCharCode === 34 ||
     firstCharCode === 45 ||
     firstCharCode === 91 ||
     firstCharCode === 123 ||
     (firstCharCode >= 48 && firstCharCode <= 57) ||
-    value === 'true' ||
-    value === 'false' ||
-    value === 'null'
+    normalized === 'true' ||
+    normalized === 'false' ||
+    normalized === 'null'
   )
 }
Does JavaScript `JSON.parse` accept trailing JSON whitespace for literals like "true ", "false\t", and "null\r"?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/searchParams.ts` around lines 17 - 31, The literal
checks currently compare the raw value so inputs with trailing whitespace (e.g.
"true ", "null\t") aren't recognized as JSON literals; create a trimmed version
(const trimmed = value.trim()) and use trimmed for the equality checks (trimmed
=== 'true' || trimmed === 'false' || trimmed === 'null') and for any
literal-specific comparisons instead of raw value, while keeping the existing
firstCharCode logic for the initial-character checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/router-core/src/searchParams.ts`:
- Around line 17-31: The literal checks currently compare the raw value so
inputs with trailing whitespace (e.g. "true ", "null\t") aren't recognized as
JSON literals; create a trimmed version (const trimmed = value.trim()) and use
trimmed for the equality checks (trimmed === 'true' || trimmed === 'false' ||
trimmed === 'null') and for any literal-specific comparisons instead of raw
value, while keeping the existing firstCharCode logic for the initial-character
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 98b675c9-eca6-4834-bf22-d3eea058e4b1

📥 Commits

Reviewing files that changed from the base of the PR and between 1ad29de and 7a5b555.

📒 Files selected for processing (2)
  • packages/router-core/src/searchParams.ts
  • packages/router-core/tests/searchParams.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/router-core/tests/searchParams.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant