Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/quiet-ssr-hotspots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/router-core': patch
'@tanstack/start-plugin-core': patch
---

Reduce per-request SSR overhead: abort settled route matches with one shared reason instead of building a stack-capturing `DOMException` per match, skip `JSON.parse` for search values that cannot start JSON, wait on request signals with one listener per wait, and keep resolved server-function modules in production builds instead of re-importing them on every call.
29 changes: 29 additions & 0 deletions .changeset/steady-streams-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@tanstack/router-core': patch
'@tanstack/router-ssr-query-core': patch
'@tanstack/react-router': patch
'@tanstack/react-router-ssr-query': patch
'@tanstack/solid-router': patch
'@tanstack/solid-router-ssr-query': patch
'@tanstack/solid-start-server': patch
'@tanstack/start-client-core': patch
'@tanstack/start-server-core': patch
'@tanstack/start-static-server-functions': patch
'@tanstack/vue-router': patch
'@tanstack/vue-router-ssr-query': patch
'@tanstack/vue-start-server': patch
---

Stream large deferred SSR hydration payloads through a backpressure-aware router transport, fail known setup errors before response creation, and close cancelled or expired transforms safely.

Start now cancels discarded middleware and HEAD response bodies, including plain streams and derived branches.

Server-function raw streams share one ordered response. Arbitrary or sequential consumption can require potentially unbounded buffering of unread data on the client. Cancelling one raw stream discards it locally, while aborting the whole call cancels the response and server work. Consume streams concurrently, cancel unused streams promptly, or use separate calls when independent backpressure is required.

The JSON wire shape of a `RawStream` server-function argument changed. Clients and servers must run matching versions for requests that pass a `RawStream`.

Solid SSR now emits one document type and renders late lazy errors through route boundaries. A Solid `<Await>` without a `fallback` no longer holds the streamed shell; it renders inside the nearest `<Suspense>` boundary like React and Vue, and now renders falsy resolved values.

Static server functions decode cached `RawStream` values with the client deserializer plugins.

SSR Query integrations now keep request cleanup and stream ownership aligned with the router lifecycle.
72 changes: 36 additions & 36 deletions benchmarks/ssr/scenarios/control-flow/vue/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,40 @@

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as TargetIdRouteImport } from './routes/target.$id'
import { Route as MissingIdRouteImport } from './routes/missing.$id'
import { Route as HeadersIdRouteImport } from './routes/headers.$id'
import { Route as FromIdRouteImport } from './routes/from.$id'
import { Route as BoomIdRouteImport } from './routes/boom.$id'
import { Route as FromIdRouteImport } from './routes/from.$id'
import { Route as HeadersIdRouteImport } from './routes/headers.$id'
import { Route as MissingIdRouteImport } from './routes/missing.$id'
import { Route as TargetIdRouteImport } from './routes/target.$id'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const TargetIdRoute = TargetIdRouteImport.update({
id: '/target/$id',
path: '/target/$id',
const BoomIdRoute = BoomIdRouteImport.update({
id: '/boom/$id',
path: '/boom/$id',
getParentRoute: () => rootRouteImport,
} as any)
const MissingIdRoute = MissingIdRouteImport.update({
id: '/missing/$id',
path: '/missing/$id',
const FromIdRoute = FromIdRouteImport.update({
id: '/from/$id',
path: '/from/$id',
getParentRoute: () => rootRouteImport,
} as any)
const HeadersIdRoute = HeadersIdRouteImport.update({
id: '/headers/$id',
path: '/headers/$id',
getParentRoute: () => rootRouteImport,
} as any)
const FromIdRoute = FromIdRouteImport.update({
id: '/from/$id',
path: '/from/$id',
const MissingIdRoute = MissingIdRouteImport.update({
id: '/missing/$id',
path: '/missing/$id',
getParentRoute: () => rootRouteImport,
} as any)
const BoomIdRoute = BoomIdRouteImport.update({
id: '/boom/$id',
path: '/boom/$id',
const TargetIdRoute = TargetIdRouteImport.update({
id: '/target/$id',
path: '/target/$id',
getParentRoute: () => rootRouteImport,
} as any)

Expand Down Expand Up @@ -117,18 +117,18 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/target/$id': {
id: '/target/$id'
path: '/target/$id'
fullPath: '/target/$id'
preLoaderRoute: typeof TargetIdRouteImport
'/boom/$id': {
id: '/boom/$id'
path: '/boom/$id'
fullPath: '/boom/$id'
preLoaderRoute: typeof BoomIdRouteImport
parentRoute: typeof rootRouteImport
}
'/missing/$id': {
id: '/missing/$id'
path: '/missing/$id'
fullPath: '/missing/$id'
preLoaderRoute: typeof MissingIdRouteImport
'/from/$id': {
id: '/from/$id'
path: '/from/$id'
fullPath: '/from/$id'
preLoaderRoute: typeof FromIdRouteImport
parentRoute: typeof rootRouteImport
}
'/headers/$id': {
Expand All @@ -138,18 +138,18 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof HeadersIdRouteImport
parentRoute: typeof rootRouteImport
}
'/from/$id': {
id: '/from/$id'
path: '/from/$id'
fullPath: '/from/$id'
preLoaderRoute: typeof FromIdRouteImport
'/missing/$id': {
id: '/missing/$id'
path: '/missing/$id'
fullPath: '/missing/$id'
preLoaderRoute: typeof MissingIdRouteImport
parentRoute: typeof rootRouteImport
}
'/boom/$id': {
id: '/boom/$id'
path: '/boom/$id'
fullPath: '/boom/$id'
preLoaderRoute: typeof BoomIdRouteImport
'/target/$id': {
id: '/target/$id'
path: '/target/$id'
fullPath: '/target/$id'
preLoaderRoute: typeof TargetIdRouteImport
parentRoute: typeof rootRouteImport
}
}
Expand Down
31 changes: 23 additions & 8 deletions benchmarks/ssr/scenarios/global-middleware/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import {
import {
expectedFunctionTotal,
expectedRequestTotal,
getExpectedRequestContext,
makeDocumentMarker,
makeServerFnMarker,
makeServerRouteMarker,
setRequestMiddlewareCount,
} from './shared'
import type { RequestMiddlewareCount } from './shared'
import type { StartRequestHandler } from '../../bench-utils'

export type { StartRequestHandler }
Expand Down Expand Up @@ -140,6 +143,7 @@ function buildPostRequest(urls: FnUrls, bodies: Array<string>, index: number) {
export async function setupGlobalMiddlewareBench(
handler: StartRequestHandler,
): Promise<GlobalMiddlewareBenchContext> {
setRequestMiddlewareCount(3)
const urls = await discoverUrls(handler)
const payloads = createPayloads()
const bodies = await createBodies(payloads)
Expand All @@ -154,6 +158,7 @@ export async function setupGlobalMiddlewareBench(
}

async function assertDocumentResponse(handler: StartRequestHandler) {
setRequestMiddlewareCount(3)
const id = 'page-sanity'
const response = await handler.fetch(
new Request(`${origin}/page/${id}`, documentRequestInit),
Expand All @@ -175,7 +180,11 @@ async function assertDocumentResponse(handler: StartRequestHandler) {
}
}

async function assertServerRouteResponse(handler: StartRequestHandler) {
async function assertServerRouteResponseForCount(
handler: StartRequestHandler,
count: RequestMiddlewareCount,
) {
setRequestMiddlewareCount(count)
const id = 'route-sanity'
const response = await handler.fetch(
new Request(`${origin}/api/ping/${id}`, apiRequestInit),
Expand All @@ -199,20 +208,18 @@ async function assertServerRouteResponse(handler: StartRequestHandler) {
marker?: string
requestTotal?: number
}
const expectedMarker = makeServerRouteMarker(id, {
requestTrace: 'req.r1.r2.r3',
requestTotal: expectedRequestTotal,
})
const expectedContext = getExpectedRequestContext(count)
const expectedMarker = makeServerRouteMarker(id, expectedContext)

if (body.marker !== expectedMarker) {
throw new Error(
`Expected server route marker ${expectedMarker}, received ${body.marker}`,
)
}

if (body.requestTotal !== expectedRequestTotal) {
if (body.requestTotal !== expectedContext.requestTotal) {
throw new Error(
`Expected server route requestTotal ${expectedRequestTotal}, received ${body.requestTotal}`,
`Expected server route requestTotal ${expectedContext.requestTotal}, received ${body.requestTotal}`,
)
}
}
Expand All @@ -221,6 +228,7 @@ async function assertServerFnResponse(
handler: StartRequestHandler,
context: GlobalMiddlewareBenchContext,
) {
setRequestMiddlewareCount(3)
const response = await handler.fetch(
buildPostRequest(context.urls, context.bodies, 0),
)
Expand Down Expand Up @@ -257,10 +265,14 @@ export async function assertGlobalMiddlewareScenario(
) {
await assertDocumentResponse(handler)
await assertServerFnResponse(handler, context)
await assertServerRouteResponse(handler)
for (const count of [0, 1, 2, 3] as const) {
await assertServerRouteResponseForCount(handler, count)
}
setRequestMiddlewareCount(3)
}

export function runGlobalMiddlewareDocumentLoop(handler: StartRequestHandler) {
setRequestMiddlewareCount(3)
return runRequestLoop(handler, {
seed: benchmarkSeed,
concurrency: 16,
Expand All @@ -273,6 +285,7 @@ export function runGlobalMiddlewareServerFnLoop(
handler: StartRequestHandler,
context: GlobalMiddlewareBenchContext,
) {
setRequestMiddlewareCount(3)
return runRequestLoop(handler, {
seed: benchmarkSeed,
concurrency: 16,
Expand All @@ -284,7 +297,9 @@ export function runGlobalMiddlewareServerFnLoop(

export function runGlobalMiddlewareServerRouteLoop(
handler: StartRequestHandler,
count: RequestMiddlewareCount = 3,
) {
setRequestMiddlewareCount(count)
return runRequestLoop(handler, {
seed: benchmarkSeed,
concurrency: 16,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { bench, describe } from 'vitest'
import {
assertGlobalMiddlewareScenario,
globalMiddlewareBenchOptions,
runGlobalMiddlewareServerRouteLoop,
setupGlobalMiddlewareBench,
} from '../bench'
import type { StartRequestHandler } from '../bench'

const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href

const { default: handler } = (await import(
/* @vite-ignore */ appModuleUrl
)) as {
default: StartRequestHandler
}
const context = await setupGlobalMiddlewareBench(handler)

await assertGlobalMiddlewareScenario(handler, context)

describe('ssr', () => {
bench(
'ssr global-mw server-route 0 middleware (react)',
() => runGlobalMiddlewareServerRouteLoop(handler, 0),
globalMiddlewareBenchOptions,
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { bench, describe } from 'vitest'
import {
assertGlobalMiddlewareScenario,
globalMiddlewareBenchOptions,
runGlobalMiddlewareServerRouteLoop,
setupGlobalMiddlewareBench,
} from '../bench'
import type { StartRequestHandler } from '../bench'

const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href

const { default: handler } = (await import(
/* @vite-ignore */ appModuleUrl
)) as {
default: StartRequestHandler
}
const context = await setupGlobalMiddlewareBench(handler)

await assertGlobalMiddlewareScenario(handler, context)

describe('ssr', () => {
bench(
'ssr global-mw server-route 1 middleware (react)',
() => runGlobalMiddlewareServerRouteLoop(handler, 1),
globalMiddlewareBenchOptions,
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { bench, describe } from 'vitest'
import {
assertGlobalMiddlewareScenario,
globalMiddlewareBenchOptions,
runGlobalMiddlewareServerRouteLoop,
setupGlobalMiddlewareBench,
} from '../bench'
import type { StartRequestHandler } from '../bench'

const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href

const { default: handler } = (await import(
/* @vite-ignore */ appModuleUrl
)) as {
default: StartRequestHandler
}
const context = await setupGlobalMiddlewareBench(handler)

await assertGlobalMiddlewareScenario(handler, context)

describe('ssr', () => {
bench(
'ssr global-mw server-route 2 middlewares (react)',
() => runGlobalMiddlewareServerRouteLoop(handler, 2),
globalMiddlewareBenchOptions,
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ await assertGlobalMiddlewareScenario(handler, context)

describe('ssr', () => {
bench(
'ssr global-mw server-route (react)',
() => runGlobalMiddlewareServerRouteLoop(handler),
'ssr global-mw server-route 3 middlewares (react)',
() => runGlobalMiddlewareServerRouteLoop(handler, 3),
globalMiddlewareBenchOptions,
)
})
14 changes: 9 additions & 5 deletions benchmarks/ssr/scenarios/global-middleware/react/src/start.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createMiddleware, createStart } from '@tanstack/react-start'
import { getRequestMiddlewareCount } from '../../shared'
import type { GlobalMiddlewareContext } from '../../shared'

function appendTrace(
Expand Down Expand Up @@ -48,6 +49,13 @@ const requestMiddlewareC = createMiddleware({ type: 'request' }).server(
},
)

const requestMiddlewareSets: Array<Array<typeof requestMiddlewareA>> = [
[],
[requestMiddlewareA],
[requestMiddlewareA, requestMiddlewareB],
[requestMiddlewareA, requestMiddlewareB, requestMiddlewareC],
]

const functionMiddlewareA = createMiddleware({ type: 'function' }).server(
({ next, context }) => {
const ctx = (context ?? {}) as GlobalMiddlewareContext
Expand Down Expand Up @@ -75,10 +83,6 @@ const functionMiddlewareB = createMiddleware({ type: 'function' }).server(
)

export const startInstance = createStart(() => ({
requestMiddleware: [
requestMiddlewareA,
requestMiddlewareB,
requestMiddlewareC,
],
requestMiddleware: requestMiddlewareSets[getRequestMiddlewareCount()],
functionMiddleware: [functionMiddlewareA, functionMiddlewareB],
}))
Loading
Loading