From 78eca7fe5ee8960da794e4d022baa09e1d0a8eb5 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Sun, 16 Aug 2026 18:53:33 +0000 Subject: [PATCH] perf(router-core): dehydrate SSR match IDs in one pass Typical route ids only contain slashes. Scan once, intern the result with a bounded cache, and skip hydrate work when the payload has no encoded characters. --- packages/router-core/src/ssr/ssr-match-id.ts | 53 +++++++++++++++++-- .../router-core/tests/ssr-match-id.test.ts | 8 +++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/router-core/src/ssr/ssr-match-id.ts b/packages/router-core/src/ssr/ssr-match-id.ts index 6c7e2e9eafc..d95488c8e57 100644 --- a/packages/router-core/src/ssr/ssr-match-id.ts +++ b/packages/router-core/src/ssr/ssr-match-id.ts @@ -1,12 +1,55 @@ +const dehydrateCache = new Map() +const DEHYDRATE_CACHE_MAX = 256 +const DEHYDRATE_CACHE_MAX_ID_LENGTH = 4096 + export function dehydrateSsrMatchId(id: string): string { - return id - .replaceAll('~', '~~') - .replaceAll('\0', '~0') - .replaceAll('\uFFFD', '~r') - .replaceAll('/', '\0') + const cached = dehydrateCache.get(id) + if (cached !== undefined) { + return cached + } + + const result = dehydrateSsrMatchIdUncached(id) + if (id.length > DEHYDRATE_CACHE_MAX_ID_LENGTH) { + return result + } + if (dehydrateCache.size >= DEHYDRATE_CACHE_MAX) { + dehydrateCache.delete(dehydrateCache.keys().next().value!) + } + dehydrateCache.set(id, result) + return result +} + +function dehydrateSsrMatchIdUncached(id: string): string { + const len = id.length + let last = 0 + let out = '' + for (let i = 0; i < len; i++) { + const c = id.charCodeAt(i) + if (c === 126) { + out += id.slice(last, i) + '~~' + last = i + 1 + } else if (c === 0) { + out += id.slice(last, i) + '~0' + last = i + 1 + } else if (c === 0xfffd) { + out += id.slice(last, i) + '~r' + last = i + 1 + } else if (c === 47) { + out += id.slice(last, i) + '\0' + last = i + 1 + } + } + return last === 0 ? id : out + id.slice(last) } export function hydrateSsrMatchId(id: string): string { + if ( + id.indexOf('\0') === -1 && + id.indexOf('\uFFFD') === -1 && + id.indexOf('~') === -1 + ) { + return id + } return id .replaceAll('\0', '/') .replaceAll('\uFFFD', '/') diff --git a/packages/router-core/tests/ssr-match-id.test.ts b/packages/router-core/tests/ssr-match-id.test.ts index 65e1cc4ebbd..f5a3747a046 100644 --- a/packages/router-core/tests/ssr-match-id.test.ts +++ b/packages/router-core/tests/ssr-match-id.test.ts @@ -39,4 +39,12 @@ describe('ssr match id codec', () => { expect(hydrateSsrMatchId(normalized)).toBe('/posts/1') }) + + it('dehydrates ids longer than the cache length limit', () => { + const id = `/${'a'.repeat(5000)}/posts` + const dehydratedId = dehydrateSsrMatchId(id) + + expect(dehydratedId).not.toContain('/') + expect(hydrateSsrMatchId(dehydratedId)).toBe(id) + }) })