From d1bbee88d7b7ee88f00d7642881d73f823a6a616 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 07:21:29 +0200 Subject: [PATCH 1/6] perf(router-core): fuse static node construction --- .../router-core/src/new-process-route-tree.ts | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts index 6978b071ce3..da82a43a500 100644 --- a/packages/router-core/src/new-process-route-tree.ts +++ b/packages/router-core/src/new-process-route-tree.ts @@ -220,35 +220,19 @@ function parseSegments( switch (kind) { case SEGMENT_TYPE_PATHNAME: { const value = path.substring(segment[2], segment[3]) - if (caseSensitive) { - const existingNode = node.static?.get(value) - if (existingNode) { - nextNode = existingNode - } else { - node.static ??= new Map() - const next = createStaticNode( - route.fullPath ?? route.from, - ) - next.parent = node - next.depth = depth - nextNode = next - node.static.set(value, next) - } + const name = caseSensitive ? value : value.toLowerCase() + const staticChildren = caseSensitive + ? (node.static ??= new Map()) + : (node.staticInsensitive ??= new Map()) + const existingNode = staticChildren.get(name) + if (existingNode) { + nextNode = existingNode } else { - const name = value.toLowerCase() - const existingNode = node.staticInsensitive?.get(name) - if (existingNode) { - nextNode = existingNode - } else { - node.staticInsensitive ??= new Map() - const next = createStaticNode( - route.fullPath ?? route.from, - ) - next.parent = node - next.depth = depth - nextNode = next - node.staticInsensitive.set(name, next) - } + const next = createStaticNode(path) + next.parent = node + next.depth = depth + nextNode = next + staticChildren.set(name, next) } break } From 2a037dc607b6c01d8003a1034283d35e602ea8d8 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 07:44:24 +0200 Subject: [PATCH 2/6] test(router-core): cover static node construction --- .../tests/new-process-route-tree.test.ts | 62 +++++++++++++ .../route-tree-static-construction.bench.ts | 86 +++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 packages/router-core/tests/route-tree-static-construction.bench.ts diff --git a/packages/router-core/tests/new-process-route-tree.test.ts b/packages/router-core/tests/new-process-route-tree.test.ts index d93581355ea..0dc3d1091d7 100644 --- a/packages/router-core/tests/new-process-route-tree.test.ts +++ b/packages/router-core/tests/new-process-route-tree.test.ts @@ -390,6 +390,63 @@ describe('findRouteMatch', () => { }) describe('case sensitivity competition', () => { + it('reuses an insensitive static node for differently cased siblings', () => { + const tree = { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: [ + { + id: '/Docs/API', + fullPath: '/Docs/API', + path: 'Docs/API', + }, + { + id: '/docs/guide', + fullPath: '/docs/guide', + path: 'docs/guide', + }, + ], + } + const { processedTree } = processRouteTree(tree) + const docs = processedTree.segmentTree.staticInsensitive?.get('docs') + + expect(processedTree.segmentTree.staticInsensitive?.size).toBe(1) + expect(docs?.staticInsensitive?.size).toBe(2) + expect(findRouteMatch('/DOCS/api', processedTree)?.route.id).toBe( + '/Docs/API', + ) + expect(findRouteMatch('/Docs/GUIDE', processedTree)?.route.id).toBe( + '/docs/guide', + ) + }) + it('allows a route to override a sensitive tree default', () => { + const tree = { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: [ + { + id: '/Strict', + fullPath: '/Strict', + path: 'Strict', + }, + { + id: '/loose', + fullPath: '/loose', + path: 'loose', + options: { caseSensitive: false }, + }, + ], + } + const { processedTree } = processRouteTree(tree, true) + + expect(findRouteMatch('/Strict', processedTree)?.route.id).toBe('/Strict') + expect(findRouteMatch('/strict', processedTree)).toBeNull() + expect(findRouteMatch('/LOOSE', processedTree)?.route.id).toBe('/loose') + }) it('a case sensitive segment early on should not prevent a case insensitive match', () => { const tree = { id: '__root__', @@ -1645,6 +1702,7 @@ describe('processRouteMasks', { sequential: true }, () => { { from: '/a/$param/d', routeTree }, { from: '/a/{-$optional}/d', routeTree }, { from: '/a/b/{$}.txt', routeTree }, + { from: '/Admin/Panel', routeTree }, ] processRouteMasks(routeMasks, processedTree) const aBranch = processedTree.masksTree?.staticInsensitive?.get('a') @@ -1657,6 +1715,10 @@ describe('processRouteMasks', { sequential: true }, () => { const res = findFlatMatch('/a/b/c', processedTree) expect(res?.route.from).toBe('/a/b/c') }) + it('matches uppercase static route masks case-insensitively', () => { + const res = findFlatMatch('/admin/panel', processedTree) + expect(res?.route.from).toBe('/Admin/Panel') + }) it('can match dynamic route masks w/ `findFlatMatch`', () => { const res = findFlatMatch('/a/123/d', processedTree) expect(res?.route.from).toBe('/a/$param/d') diff --git a/packages/router-core/tests/route-tree-static-construction.bench.ts b/packages/router-core/tests/route-tree-static-construction.bench.ts new file mode 100644 index 00000000000..54d10c5acdc --- /dev/null +++ b/packages/router-core/tests/route-tree-static-construction.bench.ts @@ -0,0 +1,86 @@ +import { bench, describe, expect } from 'vitest' +import { findRouteMatch, processRouteTree } from '../src/new-process-route-tree' + +type BenchRoute = { + id: string + fullPath: string + path?: string + isRoot?: boolean + children?: Array + options?: { + caseSensitive?: boolean + } +} + +function createStaticTree(caseSensitive: boolean, shared: boolean): BenchRoute { + return { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: Array.from({ length: 256 }, (_, index) => { + const section = shared ? index % 16 : index + const path = `/section-${section}/item-${index}` + return { + id: path, + fullPath: path, + path: path.slice(1), + options: { caseSensitive }, + } + }), + } +} + +const insensitiveShared = createStaticTree(false, true) +const insensitiveUnique = createStaticTree(false, false) +const sensitiveShared = createStaticTree(true, true) +const sensitiveUnique = createStaticTree(true, false) + +const insensitiveResult = processRouteTree(insensitiveShared) +expect( + findRouteMatch('/SECTION-3/ITEM-99', insensitiveResult.processedTree)?.route + .id, +).toBe('/section-3/item-99') +expect( + insensitiveResult.processedTree.segmentTree.staticInsensitive?.size, +).toBe(16) + +const sensitiveResult = processRouteTree(sensitiveShared) +expect( + findRouteMatch('/section-3/item-99', sensitiveResult.processedTree)?.route.id, +).toBe('/section-3/item-99') +expect( + findRouteMatch('/SECTION-3/ITEM-99', sensitiveResult.processedTree), +).toBe(null) +expect(sensitiveResult.processedTree.segmentTree.static?.size).toBe(16) + +let benchmarkSink = 0 + +function buildTrees(tree: BenchRoute, caseSensitive: boolean) { + for (let i = 0; i < 10; i++) { + const root = processRouteTree(tree).processedTree.segmentTree + benchmarkSink += caseSensitive + ? (root.static?.size ?? 0) + : (root.staticInsensitive?.size ?? 0) + } +} + +describe('static route tree construction', () => { + bench('build 10 insensitive trees with shared prefixes', () => { + buildTrees(insensitiveShared, false) + }) + + bench('build 10 insensitive trees with unique prefixes', () => { + buildTrees(insensitiveUnique, false) + }) + + bench('build 10 sensitive trees with shared prefixes', () => { + buildTrees(sensitiveShared, true) + }) + + bench('build 10 sensitive trees with unique prefixes', () => { + buildTrees(sensitiveUnique, true) + }) +}) + +void benchmarkSink From 0813b785db3540ff1ee1c73766e22fe3c41367da Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 07:49:23 +0200 Subject: [PATCH 3/6] perf(router-core): preserve one sensitivity branch --- packages/router-core/src/new-process-route-tree.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts index da82a43a500..a341d448d05 100644 --- a/packages/router-core/src/new-process-route-tree.ts +++ b/packages/router-core/src/new-process-route-tree.ts @@ -220,10 +220,14 @@ function parseSegments( switch (kind) { case SEGMENT_TYPE_PATHNAME: { const value = path.substring(segment[2], segment[3]) - const name = caseSensitive ? value : value.toLowerCase() - const staticChildren = caseSensitive - ? (node.static ??= new Map()) - : (node.staticInsensitive ??= new Map()) + let name = value + let staticChildren: Map> + if (caseSensitive) { + staticChildren = node.static ??= new Map() + } else { + name = value.toLowerCase() + staticChildren = node.staticInsensitive ??= new Map() + } const existingNode = staticChildren.get(name) if (existingNode) { nextNode = existingNode From 6457582f4ebfff68582fe67d4f76a03d3a7c47e4 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 08:00:27 +0200 Subject: [PATCH 4/6] docs: record static route node bundle result --- ...T-optimization-fused-static-route-nodes.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 RESULT-optimization-fused-static-route-nodes.md diff --git a/RESULT-optimization-fused-static-route-nodes.md b/RESULT-optimization-fused-static-route-nodes.md new file mode 100644 index 00000000000..ff617131a6d --- /dev/null +++ b/RESULT-optimization-fused-static-route-nodes.md @@ -0,0 +1,128 @@ +# Fuse static route-node construction + +Baseline: `main` at `697ebb6ddbd433d052b6b4707938a5c595865d58`. + +## Principle + +When two private branches differ only in the key and collection they select, +make that selection once and share the identical lookup and construction work. +Keep the distinguishing names descriptive, and preserve a real branch when a +branchless expression has a measurable runtime cost. + +The static route-segment paths previously duplicated node lookup, allocation, +parent/depth assignment, and map insertion for case-sensitive and +case-insensitive routes. The candidate selects the same descriptive `name` and +`staticChildren` map in one sensitivity branch, then performs that common work +once. No public API or emitted declaration changes. + +## Bundle result + +`react-router.minimal`: + +| Metric | Before | After | Change | +| -------------- | --------: | --------: | -----: | +| raw | 275,591 B | 275,429 B | -162 B | +| initial raw | 275,456 B | 275,294 B | -162 B | +| gzip | 89,200 B | 89,164 B | -36 B | +| initial gzip | 89,058 B | 89,026 B | -32 B | +| Brotli | 77,742 B | 77,581 B | -161 B | +| initial Brotli | 77,635 B | 77,476 B | -159 B | + +Because the router-core code is retained by every scenario, all 17 scenarios +improve by 162 raw bytes. Gzip improves in every scenario by 22–45 B, and +initial gzip improves by 22–44 B. Brotli ranges from -161 B to +43 B: nine +scenarios improve, one is unchanged, and seven have small compression +interactions despite containing 162 fewer raw bytes. + +| Scenario | Raw | Gzip | Initial gzip | Brotli | +| -------------------------------- | -----: | ----: | -----------: | -----: | +| react-router.minimal | -162 B | -36 B | -32 B | -161 B | +| react-router.full | -162 B | -32 B | -35 B | -117 B | +| solid-router.minimal | -162 B | -27 B | -29 B | +1 B | +| solid-router.full | -162 B | -37 B | -37 B | -3 B | +| vue-router.minimal | -162 B | -32 B | -29 B | +22 B | +| vue-router.full | -162 B | -33 B | -34 B | +21 B | +| react-start.minimal | -162 B | -45 B | -43 B | -34 B | +| react-start.deferred-hydration | -162 B | -44 B | -44 B | -123 B | +| react-start.full | -162 B | -24 B | -22 B | 0 B | +| react-start.rsbuild.minimal | -162 B | -22 B | -22 B | -93 B | +| react-start.rsbuild.minimal-iife | -162 B | -22 B | -22 B | +43 B | +| react-start.rsbuild.full | -162 B | -26 B | -26 B | -53 B | +| solid-start.minimal | -162 B | -41 B | -39 B | +16 B | +| solid-start.deferred-hydration | -162 B | -42 B | -41 B | -64 B | +| solid-start.full | -162 B | -41 B | -40 B | +27 B | +| vue-start.minimal | -162 B | -32 B | -33 B | +1 B | +| vue-start.full | -162 B | -34 B | -32 B | -63 B | + +Fresh paired full-matrix artifacts: + +- exact base: `/private/tmp/router-bundle-baseline-full.json` +- final candidate at `0813b785db3540ff1ee1c73766e22fe3c41367da`: + `/private/tmp/static-node-final-full.json` + +## Attribution and runtime gate + +The first fused form used two conditional expressions and measured 178 B raw / +43 B gzip smaller in `react-router.minimal`, but broad construction benchmarks +showed a possible slowdown on a sensitive route distribution. The final form +retains one explicit sensitivity branch. It gives back 16 raw bytes and 7 gzip +bytes in that scenario, materially improves the Brotli result, and removes the +questionable runtime result. + +The focused benchmark constructs 256-route static-heavy trees in four +distributions, batching ten complete builds per sample. Median mean times across +three runs, in milliseconds per ten builds: + +| Distribution | Exact base | Candidate | Interpretation | +| ------------------ | ---------: | --------: | ------------------ | +| insensitive/shared | 0.7998 | 0.7482 | ~6.5% faster | +| insensitive/unique | 0.8291 | 0.7684 | ~7.3% faster | +| sensitive/unique | 0.7375 | 0.7303 | neutral/~1% faster | + +The cross-case broad run was noisy for sensitive/shared construction, so that +case was isolated and repeated three times. Its median mean was 0.7450 ms on the +base and 0.7344 ms on the candidate; median p75 was 0.7479 vs 0.7476 ms, with +0.75–1.75% RME. It is therefore treated as neutral, not claimed as a speedup. +No distribution has a reproducible regression. + +## Runtime and compatibility + +- Case-sensitive and case-insensitive nodes still use distinct maps and the + same respective key casing. +- The selected map is allocated lazily under the same condition as before. +- Lookup still precedes allocation, so shared prefixes reuse the same node. +- `fullPath`, `parent`, `depth`, and insertion order are unchanged. +- Route-level sensitivity overrides and route masks retain their behavior. +- No exports, public signatures, route options, node fields, module boundaries, + annotations, or top-level effects change. + +## Validation + +- Focused static route-tree file: 172 passed. +- Router-core full unit suite: 1,526 passed and 3 expected failures; no Vitest + type errors. +- Router-core type suite: all configured TypeScript versions from 5.6 through + 7.0 passed. +- Router-core ESLint: 0 errors; 26 pre-existing warnings. +- Generator CLI React e2e: 3 passed, covering a post page, nested pathless route, + and not-found route. +- Full 17-scenario bundle-size matrix: passed. +- A focused static construction benchmark ran on both the exact base plus tests + and final production candidate because router-core has no Nx `test:perf` + target. +- Five independent reviews approved semantics, test adequacy, public API and + tree-shaking safety, runtime evidence, bundle attribution, and publishability. +- Formatting and `git diff --check`: passed. + +Focused tests cover differently cased insensitive siblings sharing a node while +retaining distinct children, the sensitive default with a route-level +insensitive override, and an uppercase route mask matched insensitively. The +benchmark also asserts the constructed map sizes and matching behavior before +timing each distribution. + +## Integration note + +Draft PR #7974 changes the same `parseSegments` switch. Whichever route-node +fusion lands second must be rebased carefully and have its focused benchmark and +full bundle matrix rerun; the independent byte attribution above should not be +carried across that rebase. From ed785b6ba3e4dcef531667417fbaf13b8616f469 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 6 Aug 2026 12:27:30 +0200 Subject: [PATCH 5/6] Delete packages/router-core/tests/route-tree-static-construction.bench.ts --- .../route-tree-static-construction.bench.ts | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 packages/router-core/tests/route-tree-static-construction.bench.ts diff --git a/packages/router-core/tests/route-tree-static-construction.bench.ts b/packages/router-core/tests/route-tree-static-construction.bench.ts deleted file mode 100644 index 54d10c5acdc..00000000000 --- a/packages/router-core/tests/route-tree-static-construction.bench.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { bench, describe, expect } from 'vitest' -import { findRouteMatch, processRouteTree } from '../src/new-process-route-tree' - -type BenchRoute = { - id: string - fullPath: string - path?: string - isRoot?: boolean - children?: Array - options?: { - caseSensitive?: boolean - } -} - -function createStaticTree(caseSensitive: boolean, shared: boolean): BenchRoute { - return { - id: '__root__', - isRoot: true, - fullPath: '/', - path: '/', - children: Array.from({ length: 256 }, (_, index) => { - const section = shared ? index % 16 : index - const path = `/section-${section}/item-${index}` - return { - id: path, - fullPath: path, - path: path.slice(1), - options: { caseSensitive }, - } - }), - } -} - -const insensitiveShared = createStaticTree(false, true) -const insensitiveUnique = createStaticTree(false, false) -const sensitiveShared = createStaticTree(true, true) -const sensitiveUnique = createStaticTree(true, false) - -const insensitiveResult = processRouteTree(insensitiveShared) -expect( - findRouteMatch('/SECTION-3/ITEM-99', insensitiveResult.processedTree)?.route - .id, -).toBe('/section-3/item-99') -expect( - insensitiveResult.processedTree.segmentTree.staticInsensitive?.size, -).toBe(16) - -const sensitiveResult = processRouteTree(sensitiveShared) -expect( - findRouteMatch('/section-3/item-99', sensitiveResult.processedTree)?.route.id, -).toBe('/section-3/item-99') -expect( - findRouteMatch('/SECTION-3/ITEM-99', sensitiveResult.processedTree), -).toBe(null) -expect(sensitiveResult.processedTree.segmentTree.static?.size).toBe(16) - -let benchmarkSink = 0 - -function buildTrees(tree: BenchRoute, caseSensitive: boolean) { - for (let i = 0; i < 10; i++) { - const root = processRouteTree(tree).processedTree.segmentTree - benchmarkSink += caseSensitive - ? (root.static?.size ?? 0) - : (root.staticInsensitive?.size ?? 0) - } -} - -describe('static route tree construction', () => { - bench('build 10 insensitive trees with shared prefixes', () => { - buildTrees(insensitiveShared, false) - }) - - bench('build 10 insensitive trees with unique prefixes', () => { - buildTrees(insensitiveUnique, false) - }) - - bench('build 10 sensitive trees with shared prefixes', () => { - buildTrees(sensitiveShared, true) - }) - - bench('build 10 sensitive trees with unique prefixes', () => { - buildTrees(sensitiveUnique, true) - }) -}) - -void benchmarkSink From f59c15d18541ae143791dd5bb7a981a7a8673cd8 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 6 Aug 2026 12:27:40 +0200 Subject: [PATCH 6/6] Delete RESULT-optimization-fused-static-route-nodes.md --- ...T-optimization-fused-static-route-nodes.md | 128 ------------------ 1 file changed, 128 deletions(-) delete mode 100644 RESULT-optimization-fused-static-route-nodes.md diff --git a/RESULT-optimization-fused-static-route-nodes.md b/RESULT-optimization-fused-static-route-nodes.md deleted file mode 100644 index ff617131a6d..00000000000 --- a/RESULT-optimization-fused-static-route-nodes.md +++ /dev/null @@ -1,128 +0,0 @@ -# Fuse static route-node construction - -Baseline: `main` at `697ebb6ddbd433d052b6b4707938a5c595865d58`. - -## Principle - -When two private branches differ only in the key and collection they select, -make that selection once and share the identical lookup and construction work. -Keep the distinguishing names descriptive, and preserve a real branch when a -branchless expression has a measurable runtime cost. - -The static route-segment paths previously duplicated node lookup, allocation, -parent/depth assignment, and map insertion for case-sensitive and -case-insensitive routes. The candidate selects the same descriptive `name` and -`staticChildren` map in one sensitivity branch, then performs that common work -once. No public API or emitted declaration changes. - -## Bundle result - -`react-router.minimal`: - -| Metric | Before | After | Change | -| -------------- | --------: | --------: | -----: | -| raw | 275,591 B | 275,429 B | -162 B | -| initial raw | 275,456 B | 275,294 B | -162 B | -| gzip | 89,200 B | 89,164 B | -36 B | -| initial gzip | 89,058 B | 89,026 B | -32 B | -| Brotli | 77,742 B | 77,581 B | -161 B | -| initial Brotli | 77,635 B | 77,476 B | -159 B | - -Because the router-core code is retained by every scenario, all 17 scenarios -improve by 162 raw bytes. Gzip improves in every scenario by 22–45 B, and -initial gzip improves by 22–44 B. Brotli ranges from -161 B to +43 B: nine -scenarios improve, one is unchanged, and seven have small compression -interactions despite containing 162 fewer raw bytes. - -| Scenario | Raw | Gzip | Initial gzip | Brotli | -| -------------------------------- | -----: | ----: | -----------: | -----: | -| react-router.minimal | -162 B | -36 B | -32 B | -161 B | -| react-router.full | -162 B | -32 B | -35 B | -117 B | -| solid-router.minimal | -162 B | -27 B | -29 B | +1 B | -| solid-router.full | -162 B | -37 B | -37 B | -3 B | -| vue-router.minimal | -162 B | -32 B | -29 B | +22 B | -| vue-router.full | -162 B | -33 B | -34 B | +21 B | -| react-start.minimal | -162 B | -45 B | -43 B | -34 B | -| react-start.deferred-hydration | -162 B | -44 B | -44 B | -123 B | -| react-start.full | -162 B | -24 B | -22 B | 0 B | -| react-start.rsbuild.minimal | -162 B | -22 B | -22 B | -93 B | -| react-start.rsbuild.minimal-iife | -162 B | -22 B | -22 B | +43 B | -| react-start.rsbuild.full | -162 B | -26 B | -26 B | -53 B | -| solid-start.minimal | -162 B | -41 B | -39 B | +16 B | -| solid-start.deferred-hydration | -162 B | -42 B | -41 B | -64 B | -| solid-start.full | -162 B | -41 B | -40 B | +27 B | -| vue-start.minimal | -162 B | -32 B | -33 B | +1 B | -| vue-start.full | -162 B | -34 B | -32 B | -63 B | - -Fresh paired full-matrix artifacts: - -- exact base: `/private/tmp/router-bundle-baseline-full.json` -- final candidate at `0813b785db3540ff1ee1c73766e22fe3c41367da`: - `/private/tmp/static-node-final-full.json` - -## Attribution and runtime gate - -The first fused form used two conditional expressions and measured 178 B raw / -43 B gzip smaller in `react-router.minimal`, but broad construction benchmarks -showed a possible slowdown on a sensitive route distribution. The final form -retains one explicit sensitivity branch. It gives back 16 raw bytes and 7 gzip -bytes in that scenario, materially improves the Brotli result, and removes the -questionable runtime result. - -The focused benchmark constructs 256-route static-heavy trees in four -distributions, batching ten complete builds per sample. Median mean times across -three runs, in milliseconds per ten builds: - -| Distribution | Exact base | Candidate | Interpretation | -| ------------------ | ---------: | --------: | ------------------ | -| insensitive/shared | 0.7998 | 0.7482 | ~6.5% faster | -| insensitive/unique | 0.8291 | 0.7684 | ~7.3% faster | -| sensitive/unique | 0.7375 | 0.7303 | neutral/~1% faster | - -The cross-case broad run was noisy for sensitive/shared construction, so that -case was isolated and repeated three times. Its median mean was 0.7450 ms on the -base and 0.7344 ms on the candidate; median p75 was 0.7479 vs 0.7476 ms, with -0.75–1.75% RME. It is therefore treated as neutral, not claimed as a speedup. -No distribution has a reproducible regression. - -## Runtime and compatibility - -- Case-sensitive and case-insensitive nodes still use distinct maps and the - same respective key casing. -- The selected map is allocated lazily under the same condition as before. -- Lookup still precedes allocation, so shared prefixes reuse the same node. -- `fullPath`, `parent`, `depth`, and insertion order are unchanged. -- Route-level sensitivity overrides and route masks retain their behavior. -- No exports, public signatures, route options, node fields, module boundaries, - annotations, or top-level effects change. - -## Validation - -- Focused static route-tree file: 172 passed. -- Router-core full unit suite: 1,526 passed and 3 expected failures; no Vitest - type errors. -- Router-core type suite: all configured TypeScript versions from 5.6 through - 7.0 passed. -- Router-core ESLint: 0 errors; 26 pre-existing warnings. -- Generator CLI React e2e: 3 passed, covering a post page, nested pathless route, - and not-found route. -- Full 17-scenario bundle-size matrix: passed. -- A focused static construction benchmark ran on both the exact base plus tests - and final production candidate because router-core has no Nx `test:perf` - target. -- Five independent reviews approved semantics, test adequacy, public API and - tree-shaking safety, runtime evidence, bundle attribution, and publishability. -- Formatting and `git diff --check`: passed. - -Focused tests cover differently cased insensitive siblings sharing a node while -retaining distinct children, the sensitive default with a route-level -insensitive override, and an uppercase route mask matched insensitively. The -benchmark also asserts the constructed map sizes and matching behavior before -timing each distribution. - -## Integration note - -Draft PR #7974 changes the same `parseSegments` switch. Whichever route-node -fusion lands second must be rebased carefully and have its focused benchmark and -full bundle matrix rerun; the independent byte attribution above should not be -carried across that rebase.