Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
79 changes: 71 additions & 8 deletions benchmarks/ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,91 @@ Each benchmark builds a Start app with file-based routes and runs Vitest benches

## Layout

- `react/` - React Start benchmark + Vitest config
- `solid/` - Solid Start benchmark + Vitest config
- `vue/` - Vue Start benchmark + Vitest config
- `react/` - React Start baseline benchmark + Vitest config
- `solid/` - Solid Start baseline benchmark + Vitest config
- `vue/` - Vue Start baseline benchmark + Vitest config
- `vitest.react.config.ts`, `vitest.solid.config.ts`, `vitest.vue.config.ts` - per-framework aggregate configs that run the baseline first, then scenario projects
- `scenarios/<scenario>/<framework>/` - isolated scenario apps

Scenario app layout:

```text
scenarios/<scenario>/<framework>/
vite.config.ts
speed.bench.ts
tsconfig.json
src/
router.tsx
routes/
routeTree.gen.ts
```

Each scenario uses one app per framework instead of sharing routes in the baseline app. This keeps route-tree size, middleware, Start options, and generated route trees isolated so one scenario cannot shift another scenario's numbers. The existing baseline apps and bench names stay stable for CodSpeed continuity.

## Scenario Responsibilities

Each scenario isolates one Start server-side responsibility so benchmark changes can be attributed to a specific feature area.

| Scenario | Start server-side responsibility |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `react/`, `solid/`, `vue/` baseline apps | Document SSR for nested file routes, route matching, search parsing, and full-page HTML response generation. |
| `assets` | Per-request asset pipeline work: CSS inlining, CDN asset URL transforms with uncached manifest resolution, and response `Link` header collection for early hints. |
| `before-load` | Nested `beforeLoad` execution: sequential per-match context building, context merging across matches, and context consumption by loaders during document SSR. |
| `control-flow` | Loader-thrown `redirect`, `notFound`, plain errors, unmatched routes, and route `headers()` emission, including HTTP status selection, redirect `location` headers, route error boundaries, and not-found HTML rendering. |
| `global-middleware` | Global `createStart` middleware registration: request middleware wrapping document SSR, server-function, and server-route requests, plus global function middleware on server functions. |
| `head` | Nested route `head` evaluation, title/meta/link serialization, and head-entry deduplication during SSR. |
| `loaders` | Nested route loader execution, loader deps from search params, router context reads, and dehydrated loader payload generation. |
| `rewrites` | Composed location rewrites from router `basepath` plus locale input/output rewrites; unprefixed URLs redirect to `/app/*` when the basepath output rewrite canonicalizes them. |
| `serialization` | Dehydration and RPC serialization of rich non-plain-JSON types: seroval native types in loader payloads, custom serialization adapters, and shallow-error serialization. |
| `selective-ssr` | Route-level `ssr` modes: rendered server HTML with `ssr: true`, dehydrated data without HTML with `ssr: 'data-only'`, and client-only omission with `ssr: false`. |
| `server-fn-transport` | Non-JSON server-function transport: multipart/FormData request decoding, raw `Response` returns, and `RawStream` streamed responses through the binary frame protocol. |
| `server-fns` | `createServerFn` GET/POST request handling, thrown redirect/notFound results, sendContext serialization, SSR-time direct calls, middleware context, input validation, and URL discovery. |
| `server-routes` | File route `server.handlers` dispatch for parameterized JSON API routes without document HTML rendering. |
| `server-routes-middleware` | Server route request middleware chains, middleware-provided context merging, and handler access to accumulated context. |
| `streaming` | Deferred loader data, `Await`/Suspense fallback output, larger streamed HTML body scanning, streamed HTML ordering, and later dehydration payload emission. |

## Run

Run all benchmarks through Nx so dependency builds are part of the graph:

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/ssr:test:perf --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:perf --outputStyle=stream --skipRemoteCache
```

Run framework-specific benchmarks:

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/ssr:test:perf:react --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/ssr:test:perf:solid --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/ssr:test:perf:vue --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:perf:react --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:perf:solid --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:perf:vue --outputStyle=stream --skipRemoteCache
```

Build framework-specific benchmark apps:

```bash
pnpm nx run @benchmarks/ssr:build:react --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:build:solid --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:build:vue --outputStyle=stream --skipRemoteCache
```

Typecheck benchmark sources:

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/ssr:test:types --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:types --outputStyle=stream --skipRemoteCache
```

Run one scenario app manually through Nx:

```bash
pnpm nx run @benchmarks/ssr-<scenario>-<framework>:build:ssr --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr-<scenario>-<framework>:test:types:ssr --outputStyle=stream --skipRemoteCache
```

Use `react`, `solid`, or `vue` for `<framework>`. The baseline projects use `@benchmarks/ssr-<framework>` without a scenario segment.

## Request Conventions

- Document GET loops use `accept: text/html`, matching the baseline request shape.
- Server-function loops must include `sec-fetch-site: same-origin` so the default CSRF middleware accepts the request.
- Loops that expect non-200 responses pass a custom `validateResponse` to `runRequestLoop`.
- Bench loops must build deterministic requests from the seeded random helper and consume response bodies through `runRequestLoop` or `runSsrRequestLoop`.
54 changes: 54 additions & 0 deletions benchmarks/ssr/bench-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export interface RunSsrRequestLoopOptions {
iterations?: number
}

export interface RunRequestLoopOptions {
seed: number
iterations?: number
buildRequest: (random: () => number, index: number) => Request
validateResponse?: (response: Response, request: Request) => void
validateBody?: (body: string, response: Response, request: Request) => void
}

const requestInit = {
method: 'GET',
headers: {
Expand All @@ -27,6 +35,8 @@ function randomSegment(random: () => number) {
return Math.floor(random() * 1_000_000_000).toString(36)
}

export { createDeterministicRandom, randomSegment }

function randomSearchValue(random: () => number) {
return `q-${randomSegment(random)}`
}
Expand Down Expand Up @@ -65,3 +75,47 @@ export async function runSsrRequestLoop(

await Promise.all(pendingBodyReads)
}

export async function runRequestLoop(
handler: StartRequestHandler,
{
seed,
iterations = 10,
buildRequest,
validateResponse,
validateBody,
}: RunRequestLoopOptions,
) {
const random = createDeterministicRandom(seed)
const pendingBodyReads: Array<Promise<void>> = []
const validate =
validateResponse ??
((response: Response, request: Request) => {
if (response.status !== 200) {
throw new Error(
`Request failed with non-200 status ${response.status} (${request.url})`,
)
}
})

for (let index = 0; index < iterations; index++) {
const request = buildRequest(random, index)
const response = await handler.fetch(request)

try {
validate(response, request)
} catch (error) {
await Promise.allSettled(pendingBodyReads)

throw error
}

pendingBodyReads.push(
response.text().then((body) => {
validateBody?.(body, response, request)
}),
)
}

await Promise.all(pendingBodyReads)
Comment thread
Sheraff marked this conversation as resolved.
}
152 changes: 127 additions & 25 deletions benchmarks/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,6 @@
"name": "@benchmarks/ssr",
"private": true,
"type": "module",
"scripts": {
"build:react": "NODE_ENV=production vite build --config ./react/vite.config.ts",
"build:solid": "NODE_ENV=production vite build --config ./solid/vite.config.ts",
"build:vue": "NODE_ENV=production vite build --config ./vue/vite.config.ts",
"test:perf": "NODE_ENV=production vitest bench",
"test:perf:react": "NODE_ENV=production vitest bench --config ./react/vite.config.ts ./react/speed.bench.ts",
"test:perf:solid": "NODE_ENV=production vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.ts",
"test:perf:vue": "NODE_ENV=production vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.ts",
"test:types": "pnpm run test:types:react && pnpm run test:types:solid && pnpm run test:types:vue",
"test:types:react": "tsc -p ./react/tsconfig.json --noEmit",
"test:types:solid": "tsc -p ./solid/tsconfig.json --noEmit",
"test:types:vue": "tsc -p ./vue/tsconfig.json --noEmit"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
Expand All @@ -31,6 +18,7 @@
"@codspeed/vitest-plugin": "^5.5.0",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"seroval": "^1.5.4",
"typescript": "^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^2.11.11",
Expand All @@ -39,68 +27,182 @@
"nx": {
"targets": {
"build:react": {
"executor": "nx:noop",
"cache": false,
"dependsOn": [
{
"projects": [
"@tanstack/react-start"
"@benchmarks/ssr-react",
"@benchmarks/ssr-assets-react",
"@benchmarks/ssr-before-load-react",
"@benchmarks/ssr-control-flow-react",
"@benchmarks/ssr-global-middleware-react",
"@benchmarks/ssr-head-react",
"@benchmarks/ssr-loaders-react",
"@benchmarks/ssr-rewrites-react",
"@benchmarks/ssr-serialization-react",
"@benchmarks/ssr-selective-ssr-react",
"@benchmarks/ssr-server-fn-transport-react",
"@benchmarks/ssr-server-fns-react",
"@benchmarks/ssr-server-routes-react",
"@benchmarks/ssr-server-routes-middleware-react",
"@benchmarks/ssr-streaming-react"
],
"target": "build"
"target": "build:ssr"
}
]
},
"build:solid": {
"executor": "nx:noop",
"cache": false,
"dependsOn": [
{
"projects": [
"@tanstack/solid-start"
"@benchmarks/ssr-solid",
"@benchmarks/ssr-assets-solid",
"@benchmarks/ssr-before-load-solid",
"@benchmarks/ssr-control-flow-solid",
"@benchmarks/ssr-global-middleware-solid",
"@benchmarks/ssr-head-solid",
"@benchmarks/ssr-loaders-solid",
"@benchmarks/ssr-rewrites-solid",
"@benchmarks/ssr-serialization-solid",
"@benchmarks/ssr-selective-ssr-solid",
"@benchmarks/ssr-server-fn-transport-solid",
"@benchmarks/ssr-server-fns-solid",
"@benchmarks/ssr-server-routes-solid",
"@benchmarks/ssr-server-routes-middleware-solid",
"@benchmarks/ssr-streaming-solid"
],
"target": "build"
"target": "build:ssr"
}
]
},
"build:vue": {
"executor": "nx:noop",
"cache": false,
"dependsOn": [
{
"projects": [
"@tanstack/vue-start"
"@benchmarks/ssr-vue",
"@benchmarks/ssr-assets-vue",
"@benchmarks/ssr-before-load-vue",
"@benchmarks/ssr-control-flow-vue",
"@benchmarks/ssr-global-middleware-vue",
"@benchmarks/ssr-head-vue",
"@benchmarks/ssr-loaders-vue",
"@benchmarks/ssr-rewrites-vue",
"@benchmarks/ssr-serialization-vue",
"@benchmarks/ssr-selective-ssr-vue",
"@benchmarks/ssr-server-fn-transport-vue",
"@benchmarks/ssr-server-fns-vue",
"@benchmarks/ssr-server-routes-vue",
"@benchmarks/ssr-server-routes-middleware-vue",
"@benchmarks/ssr-streaming-vue"
],
"target": "build"
"target": "build:ssr"
}
]
},
"test:perf": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": [
"build:react",
"build:solid",
"build:vue"
]
],
"options": {
"command": "NODE_ENV=production vitest bench",
"cwd": "benchmarks/ssr"
}
},
"test:perf:react": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": [
"build:react"
]
],
"options": {
"command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts",
"cwd": "benchmarks/ssr"
}
},
"test:perf:solid": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": [
"build:solid"
]
],
"options": {
"command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts",
"cwd": "benchmarks/ssr"
}
},
"test:perf:vue": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": [
"build:vue"
]
],
"options": {
"command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts",
"cwd": "benchmarks/ssr"
}
},
"test:types": {
"cache": false,
"executor": "nx:noop",
"dependsOn": [
"^build"
{
"projects": [
"@benchmarks/ssr-react",
"@benchmarks/ssr-assets-react",
"@benchmarks/ssr-before-load-react",
"@benchmarks/ssr-control-flow-react",
"@benchmarks/ssr-global-middleware-react",
"@benchmarks/ssr-head-react",
"@benchmarks/ssr-loaders-react",
"@benchmarks/ssr-rewrites-react",
"@benchmarks/ssr-serialization-react",
"@benchmarks/ssr-selective-ssr-react",
"@benchmarks/ssr-server-fn-transport-react",
"@benchmarks/ssr-server-fns-react",
"@benchmarks/ssr-server-routes-react",
"@benchmarks/ssr-server-routes-middleware-react",
"@benchmarks/ssr-streaming-react",
"@benchmarks/ssr-solid",
"@benchmarks/ssr-assets-solid",
"@benchmarks/ssr-before-load-solid",
"@benchmarks/ssr-control-flow-solid",
"@benchmarks/ssr-global-middleware-solid",
"@benchmarks/ssr-head-solid",
"@benchmarks/ssr-loaders-solid",
"@benchmarks/ssr-rewrites-solid",
"@benchmarks/ssr-serialization-solid",
"@benchmarks/ssr-selective-ssr-solid",
"@benchmarks/ssr-server-fn-transport-solid",
"@benchmarks/ssr-server-fns-solid",
"@benchmarks/ssr-server-routes-solid",
"@benchmarks/ssr-server-routes-middleware-solid",
"@benchmarks/ssr-streaming-solid",
"@benchmarks/ssr-vue",
"@benchmarks/ssr-assets-vue",
"@benchmarks/ssr-before-load-vue",
"@benchmarks/ssr-control-flow-vue",
"@benchmarks/ssr-global-middleware-vue",
"@benchmarks/ssr-head-vue",
"@benchmarks/ssr-loaders-vue",
"@benchmarks/ssr-rewrites-vue",
"@benchmarks/ssr-serialization-vue",
"@benchmarks/ssr-selective-ssr-vue",
"@benchmarks/ssr-server-fn-transport-vue",
"@benchmarks/ssr-server-fns-vue",
"@benchmarks/ssr-server-routes-vue",
"@benchmarks/ssr-server-routes-middleware-vue",
"@benchmarks/ssr-streaming-vue"
],
"target": "test:types:ssr"
}
]
}
}
Expand Down
Loading
Loading