Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
73 changes: 65 additions & 8 deletions benchmarks/ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,85 @@ 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. |
| `control-flow` | Loader-thrown `redirect` and `notFound` handling, including HTTP status selection, redirect `location` headers, and not-found HTML rendering. |
| `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. |
| `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-fns` | `createServerFn` GET and POST request handling, function middleware context, input validation, serialized payload decoding/encoding, CSRF-compatible request headers, and server-function 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`.
48 changes: 48 additions & 0 deletions benchmarks/ssr/bench-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export interface RunSsrRequestLoopOptions {
iterations?: number
}

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

const requestInit = {
method: 'GET',
headers: {
Expand All @@ -27,6 +34,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 +74,42 @@ export async function runSsrRequestLoop(

await Promise.all(pendingBodyReads)
}

export async function runRequestLoop(
handler: StartRequestHandler,
{
seed,
iterations = 10,
buildRequest,
validateResponse,
}: 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(() => undefined))
}

await Promise.all(pendingBodyReads)
Comment thread
Sheraff marked this conversation as resolved.
}
115 changes: 91 additions & 24 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,147 @@
"nx": {
"targets": {
"build:react": {
"executor": "nx:noop",
"cache": false,
"dependsOn": [
{
"projects": [
"@tanstack/react-start"
"@benchmarks/ssr-react",
"@benchmarks/ssr-control-flow-react",
"@benchmarks/ssr-head-react",
"@benchmarks/ssr-loaders-react",
"@benchmarks/ssr-selective-ssr-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-control-flow-solid",
"@benchmarks/ssr-head-solid",
"@benchmarks/ssr-loaders-solid",
"@benchmarks/ssr-selective-ssr-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-control-flow-vue",
"@benchmarks/ssr-head-vue",
"@benchmarks/ssr-loaders-vue",
"@benchmarks/ssr-selective-ssr-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": {
"executor": "nx:noop",
"cache": false,
"dependsOn": [
"^build"
{
"projects": [
"@benchmarks/ssr-react",
"@benchmarks/ssr-control-flow-react",
"@benchmarks/ssr-head-react",
"@benchmarks/ssr-loaders-react",
"@benchmarks/ssr-selective-ssr-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-control-flow-solid",
"@benchmarks/ssr-head-solid",
"@benchmarks/ssr-loaders-solid",
"@benchmarks/ssr-selective-ssr-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-control-flow-vue",
"@benchmarks/ssr-head-vue",
"@benchmarks/ssr-loaders-vue",
"@benchmarks/ssr-selective-ssr-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
26 changes: 26 additions & 0 deletions benchmarks/ssr/react/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@benchmarks/ssr-react",
"projectType": "application",
"targets": {
"build:ssr": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": [
{
"projects": ["@tanstack/react-start"],
"target": "build"
}
],
"options": {
"command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts"
}
},
"test:types:ssr": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "tsc -p {projectRoot}/tsconfig.json --noEmit"
}
}
}
}
Loading
Loading