From 272efe1c8abf4b09406b66aad28b0ca219dbebfd Mon Sep 17 00:00:00 2001 From: Manuel Schiller <6340397+schiller-manuel@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:22:15 +0200 Subject: [PATCH] perf: skip unused Start request setup Create the request middleware deduplication set only when request middleware is configured, and pass Start's serialization adapters to the router without copying them when the router adds none. Server route handlers and route middleware now receive the router's decoded pathname, the one used for route matching. Co-Authored-By: Claude Opus 5.5 --- .changeset/honest-walls-attack.md | 8 ++ .../src/createStartHandler.ts | 46 ++++---- .../tests/createStartHandler.test.ts | 101 ++++++++++++++++-- .../src/async-local-storage.ts | 2 +- 4 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 .changeset/honest-walls-attack.md diff --git a/.changeset/honest-walls-attack.md b/.changeset/honest-walls-attack.md new file mode 100644 index 00000000000..77ee9c495f3 --- /dev/null +++ b/.changeset/honest-walls-attack.md @@ -0,0 +1,8 @@ +--- +'@tanstack/start-server-core': patch +'@tanstack/start-storage-context': patch +--- + +Skip request middleware bookkeeping when no request middleware is configured, and stop copying serialization adapters when the router adds none. + +Server route handlers and route middleware now receive the router's decoded pathname, the same one used for route matching. Characters that would change how the path is read, such as `%2F`, `%3F`, and `%23`, stay encoded. diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index 661dffc584e..f1be8fe0d9a 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -709,15 +709,19 @@ export function createStartHandler( serializationAdapters, } - // Reuse the flattening set for request middleware deduplication. - const executedRequestMiddlewares = new Set() - const flattenedRequestMiddlewares = requestStartOptions.requestMiddleware - ? flattenMiddlewares( - requestStartOptions.requestMiddleware, - undefined, - executedRequestMiddlewares, - ) - : [] + const requestMiddleware = requestStartOptions.requestMiddleware + let executedRequestMiddlewares: Set | undefined + let flattenedRequestMiddlewares: ReturnType = + [] + if (requestMiddleware?.length) { + // Reuse the flattening set for request middleware deduplication. + executedRequestMiddlewares = new Set() + flattenedRequestMiddlewares = flattenMiddlewares( + requestMiddleware, + undefined, + executedRequestMiddlewares, + ) + } // Memoized router getter const getRouter = (): Promise => { @@ -746,10 +750,13 @@ export function createStartHandler( // Start-owned options that RouterConstructorOptions omits. ...{ defaultSsr: requestStartOptions.defaultSsr, - serializationAdapters: [ - ...requestStartOptions.serializationAdapters, - ...(requestRouter.options.serializationAdapters || []), - ], + serializationAdapters: requestRouter.options.serializationAdapters + ?.length + ? [ + ...requestStartOptions.serializationAdapters, + ...requestRouter.options.serializationAdapters, + ] + : requestStartOptions.serializationAdapters, }, basepath: ROUTER_BASEPATH, }) @@ -1139,20 +1146,17 @@ async function handleServerRoutes({ router: AnyRouter, ) => Promise context: any - executedRequestMiddlewares: Set + executedRequestMiddlewares: Set | undefined signal: AbortSignal event: StartEvent }): Promise { const router = await getRouter() - const location = router.latestLocation - // Preserve the encoded pathname exposed to server handlers and middleware. - const pathname = location.href.split(/[?#]/, 1)[0]! + const { pathname } = router.latestLocation // this will perform a fuzzy match, however for server routes we need an exact match // if the route is not an exact match, executeRouter will handle rendering the app router // The cached match avoids another route-tree traversal during the app router render. - const [matchedRoutes, rawParams, foundRoute] = router.getMatchedRoutes( - location.pathname, - ) + const [matchedRoutes, rawParams, foundRoute] = + router.getMatchedRoutes(pathname) const isExactMatch = foundRoute && rawParams['**'] === undefined @@ -1171,7 +1175,7 @@ async function handleServerRoutes({ if (serverMiddleware) { const flattened = flattenMiddlewares(serverMiddleware) for (const m of flattened) { - if (!executedRequestMiddlewares.has(m)) { + if (!executedRequestMiddlewares?.has(m)) { routeMiddlewares.push(m.options.server) } } diff --git a/packages/start-server-core/tests/createStartHandler.test.ts b/packages/start-server-core/tests/createStartHandler.test.ts index bfed64adb1e..f2918ed5234 100644 --- a/packages/start-server-core/tests/createStartHandler.test.ts +++ b/packages/start-server-core/tests/createStartHandler.test.ts @@ -14,6 +14,7 @@ import { RouterCore, createNonReactiveMutableStore, createNonReactiveReadonlyStore, + createSerializationAdapter, redirect, } from '@tanstack/router-core' import { @@ -82,6 +83,7 @@ const startMocks = vi.hoisted(() => { serverFnCalls: [] as Array<{ context?: unknown }>, router: undefined as undefined | AnyRouter, routerFactory: undefined as undefined | (() => AnyRouter), + serializationAdapters: [] as Array, } }) @@ -89,7 +91,7 @@ vi.mock('#tanstack-start-entry', () => ({ startInstance: { getOptions: () => ({ requestMiddleware: startMocks.requestMiddleware, - serializationAdapters: [], + serializationAdapters: startMocks.serializationAdapters, }), }, })) @@ -267,6 +269,7 @@ afterEach(() => { startMocks.serverFnCalls = [] startMocks.router = undefined startMocks.routerFactory = undefined + startMocks.serializationAdapters = [] vi.unstubAllEnvs() }) @@ -1827,10 +1830,8 @@ describe('createStartHandler request location reuse', () => { renderApp ? 'app response' : 'server response', ) expect(input).toHaveBeenCalledOnce() - expect(serverHandler).toHaveBeenCalledExactlyOnceWith( - `/${encodeURIComponent(path)}`, - ) - expect(middlewarePathnames).toEqual([`/${encodeURIComponent(path)}`]) + expect(serverHandler).toHaveBeenCalledExactlyOnceWith(`/${path}`) + expect(middlewarePathnames).toEqual([`/${path}`]) expect(render).toHaveBeenCalledTimes(renderApp ? 1 : 0) expect( getMatchedRoutes.mock.calls.every( @@ -1923,9 +1924,17 @@ describe('createStartHandler request location reuse', () => { expect(parseSearch).toHaveBeenCalledExactlyOnceWith('?page=2') }) - it.each(['a/b', 'a%b', 'a b', 'a?b', 'a#b'])( + // Handlers receive the router's pathname: decoded, except for characters + // that would change how the path is read. + it.each([ + ['a/b', 'a%2Fb'], + ['a%b', 'a%25b'], + ['a b', 'a b'], + ['a?b', 'a%3Fb'], + ['a#b', 'a%23b'], + ])( 'preserves encoded params %j when a server handler continues to SSR', - async (value) => { + async (value, pathname) => { const handlerParams: Array = [] const handlerPathnames: Array = [] const root = new BaseRootRoute() @@ -1963,7 +1972,7 @@ describe('createStartHandler request location reuse', () => { expect(response.status).toBe(200) expect(handlerParams).toEqual([value]) - expect(handlerPathnames).toEqual([`/params/${encodeURIComponent(value)}`]) + expect(handlerPathnames).toEqual([`/params/${pathname}`]) expect(await response.json()).toEqual({ params: { value }, loaderData: value, @@ -3273,6 +3282,39 @@ describe('createStartHandler SSR cleanup ownership', () => { }) describe('createStartHandler router initialization', () => { + it('keeps adapter order and router adapters isolated to their request', async () => { + const adapter = (key: string) => + createSerializationAdapter({ + key, + test: (value: unknown): value is Date => value instanceof Date, + toSerializable: (value: Date) => value.toISOString(), + fromSerializable: (value: string) => new Date(value), + }) + const startAdapter = adapter('start') + const routerAdapter = adapter('router') + const startAdapters = [startAdapter] + startMocks.serializationAdapters = startAdapters + let requestCount = 0 + startMocks.routerFactory = () => { + const router = makeRouter() + router.options.serializationAdapters = + requestCount++ === 0 ? [routerAdapter] : [] + return router + } + const handler = createStartHandler(({ router }) => + Response.json( + router.options.serializationAdapters?.map(({ key }) => key), + ), + ) + + const first = await handler(new Request('http://localhost/'), {}) + const second = await handler(new Request('http://localhost/'), {}) + + expect(await first.json()).toEqual(['start', '$TSS/serverfn', 'router']) + expect(await second.json()).toEqual(['start', '$TSS/serverfn']) + expect(startAdapters).toEqual([startAdapter]) + }) + it('normalizes a server function router URL while preserving the incoming URL', async () => { const requestUrl = 'http://localhost/_serverFn/test?space=a%20b&tilde=~&bad=%GG&a=1&a=2#part' @@ -3479,6 +3521,49 @@ describe('createStartHandler router initialization', () => { }) describe('createStartHandler direct server routes', () => { + it.each([false, true])( + 'runs shared route middleware once and preserves its context (global=%s)', + async (global) => { + const runMiddleware = vi.fn(({ next }) => + next({ context: { message: 'trusted middleware' } }), + ) + const middleware = createMiddleware().server(runMiddleware) + startMocks.requestMiddleware = global ? [middleware] : [] + const root = new BaseRootRoute({ + server: { middleware: [middleware] }, + }) + startMocks.router = new RouterCore( + { + isServer: true, + routeTree: root.addChildren([ + new BaseRoute({ + getParentRoute: () => root, + path: '/', + component: () => null, + }), + ]), + }, + getStoreConfig, + ) + const handler = createStartHandler<{ + server: { requestContext: { message: string; requestValue: string } } + }>(({ router }) => + Response.json(router.options.additionalContext?.serverContext), + ) + + const response = await handler(new Request('http://localhost/'), { + context: { message: 'request context', requestValue: 'retained' }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + message: 'trusted middleware', + requestValue: 'retained', + }) + expect(runMiddleware).toHaveBeenCalledOnce() + }, + ) + it('disposes a thrown sole-terminal response when the request aborts after the fast-path check', async () => { const requestController = new AbortController() const cancellation = new Error('request disconnected') diff --git a/packages/start-storage-context/src/async-local-storage.ts b/packages/start-storage-context/src/async-local-storage.ts index 0132293f760..bbde7046298 100644 --- a/packages/start-storage-context/src/async-local-storage.ts +++ b/packages/start-storage-context/src/async-local-storage.ts @@ -16,7 +16,7 @@ export interface StartStorageContext { contextAfterGlobalMiddlewares: any // Track middlewares that have already executed in the request phase // to prevent duplicate execution - executedRequestMiddlewares: Set + executedRequestMiddlewares?: Set // Type of handler processing this request handlerType: StartHandlerType