From 39678f35dc5dfb3f9cea9ebb3518fd26366958a0 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:43:27 +0200 Subject: [PATCH 01/13] perf(history): compact queued action kind --- packages/history/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/history/src/index.ts b/packages/history/src/index.ts index 0f3be8242e7..add4ccdbdb6 100644 --- a/packages/history/src/index.ts +++ b/packages/history/src/index.ts @@ -369,7 +369,7 @@ export function createBrowserHistory(opts?: { // This function queues up a call to update the browser history const queueHistoryAction = ( - type: 'push' | 'replace', + isPush: boolean, destHref: string, state: any, ) => { @@ -386,7 +386,7 @@ export function createBrowserHistory(opts?: { next = { href, state, - isPush: next?.isPush || type === 'push', + isPush: next?.isPush || isPush, } if (!scheduled) { @@ -489,8 +489,8 @@ export function createBrowserHistory(opts?: { const history = createHistory({ getLocation, getLength: () => win.history.length, - pushState: (href, state) => queueHistoryAction('push', href, state), - replaceState: (href, state) => queueHistoryAction('replace', href, state), + pushState: (href, state) => queueHistoryAction(true, href, state), + replaceState: (href, state) => queueHistoryAction(false, href, state), back: (ignoreBlocker) => { if (ignoreBlocker) skipBlockerNextPop = true ignoreNextBeforeUnload = true From e8a4cc7c5019b4e3286edaf4354db67689a1157a Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:43:40 +0200 Subject: [PATCH 02/13] perf(history): compact queued navigation state --- packages/history/src/index.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/history/src/index.ts b/packages/history/src/index.ts index add4ccdbdb6..c2695288466 100644 --- a/packages/history/src/index.ts +++ b/packages/history/src/index.ts @@ -329,14 +329,14 @@ export function createBrowserHistory(opts?: { let next: | undefined - | { - // This is the latest location that we were attempting to push/replace - href: string - // This is the latest state that we were attempting to push/replace - state: any - // This is the latest type that we were attempting to push/replace - isPush: boolean - } + | [ + // The latest location that we were attempting to push/replace + href: string, + // The latest state that we were attempting to push/replace + state: any, + // Whether any queued update needs to push rather than replace + isPush: boolean, + ] // We need to track the current scheduled update to prevent // multiple updates from being scheduled at the same time. @@ -352,10 +352,10 @@ export function createBrowserHistory(opts?: { history._ignoreSubscribers = true // Update the browser history - ;(next.isPush ? win.history.pushState : win.history.replaceState)( - next.state, + ;(next[2 /* is push */] ? win.history.pushState : win.history.replaceState)( + next[1 /* state */], '', - next.href, + next[0 /* href */], ) // Stop ignoring subscriber updates @@ -383,11 +383,7 @@ export function createBrowserHistory(opts?: { currentLocation = parseHref(destHref, state) // Keep track of the next location we need to flush to the URL - next = { - href, - state, - isPush: next?.isPush || isPush, - } + next = [href, state, next?.[2 /* is push */] || isPush] if (!scheduled) { // Schedule an update to the browser history From a3bd30dbdadc680bca835298ffd9b1dc2afb5acd Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:48:48 +0200 Subject: [PATCH 03/13] perf(history): derive queued action state --- packages/history/src/index.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/history/src/index.ts b/packages/history/src/index.ts index c2695288466..063174c7328 100644 --- a/packages/history/src/index.ts +++ b/packages/history/src/index.ts @@ -338,10 +338,6 @@ export function createBrowserHistory(opts?: { isPush: boolean, ] - // We need to track the current scheduled update to prevent - // multiple updates from being scheduled at the same time. - let scheduled: undefined | boolean - // This function flushes the next update to the browser history const flush = () => { if (!next) { @@ -361,9 +357,8 @@ export function createBrowserHistory(opts?: { // Stop ignoring subscriber updates history._ignoreSubscribers = false - // Reset the nextIsPush flag and clear the scheduled update + // Clear the queued action after it reaches browser history. next = undefined - scheduled = false rollbackLocation = undefined } @@ -374,8 +369,9 @@ export function createBrowserHistory(opts?: { state: any, ) => { const href = createHref(destHref) + const hasPendingAction = !!next - if (!scheduled) { + if (!hasPendingAction) { rollbackLocation = currentLocation } @@ -385,9 +381,8 @@ export function createBrowserHistory(opts?: { // Keep track of the next location we need to flush to the URL next = [href, state, next?.[2 /* is push */] || isPush] - if (!scheduled) { + if (!hasPendingAction) { // Schedule an update to the browser history - scheduled = true queueMicrotask(() => flush()) } } From afcc46cd35a1da25832c0f783cda93316542db27 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 00:59:40 +0200 Subject: [PATCH 04/13] perf(router-core): tuple lightweight route matches --- packages/router-core/src/router.ts | 37 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 74b6f015a8f..cc83e358de2 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -963,12 +963,12 @@ export function runRouteLifecycle( } } -type LightweightRouteMatchResult = { - matchedRoutes: ReadonlyArray - fullPath: string - search: Record - params: Record -} +type LightweightRouteMatchResult = [ + matchedRoutes: ReadonlyArray, + fullPath: string, + search: Record, + params: Record, +] type LightweightRouteMatchCacheEntry = [ lastMatchId: string | undefined, @@ -1822,12 +1822,12 @@ export class RouterCore< params = strictParams } - const result = { + const result: LightweightRouteMatchResult = [ matchedRoutes, - fullPath: lastRoute.fullPath, - search: accumulatedSearch, + lastRoute.fullPath, + accumulatedSearch, params, - } + ] this.lightweightCache.set(location, [lastStateMatchId, result]) return result } @@ -1862,12 +1862,15 @@ export class RouterCore< ) { const [allFromMatches] = this.getMatchedRoutes(dest.from) - const matchedFrom = findLast(lightweightResult.matchedRoutes, (d) => { - return comparePaths(d.fullPath, dest.from!) - }) + const matchedFrom = findLast( + lightweightResult[0 /* matchedRoutes */], + (d) => { + return comparePaths(d.fullPath, dest.from!) + }, + ) const matchedCurrent = findLast(allFromMatches, (d) => { - return comparePaths(d.fullPath, lightweightResult.fullPath) + return comparePaths(d.fullPath, lightweightResult[1 /* fullPath */]) }) // for from to be invalid it shouldn't just be unmatched to currentLocation @@ -1880,15 +1883,15 @@ export class RouterCore< const defaultedFromPath = dest.unsafeRelative === 'path' ? currentLocation.pathname - : (dest.from ?? lightweightResult.fullPath) + : (dest.from ?? lightweightResult[1 /* fullPath */]) const destTo = dest.to ? `${dest.to}` : undefined // From search should always use the current location - const fromSearch = lightweightResult.search + const fromSearch = lightweightResult[2 /* search */] // Same with params. It can't hurt to provide as many as possible const fromParams = Object.assign( Object.create(null), - lightweightResult.params, + lightweightResult[3 /* params */], ) const isAbsoluteTo = destTo?.charCodeAt(0) === 47 From 390f9d8e3a7f36aeaa93aeee9436191afaa98ebf Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:45:01 +0200 Subject: [PATCH 05/13] perf(solid-router): compact nearest match context --- packages/solid-router/src/Match.tsx | 13 +++++-------- packages/solid-router/src/Matches.tsx | 9 +++------ packages/solid-router/src/matchContext.tsx | 16 ++++++++-------- packages/solid-router/src/useMatch.tsx | 2 +- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/solid-router/src/Match.tsx b/packages/solid-router/src/Match.tsx index 8a400a85804..16038453c03 100644 --- a/packages/solid-router/src/Match.tsx +++ b/packages/solid-router/src/Match.tsx @@ -34,10 +34,7 @@ export const Match = (props: { routeId: string }) => { () => router.stores.byRoute.get(props.routeId)!.get()!, ) - const nearestMatch = { - routeId: () => props.routeId, - match: currentMatch, - } + const nearestMatch = [() => props.routeId, currentMatch] as const const route: AnyRoute = router.routesById[props.routeId] @@ -162,8 +159,8 @@ export const Match = (props: { routeId: string }) => { export const MatchInner = (): any => { const router = useRouter() const nearestMatch = Solid.useContext(nearestMatchContext) - const match = nearestMatch.match - const routeId = nearestMatch.routeId + const match = nearestMatch[1 /* match */] + const routeId = nearestMatch[0 /* route id */] const route = router.routesById[routeId()!]! const currentMatch = () => match()! @@ -230,8 +227,8 @@ export const MatchInner = (): any => { export const Outlet = () => { const router = useRouter() const nearestParentMatch = Solid.useContext(nearestMatchContext) - const parentMatch = nearestParentMatch.match - const routeId = nearestParentMatch.routeId + const parentMatch = nearestParentMatch[1 /* match */] + const routeId = nearestParentMatch[0 /* route id */] const route = router.routesById[routeId()!]! const childRouteId = () => { diff --git a/packages/solid-router/src/Matches.tsx b/packages/solid-router/src/Matches.tsx index ff270ecbd91..704e5026f4c 100644 --- a/packages/solid-router/src/Matches.tsx +++ b/packages/solid-router/src/Matches.tsx @@ -66,10 +66,7 @@ function MatchesInner() { const routeId = () => router.stores.ids.get()[0] const match = () => routeId() ? router.stores.byRoute.get(routeId()!)?.get() : undefined - const nearestMatch = { - routeId, - match, - } + const nearestMatch = [routeId, match] as const const matchComponent = () => { return ( @@ -219,7 +216,7 @@ export function useParentMatches< >( opts?: UseMatchesBaseOptions, ): Solid.Accessor> { - const contextRouteId = Solid.useContext(nearestMatchContext).routeId + const contextRouteId = Solid.useContext(nearestMatchContext)[0 /* route id */] return useMatches({ select: (matches: Array>) => { @@ -238,7 +235,7 @@ export function useChildMatches< >( opts?: UseMatchesBaseOptions, ): Solid.Accessor> { - const contextRouteId = Solid.useContext(nearestMatchContext).routeId + const contextRouteId = Solid.useContext(nearestMatchContext)[0 /* route id */] return useMatches({ select: (matches: Array>) => { diff --git a/packages/solid-router/src/matchContext.tsx b/packages/solid-router/src/matchContext.tsx index 50ab0d4b767..87cd32f4a83 100644 --- a/packages/solid-router/src/matchContext.tsx +++ b/packages/solid-router/src/matchContext.tsx @@ -1,15 +1,15 @@ import * as Solid from 'solid-js' import type { AnyRouteMatch } from '@tanstack/router-core' -export type NearestMatchContextValue = { - routeId: Solid.Accessor - match: Solid.Accessor -} +export type NearestMatchContextValue = readonly [ + routeId: Solid.Accessor, + match: Solid.Accessor, +] -const defaultNearestMatchContext: NearestMatchContextValue = { - routeId: () => undefined, - match: () => undefined, -} +const defaultNearestMatchContext: NearestMatchContextValue = [ + () => undefined, + () => undefined, +] export const nearestMatchContext = Solid.createContext(defaultNearestMatchContext) diff --git a/packages/solid-router/src/useMatch.tsx b/packages/solid-router/src/useMatch.tsx index 97880329dbf..dc3d6ff37d5 100644 --- a/packages/solid-router/src/useMatch.tsx +++ b/packages/solid-router/src/useMatch.tsx @@ -78,7 +78,7 @@ export function useMatch< return router.stores.getMatchStore(opts.from).get() } - return nearestMatch?.match() + return nearestMatch?.[1 /* match */]() } Solid.createEffect(() => { From 392893c70feaad914ffaede5e1700ca3c870a2bc Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:25:37 +0200 Subject: [PATCH 06/13] perf(vue-router): reuse the computed scripts tuple --- packages/vue-router/src/Scripts.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/vue-router/src/Scripts.tsx b/packages/vue-router/src/Scripts.tsx index 8c0a1f87f3a..4c0d8feed06 100644 --- a/packages/vue-router/src/Scripts.tsx +++ b/packages/vue-router/src/Scripts.tsx @@ -6,13 +6,6 @@ import { Asset } from './Asset' import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' -type ScriptsRenderState = { - scripts: Array - assetScripts: Array - mounted: boolean - nonce?: string -} - export const Scripts = Vue.defineComponent({ name: 'Scripts', setup() { @@ -53,20 +46,19 @@ export const Scripts = Vue.defineComponent({ }) return () => { - const [userScripts, assetScripts] = scripts.value - return renderScripts(router, { - scripts: userScripts, - assetScripts, - mounted: mounted.value, - nonce, - }) + return renderScripts(router, scripts.value, mounted.value, nonce) } }, }) function renderScripts( router: ReturnType, - { scripts, assetScripts, mounted, nonce }: ScriptsRenderState, + [scripts, assetScripts]: readonly [ + Array, + Array, + ], + mounted: boolean, + nonce?: string, ) { const allScripts: Array = [] From 5d3c3af7d44c36e3543e9709a228c48495db0a0b Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:46:45 +0200 Subject: [PATCH 07/13] perf(vue-router): flatten private href input --- packages/vue-router/src/link.tsx | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 3a5a9875a82..a9a7016504e 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -180,11 +180,7 @@ export function useLinkProps< // Avoid store subscriptions, effects and observers on the server. if (isServer ?? router.isServer) { const next = router.buildLocation(options as any) - const href = getHref({ - options: options as AnyLinkPropsOptions, - router, - nextLocation: next, - }) + const href = getHref(options as AnyLinkPropsOptions, router, next) const isActive = getIsActive({ loc: router.stores.location.get(), @@ -385,11 +381,7 @@ export function useLinkProps< ) const href = Vue.computed(() => - getHref({ - options: options as AnyLinkPropsOptions, - router, - nextLocation: next.value, - }), + getHref(options as AnyLinkPropsOptions, router, next.value), ) // Create static event handlers that don't change between renders @@ -702,15 +694,11 @@ function getIsActive({ return true } -function getHref({ - options, - router, - nextLocation, -}: { - options: AnyLinkPropsOptions - router: AnyRouter - nextLocation?: ParsedLocation -}) { +function getHref( + options: AnyLinkPropsOptions, + router: AnyRouter, + nextLocation?: ParsedLocation, +) { if (options.disabled) { return undefined } From 9fbeb274d5f94fa856ca97835336fa7ec957293b Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:47:09 +0200 Subject: [PATCH 08/13] perf(vue-router): flatten private active-state input --- packages/vue-router/src/link.tsx | 37 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index a9a7016504e..135add7fdc9 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -182,12 +182,12 @@ export function useLinkProps< const next = router.buildLocation(options as any) const href = getHref(options as AnyLinkPropsOptions, router, next) - const isActive = getIsActive({ - loc: router.stores.location.get(), - nextLoc: next, - activeOptions: options.activeOptions, + const isActive = getIsActive( + router.stores.location.get(), + next, + options.activeOptions, router, - }) + ) const { resolvedActiveProps, @@ -238,12 +238,12 @@ export function useLinkProps< ) const isActive = Vue.computed(() => - getIsActive({ - activeOptions: options.activeOptions, - loc: currentLocation.value, - nextLoc: next.value, + getIsActive( + currentLocation.value, + next.value, + options.activeOptions, router, - }), + ), ) const doPreload = () => @@ -637,25 +637,20 @@ const getPropsSafeToSpread = (options: AnyLinkPropsOptions) => { return propsSafeToSpread } -function getIsActive({ - activeOptions, - loc, - nextLoc, - router, -}: { - activeOptions: LinkOptions['activeOptions'] +function getIsActive( loc: { pathname: string search: any hash: string - } + }, nextLoc: { pathname: string search: any hash: string - } - router: AnyRouter -}) { + }, + activeOptions: LinkOptions['activeOptions'], + router: AnyRouter, +) { if (activeOptions?.exact) { const testExact = exactPathTest( loc.pathname, From f4f8b839c89f944723cffc30de0644be26507534 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:47:21 +0200 Subject: [PATCH 09/13] perf(vue-router): flatten private style input --- packages/vue-router/src/link.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 135add7fdc9..fb0e4413b32 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -194,10 +194,7 @@ export function useLinkProps< resolvedInactiveProps, resolvedClassName, resolvedStyle, - } = resolveStyleProps({ - options: options as AnyLinkPropsOptions, - isActive, - }) + } = resolveStyleProps(options as AnyLinkPropsOptions, isActive) const result = combineResultProps({ href, @@ -374,10 +371,7 @@ export function useLinkProps< // Get the active and inactive props const resolvedStyleProps = Vue.computed(() => - resolveStyleProps({ - options: options as AnyLinkPropsOptions, - isActive: isActive.value, - }), + resolveStyleProps(options as AnyLinkPropsOptions, isActive.value), ) const href = Vue.computed(() => @@ -441,13 +435,7 @@ export function useLinkProps< return computedProps as unknown as LinkHTMLAttributes } -function resolveStyleProps({ - options, - isActive, -}: { - options: AnyLinkPropsOptions - isActive: boolean -}) { +function resolveStyleProps(options: AnyLinkPropsOptions, isActive: boolean) { const activeProps = options.activeProps || (() => ({ class: 'active' })) const resolvedActiveProps: StyledProps = (isActive ? typeof activeProps === 'function' From 1c457fc281cb9ca2ba0ada75c30ebb596c3b6b75 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 02:26:33 +0200 Subject: [PATCH 10/13] perf(start): return fetch bodies without an internal wrapper --- .../src/client-rpc/serverFnFetcher.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts index 2e993205bcf..5c467bcdf09 100644 --- a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts +++ b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts @@ -157,11 +157,10 @@ export async function serverFnFetcher( let body = undefined if (first.method === 'POST') { - const fetchBody = await getFetchBody(first) - if (fetchBody?.contentType) { - headers.set('content-type', fetchBody.contentType) + body = await getFetchBody(first) + if (typeof body === 'string') { + headers.set('content-type', 'application/json') } - body = fetchBody?.body } return await getResponse(async () => @@ -204,7 +203,7 @@ async function serialize(data: any) { async function getFetchBody( opts: FunctionMiddlewareClientFnOptions, -): Promise<{ body: FormData | string; contentType?: string } | undefined> { +): Promise { if (opts.data instanceof FormData) { let serializedContext = undefined // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -214,11 +213,11 @@ async function getFetchBody( if (serializedContext !== undefined) { opts.data.set(TSS_FORMDATA_CONTEXT, serializedContext) } - return { body: opts.data } + return opts.data } const serializedBody = await serializePayload(opts) if (serializedBody) { - return { body: serializedBody, contentType: 'application/json' } + return serializedBody } return undefined } From 35dd298422d2623996f4a6c232a35a6b07fb973c Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 03:46:04 +0200 Subject: [PATCH 11/13] test(history): cover browser action batching --- .../tests/createBrowserHistory.test.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/history/tests/createBrowserHistory.test.ts diff --git a/packages/history/tests/createBrowserHistory.test.ts b/packages/history/tests/createBrowserHistory.test.ts new file mode 100644 index 00000000000..97b3c40ef3c --- /dev/null +++ b/packages/history/tests/createBrowserHistory.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test, vi } from 'vitest' +import { createBrowserHistory } from '../src' + +function createBrowserHistoryHarness() { + const location = { + pathname: '/', + search: '', + hash: '', + } + const pushState = vi.fn() + const replaceState = vi.fn() + const nativeHistory = { + state: { __TSR_index: 0, __TSR_key: 'initial' }, + length: 1, + pushState, + replaceState, + back: vi.fn(), + forward: vi.fn(), + go: vi.fn(), + } + const window = { + location, + history: nativeHistory, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + } + const history = createBrowserHistory({ window }) + + return { + history, + pushState, + replaceState, + } +} + +describe('createBrowserHistory', () => { + test('coalesces consecutive replaces into the latest replace', async () => { + const { history, pushState, replaceState } = createBrowserHistoryHarness() + + history.replace('/first', { value: 1 }) + history.replace('/second', { value: 2 }) + await Promise.resolve() + + expect(pushState).not.toHaveBeenCalled() + expect(replaceState).toHaveBeenCalledTimes(1) + expect(replaceState).toHaveBeenCalledWith( + expect.objectContaining({ value: 2 }), + '', + '/second', + ) + history.destroy() + }) + + test('promotes a queued replace to a push', async () => { + const { history, pushState, replaceState } = createBrowserHistoryHarness() + + history.replace('/first', { value: 1 }) + history.push('/second', { value: 2 }) + await Promise.resolve() + + expect(replaceState).not.toHaveBeenCalled() + expect(pushState).toHaveBeenCalledTimes(1) + expect(pushState).toHaveBeenCalledWith( + expect.objectContaining({ value: 2 }), + '', + '/second', + ) + history.destroy() + }) + + test('keeps a queued push when followed by a replace', async () => { + const { history, pushState, replaceState } = createBrowserHistoryHarness() + + history.push('/first', { value: 1 }) + history.replace('/second', { value: 2 }) + await Promise.resolve() + + expect(replaceState).not.toHaveBeenCalled() + expect(pushState).toHaveBeenCalledTimes(1) + expect(pushState).toHaveBeenCalledWith( + expect.objectContaining({ value: 2 }), + '', + '/second', + ) + history.destroy() + }) + + test('flushes a later action after an explicit flush', async () => { + const { history, pushState, replaceState } = createBrowserHistoryHarness() + + history.replace('/first', { value: 1 }) + history.flush() + await Promise.resolve() + history.replace('/second', { value: 2 }) + await Promise.resolve() + + expect(pushState).not.toHaveBeenCalled() + expect(replaceState).toHaveBeenCalledTimes(2) + expect(replaceState).toHaveBeenLastCalledWith( + expect.objectContaining({ value: 2 }), + '', + '/second', + ) + history.destroy() + }) +}) From c326505fa7595350fdcc4fafc5d5f55d88e09645 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 6 Aug 2026 03:46:12 +0200 Subject: [PATCH 12/13] docs: record compact private boundary results --- ...optimization-compact-private-boundaries.md | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 RESULT-optimization-compact-private-boundaries.md diff --git a/RESULT-optimization-compact-private-boundaries.md b/RESULT-optimization-compact-private-boundaries.md new file mode 100644 index 00000000000..08b73e2ede7 --- /dev/null +++ b/RESULT-optimization-compact-private-boundaries.md @@ -0,0 +1,107 @@ +# Compact private boundaries + +Baseline: `main` at `697ebb6ddbd433d052b6b4707938a5c595865d58`. + +## Principle + +When a value is constructed and consumed entirely inside one module or a +private framework context, prefer the smallest readable boundary for that +value: a labeled tuple, positional parameters, or the value itself instead of +a one-use wrapper object. Public options, component props, callbacks, browser +history state, and serialized HTTP data keep their existing shapes. + +This change applies that rule to: + +- coalesced browser-history actions, +- router-core's private lightweight match result, +- Solid's private nearest-match context, +- Vue's private Scripts and link helper inputs, and +- Start's private fetch-body result. + +## Bundle result + +All 17 scenarios improved in gzip, initial gzip, and raw JavaScript size. + +| Scenario | gzip before → after | Initial gzip | Raw | Brotli | +| ---------------------------------- | ------------------: | -----------: | -----: | -----: | +| `react-router.full` | 93,079 → 93,058 B | -23 B | -108 B | -158 B | +| `react-router.minimal` | 89,200 → 89,162 B | -34 B | -112 B | -72 B | +| `react-start.deferred-hydration` | 103,769 → 103,691 B | -76 B | -161 B | -35 B | +| `react-start.full` | 106,415 → 106,370 B | -44 B | -161 B | +42 B | +| `react-start.minimal` | 103,012 → 102,952 B | -62 B | -161 B | +1 B | +| `react-start.rsbuild.full` | 106,054 → 105,968 B | -86 B | -163 B | -140 B | +| `react-start.rsbuild.minimal` | 102,641 → 102,561 B | -80 B | -163 B | -34 B | +| `react-start.rsbuild.minimal-iife` | 103,059 → 102,982 B | -77 B | -163 B | -121 B | +| `solid-router.full` | 40,857 → 40,816 B | -39 B | -181 B | -58 B | +| `solid-router.minimal` | 35,632 → 35,605 B | -27 B | -171 B | +12 B | +| `solid-start.deferred-hydration` | 52,678 → 52,629 B | -51 B | -223 B | -124 B | +| `solid-start.full` | 54,715 → 54,659 B | -55 B | -234 B | -41 B | +| `solid-start.minimal` | 49,338 → 49,287 B | -50 B | -223 B | +21 B | +| `vue-router.full` | 59,444 → 59,356 B | -89 B | -376 B | -35 B | +| `vue-router.minimal` | 53,346 → 53,282 B | -63 B | -278 B | -3 B | +| `vue-start.full` | 74,681 → 74,569 B | -112 B | -425 B | -26 B | +| `vue-start.minimal` | 70,627 → 70,502 B | -126 B | -425 B | +52 B | + +The ranges are -21 to -125 B gzip, -23 to -126 B initial gzip, and +-108 to -425 B raw. Brotli changed by -158 to +52 B; 5 of 17 scenarios +regressed slightly in Brotli despite improving in the primary gzip metric and +in raw size. + +## Hunk attribution + +Each production group was measured alone against the same baseline. The +scenario shown is the smallest representative bundle containing the group. + +| Group | Scenario | gzip | Initial gzip | Raw | Brotli | +| --------------------------------- | ---------------------- | ----: | -----------: | -----: | -----: | +| History queued action tuple | `react-router.minimal` | -23 B | -24 B | -61 B | -130 B | +| Lightweight match tuple | `react-router.minimal` | -11 B | -10 B | -51 B | -4 B | +| Solid nearest-match tuple | `solid-router.minimal` | -13 B | -13 B | -60 B | -5 B | +| Vue Scripts tuple/direct inputs | `vue-router.full` | -25 B | -26 B | -96 B | +14 B | +| Vue link positional helper inputs | `vue-router.minimal` | -35 B | -33 B | -168 B | -37 B | +| Start fetch-body scalar result | `react-start.full` | -23 B | -19 B | -52 B | -21 B | + +For the Vue link group, href alone was -7 B gzip, href plus active state was +-29 B, and the complete href/active/style group was -35 B. Compression is +nonlinear, so isolated values do not add exactly to the composed result. + +## Performance gate + +A proposed internal LRU node tuple was excluded from this change. It saved +9 B gzip in isolation, but two direct benchmark runs showed approximately +22–24% lower eviction-churn throughput. Runtime performance takes precedence +over that size win. + +The retained lightweight match tuple was benchmarked through cached and +uncached `buildLocation` calls: + +| Workload | Baseline run 1 | Candidate run 1 | Baseline run 2 | Candidate run 2 | +| --------------- | -------------: | --------------: | -------------: | --------------: | +| Cached source | 456,242 ops/s | 460,718 ops/s | 457,658 ops/s | 455,549 ops/s | +| Uncached source | 301,365 ops/s | 300,952 ops/s | 302,925 ops/s | 300,540 ops/s | + +The cached result moved in both directions across runs. The uncached result +was within 0.8% of baseline while its benchmark margin of error was about 3%, +so no material performance change was detected. + +## Correctness and review + +Focused browser-history tests cover replace→replace, replace→push, +push→replace, latest href/state selection, and rescheduling after an explicit +flush. The explicit-flush case drains the old microtask before queuing the next +action so it cannot pass via the earlier scheduled callback. + +Validation passed: + +- History unit tests: 29 passed, including the 4 new batching cases. +- Router-core unit tests: 1,523 passed and 3 expected failures. +- Solid Router client tests: 838 passed and 1 skipped; server tests: 3 passed. +- Vue Router unit tests: 814 passed and 1 skipped. +- Start client core unit tests: 80 passed. +- Type tests for all five affected packages across their configured TypeScript versions. +- ESLint for all five affected packages; no errors, existing warnings remain. +- React Start server-function E2E coverage for JSON POST, multipart upload, + FormData serialization, FormData context, and direct FormData POST: 5 passed. +- Full 17-scenario bundle-size matrix. +- Five independent semantic, framework, core/performance, tree-shaking, and + adversarial reviews; no remaining blockers. From ba8e35b3377c9b9cf2fced03a315078ecfd54735 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 6 Aug 2026 09:17:52 +0200 Subject: [PATCH 13/13] Delete RESULT-optimization-compact-private-boundaries.md --- ...optimization-compact-private-boundaries.md | 107 ------------------ 1 file changed, 107 deletions(-) delete mode 100644 RESULT-optimization-compact-private-boundaries.md diff --git a/RESULT-optimization-compact-private-boundaries.md b/RESULT-optimization-compact-private-boundaries.md deleted file mode 100644 index 08b73e2ede7..00000000000 --- a/RESULT-optimization-compact-private-boundaries.md +++ /dev/null @@ -1,107 +0,0 @@ -# Compact private boundaries - -Baseline: `main` at `697ebb6ddbd433d052b6b4707938a5c595865d58`. - -## Principle - -When a value is constructed and consumed entirely inside one module or a -private framework context, prefer the smallest readable boundary for that -value: a labeled tuple, positional parameters, or the value itself instead of -a one-use wrapper object. Public options, component props, callbacks, browser -history state, and serialized HTTP data keep their existing shapes. - -This change applies that rule to: - -- coalesced browser-history actions, -- router-core's private lightweight match result, -- Solid's private nearest-match context, -- Vue's private Scripts and link helper inputs, and -- Start's private fetch-body result. - -## Bundle result - -All 17 scenarios improved in gzip, initial gzip, and raw JavaScript size. - -| Scenario | gzip before → after | Initial gzip | Raw | Brotli | -| ---------------------------------- | ------------------: | -----------: | -----: | -----: | -| `react-router.full` | 93,079 → 93,058 B | -23 B | -108 B | -158 B | -| `react-router.minimal` | 89,200 → 89,162 B | -34 B | -112 B | -72 B | -| `react-start.deferred-hydration` | 103,769 → 103,691 B | -76 B | -161 B | -35 B | -| `react-start.full` | 106,415 → 106,370 B | -44 B | -161 B | +42 B | -| `react-start.minimal` | 103,012 → 102,952 B | -62 B | -161 B | +1 B | -| `react-start.rsbuild.full` | 106,054 → 105,968 B | -86 B | -163 B | -140 B | -| `react-start.rsbuild.minimal` | 102,641 → 102,561 B | -80 B | -163 B | -34 B | -| `react-start.rsbuild.minimal-iife` | 103,059 → 102,982 B | -77 B | -163 B | -121 B | -| `solid-router.full` | 40,857 → 40,816 B | -39 B | -181 B | -58 B | -| `solid-router.minimal` | 35,632 → 35,605 B | -27 B | -171 B | +12 B | -| `solid-start.deferred-hydration` | 52,678 → 52,629 B | -51 B | -223 B | -124 B | -| `solid-start.full` | 54,715 → 54,659 B | -55 B | -234 B | -41 B | -| `solid-start.minimal` | 49,338 → 49,287 B | -50 B | -223 B | +21 B | -| `vue-router.full` | 59,444 → 59,356 B | -89 B | -376 B | -35 B | -| `vue-router.minimal` | 53,346 → 53,282 B | -63 B | -278 B | -3 B | -| `vue-start.full` | 74,681 → 74,569 B | -112 B | -425 B | -26 B | -| `vue-start.minimal` | 70,627 → 70,502 B | -126 B | -425 B | +52 B | - -The ranges are -21 to -125 B gzip, -23 to -126 B initial gzip, and --108 to -425 B raw. Brotli changed by -158 to +52 B; 5 of 17 scenarios -regressed slightly in Brotli despite improving in the primary gzip metric and -in raw size. - -## Hunk attribution - -Each production group was measured alone against the same baseline. The -scenario shown is the smallest representative bundle containing the group. - -| Group | Scenario | gzip | Initial gzip | Raw | Brotli | -| --------------------------------- | ---------------------- | ----: | -----------: | -----: | -----: | -| History queued action tuple | `react-router.minimal` | -23 B | -24 B | -61 B | -130 B | -| Lightweight match tuple | `react-router.minimal` | -11 B | -10 B | -51 B | -4 B | -| Solid nearest-match tuple | `solid-router.minimal` | -13 B | -13 B | -60 B | -5 B | -| Vue Scripts tuple/direct inputs | `vue-router.full` | -25 B | -26 B | -96 B | +14 B | -| Vue link positional helper inputs | `vue-router.minimal` | -35 B | -33 B | -168 B | -37 B | -| Start fetch-body scalar result | `react-start.full` | -23 B | -19 B | -52 B | -21 B | - -For the Vue link group, href alone was -7 B gzip, href plus active state was --29 B, and the complete href/active/style group was -35 B. Compression is -nonlinear, so isolated values do not add exactly to the composed result. - -## Performance gate - -A proposed internal LRU node tuple was excluded from this change. It saved -9 B gzip in isolation, but two direct benchmark runs showed approximately -22–24% lower eviction-churn throughput. Runtime performance takes precedence -over that size win. - -The retained lightweight match tuple was benchmarked through cached and -uncached `buildLocation` calls: - -| Workload | Baseline run 1 | Candidate run 1 | Baseline run 2 | Candidate run 2 | -| --------------- | -------------: | --------------: | -------------: | --------------: | -| Cached source | 456,242 ops/s | 460,718 ops/s | 457,658 ops/s | 455,549 ops/s | -| Uncached source | 301,365 ops/s | 300,952 ops/s | 302,925 ops/s | 300,540 ops/s | - -The cached result moved in both directions across runs. The uncached result -was within 0.8% of baseline while its benchmark margin of error was about 3%, -so no material performance change was detected. - -## Correctness and review - -Focused browser-history tests cover replace→replace, replace→push, -push→replace, latest href/state selection, and rescheduling after an explicit -flush. The explicit-flush case drains the old microtask before queuing the next -action so it cannot pass via the earlier scheduled callback. - -Validation passed: - -- History unit tests: 29 passed, including the 4 new batching cases. -- Router-core unit tests: 1,523 passed and 3 expected failures. -- Solid Router client tests: 838 passed and 1 skipped; server tests: 3 passed. -- Vue Router unit tests: 814 passed and 1 skipped. -- Start client core unit tests: 80 passed. -- Type tests for all five affected packages across their configured TypeScript versions. -- ESLint for all five affected packages; no errors, existing warnings remain. -- React Start server-function E2E coverage for JSON POST, multipart upload, - FormData serialization, FormData context, and direct FormData POST: 5 passed. -- Full 17-scenario bundle-size matrix. -- Five independent semantic, framework, core/performance, tree-shaking, and - adversarial reviews; no remaining blockers.