From 86af623b45e0c6b53f592d501049c925096383f7 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:10:14 +0200 Subject: [PATCH 1/4] test(router-core): cover deferred route sorting --- .../tests/new-process-route-tree.test.ts | 124 +++++++++++- .../tests/route-tree-construction.bench.ts | 178 ++++++++++++++++++ 2 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 packages/router-core/tests/route-tree-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 d93581355e..acd60e10da 100644 --- a/packages/router-core/tests/new-process-route-tree.test.ts +++ b/packages/router-core/tests/new-process-route-tree.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { beforeAll, describe, expect, it } from 'vitest' import { findFlatMatch, findRouteMatch, @@ -129,6 +129,86 @@ describe('findRouteMatch', () => { }) }) + it('sorts parser priorities after building required, optional, and wildcard siblings', () => { + const cases = [ + ['pre{$value}suf', 'dynamic'], + ['pre{-$value}suf', 'optional'], + ['pre{$}suf', 'wildcard'], + ] as const + + for (const [segment, siblingKind] of cases) { + let acceptHigherPriority = true + const fullPath = `/a/${segment}` + const routeTree = { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: [ + { + id: `low-${siblingKind}`, + fullPath, + path: fullPath.slice(1), + options: { + params: { + priority: 1, + parse: (params: Record) => params, + }, + }, + }, + { + id: `high-${siblingKind}`, + fullPath, + path: fullPath.slice(1), + options: { + params: { + priority: 2, + parse: (params: Record) => + acceptHigherPriority ? params : false, + }, + }, + }, + ], + } + const { processedTree } = processRouteTree(routeTree) + const branch = processedTree.segmentTree.staticInsensitive?.get('a') + + expect(branch?.[siblingKind]).toHaveLength(2) + expect(findRouteMatch('/a/prewinsuf', processedTree)?.route.id).toBe( + `high-${siblingKind}`, + ) + + acceptHigherPriority = false + expect( + findRouteMatch('/a/prefallbacksuf', processedTree)?.route.id, + ).toBe(`low-${siblingKind}`) + } + }) + + it('reuses optional nodes with the same shape when they have no parser', () => { + const tree = makeTree([ + '/a/{-$first}/first-child', + '/a/{-$second}/second-child', + ]) + const branch = tree.segmentTree.staticInsensitive?.get('a') + + expect(branch?.optional).toHaveLength(1) + expect(findRouteMatch('/a/value/first-child', tree)?.route.id).toBe( + '/a/{-$first}/first-child', + ) + expect(findRouteMatch('/a/value/second-child', tree)?.route.id).toBe( + '/a/{-$second}/second-child', + ) + }) + + it('keeps same-shaped wildcard aliases as separate match candidates', () => { + const tree = makeTree(['/a/$', '/a/{$}']) + const branch = tree.segmentTree.staticInsensitive?.get('a') + + expect(branch?.wildcard).toHaveLength(2) + expect(findRouteMatch('/a/value', tree)?.route.id).toBe('/a/$') + }) + describe('prefix / suffix lengths', () => { it('longer overlapping prefix wins over shorter prefix', () => { const tree = makeTree(['/a/b{$b}', '/a/bbbb{$b}']) @@ -1638,15 +1718,26 @@ describe('processRouteMasks', { sequential: true }, () => { fullPath: '/', } as AnyRoute const { processedTree } = processRouteTree(routeTree) - it('processes a route masks list into a segment tree', () => { - const routeMasks: Array> = [ - { from: '/a/b/c', routeTree }, - { from: '/a/b/d', routeTree }, - { from: '/a/$param/d', routeTree }, - { from: '/a/{-$optional}/d', routeTree }, - { from: '/a/b/{$}.txt', routeTree }, - ] + const routeMasks: Array> = [ + { from: '/a/b/c', routeTree }, + { from: '/a/b/d', routeTree }, + { from: '/a/$param/d', routeTree }, + { from: '/a/{-$optional}/d', routeTree }, + { from: '/a/b/{$}.txt', routeTree }, + { from: '/a/$', routeTree }, + { from: '/a/foo{$}', routeTree }, + { from: '/a/foo{$}bar', routeTree }, + { from: '/required/$param', routeTree }, + { from: '/required/foo{$param}bar', routeTree }, + { from: '/optional/{-$param}', routeTree }, + { from: '/optional/foo{-$param}bar', routeTree }, + ] + + beforeAll(() => { processRouteMasks(routeMasks, processedTree) + }) + + it('processes a route masks list into a segment tree', () => { const aBranch = processedTree.masksTree?.staticInsensitive?.get('a') expect(aBranch).toBeDefined() expect(aBranch?.staticInsensitive?.get('b')).toBeDefined() @@ -1672,4 +1763,19 @@ describe('processRouteMasks', { sequential: true }, () => { expect(res?.route.from).toBe('/a/b/{$}.txt') expect(res?.rawParams).toEqual({ '*': 'file/path', _splat: 'file/path' }) }) + it('sorts competing wildcard masks by specificity', () => { + const res = findFlatMatch('/a/fooxbar', processedTree) + expect(res?.route.from).toBe('/a/foo{$}bar') + expect(res?.rawParams).toEqual({ '*': 'x', _splat: 'x' }) + }) + it('sorts competing required masks by specificity', () => { + const res = findFlatMatch('/required/fooxbar', processedTree) + expect(res?.route.from).toBe('/required/foo{$param}bar') + expect(res?.rawParams).toEqual({ param: 'x' }) + }) + it('sorts competing optional masks by specificity', () => { + const res = findFlatMatch('/optional/fooxbar', processedTree) + expect(res?.route.from).toBe('/optional/foo{-$param}bar') + expect(res?.rawParams).toEqual({ param: 'x' }) + }) }) diff --git a/packages/router-core/tests/route-tree-construction.bench.ts b/packages/router-core/tests/route-tree-construction.bench.ts new file mode 100644 index 0000000000..39f86b8088 --- /dev/null +++ b/packages/router-core/tests/route-tree-construction.bench.ts @@ -0,0 +1,178 @@ +import { bench, describe, expect } from 'vitest' +import { + findFlatMatch, + findRouteMatch, + processRouteMasks, + processRouteTree, +} from '../src/new-process-route-tree' + +type BenchRoute = { + id: string + fullPath: string + path?: string + isRoot?: boolean + children?: Array + options?: { + caseSensitive?: boolean + params?: { + parse?: (params: Record) => unknown + priority?: number + } + } +} + +const mostlyStaticTree: BenchRoute = { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: Array.from({ length: 256 }, (_, index) => ({ + id: `/section-${index % 16}/item-${index}`, + fullPath: `/section-${index % 16}/item-${index}`, + path: `section-${index % 16}/item-${index}`, + })), +} + +const dynamicPatterns = [ + '$value', + 'pre{$value}', + '{$value}suf', + 'pre{$value}suf', + '{-$value}', + 'pre{-$value}', + '{-$value}suf', + 'pre{-$value}suf', + '$', + 'pre{$}', + '{$}suf', + 'pre{$}suf', +] + +const denseDynamicTree: BenchRoute = { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: Array.from({ length: 16 }, (_, group) => + dynamicPatterns.map((pattern, index) => ({ + id: `/group-${group}/${pattern}`, + fullPath: `/group-${group}/${pattern}`, + path: `group-${group}/${pattern}`, + options: { + params: + index % 3 === 0 + ? { + parse: (params: Record) => params, + priority: index % 4, + } + : undefined, + }, + })), + ).flat(), +} + +const reusedDynamicTree: BenchRoute = { + id: '__root__', + isRoot: true, + fullPath: '/', + path: '/', + children: Array.from({ length: 256 }, (_, index) => ({ + id: `/shared/$value/item-${index}`, + fullPath: `/shared/$value/item-${index}`, + path: `shared/$value/item-${index}`, + })), +} + +const maskBase = processRouteTree({ + id: '__root__', + isRoot: true, + fullPath: '/', +}).processedTree +const routeMasks = dynamicPatterns.map((pattern) => ({ + from: `/group/${pattern}`, + routeTree: denseDynamicTree, +})) + +const staticResult = processRouteTree(mostlyStaticTree) +expect( + findRouteMatch('/section-3/item-99', staticResult.processedTree)?.route.id, +).toBe('/section-3/item-99') + +const dynamicResult = processRouteTree(denseDynamicTree) +expect( + findRouteMatch('/group-0/prexsuf', dynamicResult.processedTree)?.route.id, +).toBe('/group-0/pre{$value}suf') +const denseBranch = + dynamicResult.processedTree.segmentTree.staticInsensitive?.get('group-0') +expect(denseBranch?.dynamic?.map((node) => node.fullPath)).toEqual([ + '/group-0/pre{$value}suf', + '/group-0/$value', + '/group-0/pre{$value}', + '/group-0/{$value}suf', +]) +expect(denseBranch?.optional?.map((node) => node.fullPath)).toEqual([ + '/group-0/{-$value}suf', + '/group-0/pre{-$value}suf', + '/group-0/pre{-$value}', + '/group-0/{-$value}', +]) +expect(denseBranch?.wildcard?.map((node) => node.fullPath)).toEqual([ + '/group-0/pre{$}', + '/group-0/pre{$}suf', + '/group-0/{$}suf', + '/group-0/$', +]) + +const reusedResult = processRouteTree(reusedDynamicTree) +expect( + reusedResult.processedTree.segmentTree.staticInsensitive?.get('shared') + ?.dynamic, +).toHaveLength(1) + +processRouteMasks(routeMasks, maskBase) +expect(findFlatMatch('/group/prexsuf', maskBase)?.route.from).toBe( + '/group/pre{$value}suf', +) + +let benchmarkSink = 0 + +describe('route tree construction', () => { + bench('build 10 mostly static route trees', () => { + for (let i = 0; i < 10; i++) { + benchmarkSink += + processRouteTree(mostlyStaticTree).processedTree.segmentTree + .staticInsensitive?.size ?? 0 + } + }) + + bench('build 10 dense dynamic route trees', () => { + for (let i = 0; i < 10; i++) { + benchmarkSink += + processRouteTree(denseDynamicTree).processedTree.segmentTree + .staticInsensitive?.size ?? 0 + } + }) + + bench('build 10 same-shape dynamic route trees', () => { + for (let i = 0; i < 10; i++) { + benchmarkSink += + processRouteTree( + reusedDynamicTree, + ).processedTree.segmentTree.staticInsensitive?.get('shared')?.dynamic + ?.length ?? 0 + } + }) + + bench('build 10 dense route-mask trees', () => { + for (let i = 0; i < 10; i++) { + processRouteMasks(routeMasks, maskBase) + const group = maskBase.masksTree?.staticInsensitive?.get('group') + benchmarkSink += + (group?.dynamic?.length ?? 0) + + (group?.optional?.length ?? 0) + + (group?.wildcard?.length ?? 0) + } + }) +}) + +void benchmarkSink From c4597aeb43aae0ca25c1ffefa44f5584c8bc870c Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:20:25 +0200 Subject: [PATCH 2/4] perf(router-core): fuse dynamic route node construction --- RESULT-optimization-fused-route-nodes.md | 67 ++++++ .../router-core/src/new-process-route-tree.ts | 225 ++++++------------ 2 files changed, 142 insertions(+), 150 deletions(-) create mode 100644 RESULT-optimization-fused-route-nodes.md diff --git a/RESULT-optimization-fused-route-nodes.md b/RESULT-optimization-fused-route-nodes.md new file mode 100644 index 0000000000..0b31e61d6f --- /dev/null +++ b/RESULT-optimization-fused-route-nodes.md @@ -0,0 +1,67 @@ +# Fused dynamic route-node construction + +Baseline: `main` at `697ebb6ddbd433d052b6b4707938a5c595865d58`. + +## Bundle result + +`react-router.minimal`: + +| Metric | Before | After | Change | +| --- | ---: | ---: | ---: | +| gzip | 89,200 B | 89,145 B | -55 B | +| initial gzip | 89,058 B | 89,004 B | -54 B | +| raw | 275,591 B | 274,715 B | -876 B | +| Brotli | 77,742 B | 77,571 B | -171 B | + +All 17 bundle scenarios improved in gzip by 55–125 B and in raw size by +870–904 B. Initial gzip improved by 54–125 B. Brotli changed by -180 to ++83 B; 6 of 17 scenarios had a small Brotli regression despite the raw and +gzip reductions. + +Hunk-level gzip attribution on `react-router.minimal`: + +| Change | Isolated/cumulative change | +| --- | ---: | +| Record only dynamic sibling lists that need sorting | -16 B isolated | +| Cache parsed route fields | -1 B isolated | +| First two changes together | -19 B cumulative | +| Fuse required, optional, and wildcard construction | -36 B incremental | +| Complete group | -55 B cumulative | + +Compression is nonlinear, so the isolated figures do not add exactly. + +## Construction benchmark + +Each value is the median mean time, in milliseconds, from three runs. Every +sample builds the tree ten times. + +| Cumulative stage | Mostly static | Dense dynamic | Reused dynamic shape | Route masks | +| --- | ---: | ---: | ---: | ---: | +| Baseline | 0.8106 | 0.6019 | 0.8167 | 0.0245 | +| Sparse sorting | 0.7658 | 0.5813 | 0.8007 | 0.0224 | +| Cached route fields | 0.7607 | 0.5686 | 0.7964 | 0.0222 | +| Fused construction | 0.7853 | 0.5785 | 0.8005 | 0.0224 | + +The complete group was approximately 3.1%, 3.9%, 2.0%, and 8.6% faster than +baseline across those workloads. The fusion hunk alone moved the cumulative +median by +0.5% to +3.2%, so it should be treated as a size optimization, not +as an independent runtime-performance improvement. + +## Correctness coverage + +- Required, optional, and wildcard parser priorities are sorted only after + parser metadata is assigned, including parser rejection and fallback. +- Same-shape required and optional nodes without parsers remain reusable. +- Same-shape wildcard aliases remain separate match candidates. +- Route-mask dynamic, optional, and wildcard sibling lists are sorted by + specificity and can be run independently. +- Construction benchmarks validate representative matches and the sorted + sibling arrays before collecting timing samples. + +Validation passed: + +- Router-core unit tests: 1,529 passed and 3 expected failures. +- Router-core type tests across all configured TypeScript versions. +- Router-core ESLint (no errors; existing warnings remain). +- React Router generator CLI end-to-end suite: 3 passed. +- Full 17-scenario bundle-size matrix. diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts index 6978b071ce..975524f244 100644 --- a/packages/router-core/src/new-process-route-tree.ts +++ b/packages/router-core/src/new-process-route-tree.ts @@ -199,16 +199,18 @@ function parseSegments( start: number, node: AnySegmentNode, depth: number, + /** Each dynamic sibling list is recorded once, when it first needs sorting. */ + dynamicListsToSort?: Array>>, onRoute?: (route: TRouteLike) => void, ) { onRoute?.(route) let cursor = start { const path = route.fullPath ?? route.from + const options = route.options const length = path.length - const caseSensitive = route.options?.caseSensitive ?? defaultCaseSensitive - const parseParams = - route.options?.params?.parse ?? route.options?.parseParams + const caseSensitive = options?.caseSensitive ?? defaultCaseSensitive + const parseParams = options?.params?.parse ?? options?.parseParams while (cursor < length) { const segment = parseSegment(path, cursor, data) let nextNode: AnySegmentNode @@ -226,9 +228,7 @@ function parseSegments( nextNode = existingNode } else { node.static ??= new Map() - const next = createStaticNode( - route.fullPath ?? route.from, - ) + const next = createStaticNode(path) next.parent = node next.depth = depth nextNode = next @@ -241,9 +241,7 @@ function parseSegments( nextNode = existingNode } else { node.staticInsensitive ??= new Map() - const next = createStaticNode( - route.fullPath ?? route.from, - ) + const next = createStaticNode(path) next.parent = node next.depth = depth nextNode = next @@ -252,49 +250,9 @@ function parseSegments( } break } - case SEGMENT_TYPE_PARAM: { - const prefix_raw = path.substring(start, segment[1]) - const suffix_raw = path.substring(segment[4], end) - const actuallyCaseSensitive = - caseSensitive && !!(prefix_raw || suffix_raw) - const prefix = !prefix_raw - ? undefined - : actuallyCaseSensitive - ? prefix_raw - : prefix_raw.toLowerCase() - const suffix = !suffix_raw - ? undefined - : actuallyCaseSensitive - ? suffix_raw - : suffix_raw.toLowerCase() - const existingNode = - !parseParams && - node.dynamic?.find( - (s) => - !s.parse && - s.caseSensitive === actuallyCaseSensitive && - s.prefix === prefix && - s.suffix === suffix, - ) - if (existingNode) { - nextNode = existingNode - } else { - const next = createDynamicNode( - SEGMENT_TYPE_PARAM, - route.fullPath ?? route.from, - actuallyCaseSensitive, - prefix, - suffix, - ) - nextNode = next - next.depth = depth - next.parent = node - node.dynamic ??= [] - node.dynamic.push(next) - } - break - } - case SEGMENT_TYPE_OPTIONAL_PARAM: { + case SEGMENT_TYPE_PARAM: + case SEGMENT_TYPE_OPTIONAL_PARAM: + case SEGMENT_TYPE_WILDCARD: { const prefix_raw = path.substring(start, segment[1]) const suffix_raw = path.substring(segment[4], end) const actuallyCaseSensitive = @@ -309,9 +267,18 @@ function parseSegments( : actuallyCaseSensitive ? suffix_raw : suffix_raw.toLowerCase() + const siblings = + kind === SEGMENT_TYPE_PARAM + ? node.dynamic + : kind === SEGMENT_TYPE_OPTIONAL_PARAM + ? node.optional + : node.wildcard const existingNode = + // Keep wildcard aliases as separate match candidates, even when + // they have the same shape and no parser. + kind !== SEGMENT_TYPE_WILDCARD && !parseParams && - node.optional?.find( + siblings?.find( (s) => !s.parse && s.caseSensitive === actuallyCaseSensitive && @@ -322,8 +289,8 @@ function parseSegments( nextNode = existingNode } else { const next = createDynamicNode( - SEGMENT_TYPE_OPTIONAL_PARAM, - route.fullPath ?? route.from, + kind, + path, actuallyCaseSensitive, prefix, suffix, @@ -331,39 +298,21 @@ function parseSegments( nextNode = next next.parent = node next.depth = depth - node.optional ??= [] - node.optional.push(next) + let nodes: Array> + if (kind === SEGMENT_TYPE_PARAM) { + nodes = node.dynamic ??= [] + } else if (kind === SEGMENT_TYPE_OPTIONAL_PARAM) { + nodes = node.optional ??= [] + } else { + nodes = node.wildcard ??= [] + } + nodes.push(next) + if (nodes.length === 2) { + dynamicListsToSort?.push(nodes) + } } break } - case SEGMENT_TYPE_WILDCARD: { - const prefix_raw = path.substring(start, segment[1]) - const suffix_raw = path.substring(segment[4], end) - const actuallyCaseSensitive = - caseSensitive && !!(prefix_raw || suffix_raw) - const prefix = !prefix_raw - ? undefined - : actuallyCaseSensitive - ? prefix_raw - : prefix_raw.toLowerCase() - const suffix = !suffix_raw - ? undefined - : actuallyCaseSensitive - ? suffix_raw - : suffix_raw.toLowerCase() - const next = createDynamicNode( - SEGMENT_TYPE_WILDCARD, - route.fullPath ?? route.from, - actuallyCaseSensitive, - prefix, - suffix, - ) - nextNode = next - next.parent = node - next.depth = depth - node.wildcard ??= [] - node.wildcard.push(next) - } } node = nextNode } @@ -376,9 +325,7 @@ function parseSegments( route.id && route.id.charCodeAt(route.id.lastIndexOf('/') + 1) === 95 /* '_' */ ) { - const pathlessNode = createStaticNode( - route.fullPath ?? route.from, - ) + const pathlessNode = createStaticNode(path) pathlessNode.kind = SEGMENT_TYPE_PATHLESS pathlessNode.parent = node depth++ @@ -391,9 +338,7 @@ function parseSegments( const isLeaf = (route.path || !route.children) && !route.isRoot // create index node if (isLeaf && path.endsWith('/')) { - const indexNode = createStaticNode( - route.fullPath ?? route.from, - ) + const indexNode = createStaticNode(path) indexNode.kind = SEGMENT_TYPE_INDEX indexNode.parent = node depth++ @@ -403,12 +348,12 @@ function parseSegments( } node.parse = parseParams ?? null - node.priority = route.options?.params?.priority ?? 0 + node.priority = options?.params?.priority ?? 0 // make node "matchable" if (isLeaf && !node.route) { node.route = route - node.fullPath = route.fullPath ?? route.from + node.fullPath = path } } if (route.children) @@ -420,6 +365,7 @@ function parseSegments( cursor, node, depth, + dynamicListsToSort, onRoute, ) } @@ -464,42 +410,6 @@ function sortDynamic( return 0 } -function sortTreeNodes(node: SegmentNode) { - if (node.pathless) { - for (const child of node.pathless) { - sortTreeNodes(child) - } - } - if (node.static) { - for (const child of node.static.values()) { - sortTreeNodes(child) - } - } - if (node.staticInsensitive) { - for (const child of node.staticInsensitive.values()) { - sortTreeNodes(child) - } - } - if (node.dynamic?.length) { - node.dynamic.sort(sortDynamic) - for (const child of node.dynamic) { - sortTreeNodes(child) - } - } - if (node.optional?.length) { - node.optional.sort(sortDynamic) - for (const child of node.optional) { - sortTreeNodes(child) - } - } - if (node.wildcard?.length) { - node.wildcard.sort(sortDynamic) - for (const child of node.wildcard) { - sortTreeNodes(child) - } - } -} - function createStaticNode( fullPath: string, ): StaticSegmentNode { @@ -663,10 +573,13 @@ export function processRouteMasks< ) { const segmentTree = createStaticNode('/') const data = new Uint16Array(6) + const dynamicListsToSort: Array>> = [] for (const route of routeList) { - parseSegments(false, data, route, 1, segmentTree, 0) + parseSegments(false, data, route, 1, segmentTree, 0, dynamicListsToSort) + } + for (const nodes of dynamicListsToSort) { + nodes.sort(sortDynamic) } - sortTreeNodes(segmentTree) processedTree.masksTree = segmentTree processedTree.flatCache = createLRUCache< string, @@ -789,34 +702,46 @@ export function processRouteTree< ): ProcessRouteTreeResult { const segmentTree = createStaticNode(routeTree.fullPath) const data = new Uint16Array(6) + const dynamicListsToSort: Array>> = [] const routesById = {} as Record const routesByPath = {} as Record let index = 0 - parseSegments(caseSensitive, data, routeTree, 1, segmentTree, 0, (route) => { - initRoute?.(route, index) + parseSegments( + caseSensitive, + data, + routeTree, + 1, + segmentTree, + 0, + dynamicListsToSort, + (route) => { + initRoute?.(route, index) + + if (route.id in routesById) { + if (process.env.NODE_ENV !== 'production') { + throw new Error( + `Invariant failed: Duplicate routes found with id: ${String(route.id)}`, + ) + } - if (route.id in routesById) { - if (process.env.NODE_ENV !== 'production') { - throw new Error( - `Invariant failed: Duplicate routes found with id: ${String(route.id)}`, - ) + invariant() } - invariant() - } - - routesById[route.id] = route + routesById[route.id] = route - if (index !== 0 && route.path) { - const trimmedFullPath = trimPathRight(route.fullPath) - if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) { - routesByPath[trimmedFullPath] = route + if (index !== 0 && route.path) { + const trimmedFullPath = trimPathRight(route.fullPath) + if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) { + routesByPath[trimmedFullPath] = route + } } - } - index++ - }) - sortTreeNodes(segmentTree) + index++ + }, + ) + for (const nodes of dynamicListsToSort) { + nodes.sort(sortDynamic) + } const processedTree: ProcessedTree = { segmentTree, singleCache: createLRUCache>(1000), From 468bd90b85c5a95278ffba7878c6502a77fae0a7 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 6 Aug 2026 12:26:51 +0200 Subject: [PATCH 3/4] Delete RESULT-optimization-fused-route-nodes.md --- RESULT-optimization-fused-route-nodes.md | 67 ------------------------ 1 file changed, 67 deletions(-) delete mode 100644 RESULT-optimization-fused-route-nodes.md diff --git a/RESULT-optimization-fused-route-nodes.md b/RESULT-optimization-fused-route-nodes.md deleted file mode 100644 index 0b31e61d6f..0000000000 --- a/RESULT-optimization-fused-route-nodes.md +++ /dev/null @@ -1,67 +0,0 @@ -# Fused dynamic route-node construction - -Baseline: `main` at `697ebb6ddbd433d052b6b4707938a5c595865d58`. - -## Bundle result - -`react-router.minimal`: - -| Metric | Before | After | Change | -| --- | ---: | ---: | ---: | -| gzip | 89,200 B | 89,145 B | -55 B | -| initial gzip | 89,058 B | 89,004 B | -54 B | -| raw | 275,591 B | 274,715 B | -876 B | -| Brotli | 77,742 B | 77,571 B | -171 B | - -All 17 bundle scenarios improved in gzip by 55–125 B and in raw size by -870–904 B. Initial gzip improved by 54–125 B. Brotli changed by -180 to -+83 B; 6 of 17 scenarios had a small Brotli regression despite the raw and -gzip reductions. - -Hunk-level gzip attribution on `react-router.minimal`: - -| Change | Isolated/cumulative change | -| --- | ---: | -| Record only dynamic sibling lists that need sorting | -16 B isolated | -| Cache parsed route fields | -1 B isolated | -| First two changes together | -19 B cumulative | -| Fuse required, optional, and wildcard construction | -36 B incremental | -| Complete group | -55 B cumulative | - -Compression is nonlinear, so the isolated figures do not add exactly. - -## Construction benchmark - -Each value is the median mean time, in milliseconds, from three runs. Every -sample builds the tree ten times. - -| Cumulative stage | Mostly static | Dense dynamic | Reused dynamic shape | Route masks | -| --- | ---: | ---: | ---: | ---: | -| Baseline | 0.8106 | 0.6019 | 0.8167 | 0.0245 | -| Sparse sorting | 0.7658 | 0.5813 | 0.8007 | 0.0224 | -| Cached route fields | 0.7607 | 0.5686 | 0.7964 | 0.0222 | -| Fused construction | 0.7853 | 0.5785 | 0.8005 | 0.0224 | - -The complete group was approximately 3.1%, 3.9%, 2.0%, and 8.6% faster than -baseline across those workloads. The fusion hunk alone moved the cumulative -median by +0.5% to +3.2%, so it should be treated as a size optimization, not -as an independent runtime-performance improvement. - -## Correctness coverage - -- Required, optional, and wildcard parser priorities are sorted only after - parser metadata is assigned, including parser rejection and fallback. -- Same-shape required and optional nodes without parsers remain reusable. -- Same-shape wildcard aliases remain separate match candidates. -- Route-mask dynamic, optional, and wildcard sibling lists are sorted by - specificity and can be run independently. -- Construction benchmarks validate representative matches and the sorted - sibling arrays before collecting timing samples. - -Validation passed: - -- Router-core unit tests: 1,529 passed and 3 expected failures. -- Router-core type tests across all configured TypeScript versions. -- Router-core ESLint (no errors; existing warnings remain). -- React Router generator CLI end-to-end suite: 3 passed. -- Full 17-scenario bundle-size matrix. From 01aaec54099b6344852d067863cf4a3af8c05389 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 6 Aug 2026 12:27:11 +0200 Subject: [PATCH 4/4] Delete packages/router-core/tests/route-tree-construction.bench.ts --- .../tests/route-tree-construction.bench.ts | 178 ------------------ 1 file changed, 178 deletions(-) delete mode 100644 packages/router-core/tests/route-tree-construction.bench.ts diff --git a/packages/router-core/tests/route-tree-construction.bench.ts b/packages/router-core/tests/route-tree-construction.bench.ts deleted file mode 100644 index 39f86b8088..0000000000 --- a/packages/router-core/tests/route-tree-construction.bench.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { bench, describe, expect } from 'vitest' -import { - findFlatMatch, - findRouteMatch, - processRouteMasks, - processRouteTree, -} from '../src/new-process-route-tree' - -type BenchRoute = { - id: string - fullPath: string - path?: string - isRoot?: boolean - children?: Array - options?: { - caseSensitive?: boolean - params?: { - parse?: (params: Record) => unknown - priority?: number - } - } -} - -const mostlyStaticTree: BenchRoute = { - id: '__root__', - isRoot: true, - fullPath: '/', - path: '/', - children: Array.from({ length: 256 }, (_, index) => ({ - id: `/section-${index % 16}/item-${index}`, - fullPath: `/section-${index % 16}/item-${index}`, - path: `section-${index % 16}/item-${index}`, - })), -} - -const dynamicPatterns = [ - '$value', - 'pre{$value}', - '{$value}suf', - 'pre{$value}suf', - '{-$value}', - 'pre{-$value}', - '{-$value}suf', - 'pre{-$value}suf', - '$', - 'pre{$}', - '{$}suf', - 'pre{$}suf', -] - -const denseDynamicTree: BenchRoute = { - id: '__root__', - isRoot: true, - fullPath: '/', - path: '/', - children: Array.from({ length: 16 }, (_, group) => - dynamicPatterns.map((pattern, index) => ({ - id: `/group-${group}/${pattern}`, - fullPath: `/group-${group}/${pattern}`, - path: `group-${group}/${pattern}`, - options: { - params: - index % 3 === 0 - ? { - parse: (params: Record) => params, - priority: index % 4, - } - : undefined, - }, - })), - ).flat(), -} - -const reusedDynamicTree: BenchRoute = { - id: '__root__', - isRoot: true, - fullPath: '/', - path: '/', - children: Array.from({ length: 256 }, (_, index) => ({ - id: `/shared/$value/item-${index}`, - fullPath: `/shared/$value/item-${index}`, - path: `shared/$value/item-${index}`, - })), -} - -const maskBase = processRouteTree({ - id: '__root__', - isRoot: true, - fullPath: '/', -}).processedTree -const routeMasks = dynamicPatterns.map((pattern) => ({ - from: `/group/${pattern}`, - routeTree: denseDynamicTree, -})) - -const staticResult = processRouteTree(mostlyStaticTree) -expect( - findRouteMatch('/section-3/item-99', staticResult.processedTree)?.route.id, -).toBe('/section-3/item-99') - -const dynamicResult = processRouteTree(denseDynamicTree) -expect( - findRouteMatch('/group-0/prexsuf', dynamicResult.processedTree)?.route.id, -).toBe('/group-0/pre{$value}suf') -const denseBranch = - dynamicResult.processedTree.segmentTree.staticInsensitive?.get('group-0') -expect(denseBranch?.dynamic?.map((node) => node.fullPath)).toEqual([ - '/group-0/pre{$value}suf', - '/group-0/$value', - '/group-0/pre{$value}', - '/group-0/{$value}suf', -]) -expect(denseBranch?.optional?.map((node) => node.fullPath)).toEqual([ - '/group-0/{-$value}suf', - '/group-0/pre{-$value}suf', - '/group-0/pre{-$value}', - '/group-0/{-$value}', -]) -expect(denseBranch?.wildcard?.map((node) => node.fullPath)).toEqual([ - '/group-0/pre{$}', - '/group-0/pre{$}suf', - '/group-0/{$}suf', - '/group-0/$', -]) - -const reusedResult = processRouteTree(reusedDynamicTree) -expect( - reusedResult.processedTree.segmentTree.staticInsensitive?.get('shared') - ?.dynamic, -).toHaveLength(1) - -processRouteMasks(routeMasks, maskBase) -expect(findFlatMatch('/group/prexsuf', maskBase)?.route.from).toBe( - '/group/pre{$value}suf', -) - -let benchmarkSink = 0 - -describe('route tree construction', () => { - bench('build 10 mostly static route trees', () => { - for (let i = 0; i < 10; i++) { - benchmarkSink += - processRouteTree(mostlyStaticTree).processedTree.segmentTree - .staticInsensitive?.size ?? 0 - } - }) - - bench('build 10 dense dynamic route trees', () => { - for (let i = 0; i < 10; i++) { - benchmarkSink += - processRouteTree(denseDynamicTree).processedTree.segmentTree - .staticInsensitive?.size ?? 0 - } - }) - - bench('build 10 same-shape dynamic route trees', () => { - for (let i = 0; i < 10; i++) { - benchmarkSink += - processRouteTree( - reusedDynamicTree, - ).processedTree.segmentTree.staticInsensitive?.get('shared')?.dynamic - ?.length ?? 0 - } - }) - - bench('build 10 dense route-mask trees', () => { - for (let i = 0; i < 10; i++) { - processRouteMasks(routeMasks, maskBase) - const group = maskBase.masksTree?.staticInsensitive?.get('group') - benchmarkSink += - (group?.dynamic?.length ?? 0) + - (group?.optional?.length ?? 0) + - (group?.wildcard?.length ?? 0) - } - }) -}) - -void benchmarkSink