From 6175fe25dc93622181b6c083b1a497a01aa4146c Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 15 Nov 2025 14:42:49 +0100 Subject: [PATCH] refactor(router-core): interpolatePath can cache the parsed path template --- .../router-core/src/new-process-route-tree.ts | 2 +- packages/router-core/src/path.ts | 76 +++++++++++++------ packages/router-core/src/router.ts | 5 ++ 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/packages/router-core/src/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts index e10be41c84..78e49b81ec 100644 --- a/packages/router-core/src/new-process-route-tree.ts +++ b/packages/router-core/src/new-process-route-tree.ts @@ -20,7 +20,7 @@ const OPTIONAL_PARAM_W_CURLY_BRACES_RE = /^([^{]*)\{-\$([a-zA-Z_$][a-zA-Z0-9_$]*)\}([^}]*)$/ // prefix{-$paramName}suffix const WILDCARD_W_CURLY_BRACES_RE = /^([^{]*)\{\$\}([^}]*)$/ // prefix{$}suffix -type ParsedSegment = Uint16Array & { +export type ParsedSegment = Uint16Array & { /** segment type (0 = pathname, 1 = param, 2 = wildcard, 3 = optional param) */ 0: SegmentKind /** index of the end of the prefix */ diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index 88b489d8aa..42f73dc504 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -6,6 +6,7 @@ import { SEGMENT_TYPE_WILDCARD, parseSegment, } from './new-process-route-tree' +import type { ParsedSegment } from './new-process-route-tree' import type { LRUCache } from './lru-cache' /** Join path segments, cleaning duplicate slashes between parts. */ @@ -210,6 +211,7 @@ interface InterpolatePathOptions { params: Record // Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params decodeCharMap?: Map + cache?: LRUCache } type InterPolatePathResult = { @@ -234,6 +236,41 @@ function encodeParam( } } +const SEGMENT_DATA_LENGTH = 6 + +function forEachSegment( + path: string, + cache: LRUCache | undefined, + cb: (start: number, end: number, data: ParsedSegment) => void, +) { + const cached = cache?.get(path) + let cursor = 0 + const length = path.length + const all = !cached ? Array() : null + let i = 0 + while (cursor < length) { + const start = cursor + let data + if (cached) { + data = cached.subarray(i, i + SEGMENT_DATA_LENGTH) as ParsedSegment + i += SEGMENT_DATA_LENGTH + } else { + data = parseSegment(path, start) + all!.push(data) + } + const end = data[5] + cursor = end + 1 + if (start === end) continue + cb(start, end, data) + } + if (!all) return + const next = new Uint16Array(all.length * SEGMENT_DATA_LENGTH) + for (let i = 0; i < all.length; i++) { + next.set(all[i]!, i * SEGMENT_DATA_LENGTH) + } + cache?.set(path, next) +} + /** * Interpolate params and wildcards into a route path template. * @@ -248,6 +285,7 @@ export function interpolatePath({ path, params, decodeCharMap, + cache, }: InterpolatePathOptions): InterPolatePathResult { // Tracking if any params are missing in the `params` object // when interpolating the path @@ -259,23 +297,13 @@ export function interpolatePath({ if (!path.includes('$')) return { interpolatedPath: path, usedParams, isMissingParams } - const length = path.length - let cursor = 0 - let segment let joined = '' - while (cursor < length) { - const start = cursor - segment = parseSegment(path, start, segment) - const end = segment[5] - cursor = end + 1 - - if (start === end) continue - + forEachSegment(path, cache, (start, end, segment) => { const kind = segment[0] if (kind === SEGMENT_TYPE_PATHNAME) { joined += '/' + path.substring(start, end) - continue + return } if (kind === SEGMENT_TYPE_WILDCARD) { @@ -295,51 +323,51 @@ export function interpolatePath({ if (prefix || suffix) { joined += '/' + prefix + suffix } - continue + return } const value = encodeParam('_splat', params, decodeCharMap) joined += '/' + prefix + value + suffix - continue + return } if (kind === SEGMENT_TYPE_PARAM) { + const prefix = path.substring(start, segment[1]) const key = path.substring(segment[2], segment[3]) + const suffix = path.substring(segment[4], end) if (!isMissingParams && !(key in params)) { isMissingParams = true } usedParams[key] = params[key] - const prefix = path.substring(start, segment[1]) - const suffix = path.substring(segment[4], end) const value = encodeParam(key, params, decodeCharMap) ?? 'undefined' joined += '/' + prefix + value + suffix - continue + return } if (kind === SEGMENT_TYPE_OPTIONAL_PARAM) { - const key = path.substring(segment[2], segment[3]) const prefix = path.substring(start, segment[1]) + const key = path.substring(segment[2], segment[3]) const suffix = path.substring(segment[4], end) - const valueRaw = params[key] + const rawValue = params[key] // Check if optional parameter is missing or undefined - if (valueRaw == null) { + if (rawValue == null) { if (prefix || suffix) { // For optional params with prefix/suffix, keep the prefix/suffix but omit the param joined += '/' + prefix + suffix } // If no prefix/suffix, omit the entire segment - continue + return } - usedParams[key] = valueRaw + usedParams[key] = rawValue const value = encodeParam(key, params, decodeCharMap) ?? '' joined += '/' + prefix + value + suffix - continue + return } - } + }) if (path.endsWith('/')) joined += '/' diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index e0f6246eab..d596be695d 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1244,6 +1244,8 @@ export class RouterCore< return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts) } + private interpolateCache = createLRUCache(1000) + private matchRoutesInternal( next: ParsedLocation, opts?: MatchRoutesOpts, @@ -1363,6 +1365,7 @@ export class RouterCore< path: route.fullPath, params: routeParams, decodeCharMap: this.pathParamsDecodeCharMap, + cache: this.interpolateCache, }) // Waste not, want not. If we already have a match for this route, @@ -1660,6 +1663,7 @@ export class RouterCore< const interpolatedNextTo = interpolatePath({ path: nextTo, params: nextParams, + cache: this.interpolateCache, }).interpolatedPath const destRoutes = this.matchRoutes(interpolatedNextTo, undefined, { @@ -1686,6 +1690,7 @@ export class RouterCore< path: nextTo, params: nextParams, decodeCharMap: this.pathParamsDecodeCharMap, + cache: this.interpolateCache, }).interpolatedPath, )