From 7edd7ea841dafda71dd2e6eef348c8194a70ec90 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Thu, 10 Sep 2026 19:39:08 -0700 Subject: [PATCH] perf(history): avoid temporary arrays in hash parsing --- .changeset/history-hash-allocations.md | 5 ++ packages/history/src/index.ts | 16 +++-- .../history/tests/createHashHistory.test.ts | 72 +++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 .changeset/history-hash-allocations.md diff --git a/.changeset/history-hash-allocations.md b/.changeset/history-hash-allocations.md new file mode 100644 index 00000000000..ba8755bb97e --- /dev/null +++ b/.changeset/history-hash-allocations.md @@ -0,0 +1,5 @@ +--- +'@tanstack/history': patch +--- + +Avoid temporary arrays when reading hash-history locations. diff --git a/packages/history/src/index.ts b/packages/history/src/index.ts index b0e006c80da..6c80f201ca4 100644 --- a/packages/history/src/index.ts +++ b/packages/history/src/index.ts @@ -616,13 +616,15 @@ export function createHashHistory(opts?: { window?: any }): RouterHistory { return createBrowserHistory({ window: win, parseLocation: () => { - const hashSplit = win.location.hash.split('#').slice(1) - const pathPart = hashSplit[0] ?? '/' - const searchPart = win.location.search - const hashEntries = hashSplit.slice(1) - const hashPart = - hashEntries.length === 0 ? '' : `#${hashEntries.join('#')}` - const hashHref = `${pathPart}${searchPart}${hashPart}` + const hash = win.location.hash + const pathStart = hash.indexOf('#') + 1 + const hashIndex = hash.indexOf('#', pathStart) + const pathPart = + pathStart === 0 + ? '/' + : hash.slice(pathStart, hashIndex === -1 ? undefined : hashIndex) + const hashPart = hashIndex === -1 ? '' : hash.slice(hashIndex) + const hashHref = `${pathPart}${win.location.search}${hashPart}` return parseHref(hashHref, win.history.state) }, createHref: (href) => diff --git a/packages/history/tests/createHashHistory.test.ts b/packages/history/tests/createHashHistory.test.ts index 621cefe1568..da3b4d68a1c 100644 --- a/packages/history/tests/createHashHistory.test.ts +++ b/packages/history/tests/createHashHistory.test.ts @@ -59,6 +59,78 @@ describe('createHashHistory', () => { history.destroy() }) + test.each([ + [ + '/?shell=1#/hello#section#tail', + { + href: '/hello?shell=1#section#tail', + pathname: '/hello', + search: '?shell=1', + hash: '#section#tail', + }, + ], + [ + '/?shell=1#/hello?route=2#section', + { + href: '/hello?route=2?shell=1#section', + pathname: '/hello', + search: '?route=2?shell=1', + hash: '#section', + }, + ], + [ + '/#/hello##tail#', + { + href: '/hello##tail#', + pathname: '/hello', + search: '', + hash: '##tail#', + }, + ], + [ + '/#/hello%23nested?value=%23#anchor%23tail', + { + href: '/hello%23nested?value=%23#anchor%23tail', + pathname: '/hello%23nested', + search: '?value=%23', + hash: '#anchor%23tail', + }, + ], + [ + '/?shell=1#//evil.example/path#fragment', + { + href: '/evil.example/path?shell=1#fragment', + pathname: '/evil.example/path', + search: '?shell=1', + hash: '#fragment', + }, + ], + [ + '/?shell=1#', + { + href: '/?shell=1', + pathname: '/', + search: '?shell=1', + hash: '', + }, + ], + ])('preserves the logical URL when reading %s', (href, expected) => { + const originalHref = window.location.href + const originalState = window.history.state + window.history.replaceState(null, '', href) + const history = createHashHistory() + + try { + expect(history.location).toMatchObject(expected) + window.history.replaceState(window.history.state, '', '/shell') + window.history.pushState(window.history.state, '', href) + expect(history.location).toMatchObject(expected) + } finally { + history.destroy() + window.history.replaceState(originalState, '', originalHref) + } + }) + describe('parseLocation', () => { describe.each([ ['/', { pathname: '/', search: '' }, 'neither search params nor hash'],