Skip to content
Merged
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
26 changes: 26 additions & 0 deletions packages/app-astro/src/pages/ssr-throughput.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
import { testData } from '../../../testdata/src/ssr'
const entries = await testData()
---

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro SSR Throughput Benchmark</title>
</head>
<body>
<table>
<tbody>
{
entries.map((entry) => (
<tr>
<td>{entry.id}</td>
<td>{entry.name}</td>
</tr>
))
}
</tbody>
</table>
</body>
</html>
8 changes: 8 additions & 0 deletions packages/app-next-js/app/ssr-throughput/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { testData } from '../../../testdata/src/ssr'
import { Table } from './table'

export const dynamic = 'force-dynamic'

export default async function SSRThroughputPage() {
return <Table data={await testData()} />
}
17 changes: 17 additions & 0 deletions packages/app-next-js/app/ssr-throughput/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'
import type { TableEntry } from '../../../testdata/src/ssr'

export function Table({ data }: { data: TableEntry[] }) {
return (
<table>
<tbody>
{data.map((entry) => (
<tr key={entry.id}>
<td>{entry.id}</td>
<td>{entry.name}</td>
</tr>
))}
</tbody>
</table>
)
}
16 changes: 16 additions & 0 deletions packages/app-nuxt/app/pages/ssr-throughput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { testData } from '../../../testdata/src/ssr'

const { data } = await useAsyncData(() => testData(), { deep: false })
</script>

<template>
<table>
<tbody>
<tr v-for="entry in data" :key="entry.id">
<td>{{ entry.id }}</td>
<td>{{ entry.name }}</td>
</tr>
</tbody>
</table>
</template>
1 change: 1 addition & 0 deletions packages/app-react-router/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export default [
route('/client-side-rendered/:id', 'routes/client-side-rendered.detail.tsx'),
route('/server-side-rendered', 'routes/server-side-rendered.tsx'),
route('/server-side-rendered/:id', 'routes/server-side-rendered.detail.tsx'),
route('/ssr-throughput', 'routes/ssr-throughput.tsx'),
] satisfies RouteConfig
24 changes: 24 additions & 0 deletions packages/app-react-router/app/routes/ssr-throughput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { testData } from '../../../testdata/src/ssr'
import type { Route } from './+types/ssr-throughput'

export async function loader() {
const data = await testData()
return { data }
}

export default function SSRThroughputPage({
loaderData,
}: Route.ComponentProps) {
return (
<table>
<tbody>
{loaderData.data.map((entry) => (
<tr key={entry.id}>
<td>{entry.id}</td>
<td>{entry.name}</td>
</tr>
))}
</tbody>
</table>
)
}
31 changes: 31 additions & 0 deletions packages/app-solid-start/src/routes/ssr-throughput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { For } from 'solid-js'
import { query, createAsync } from '@solidjs/router'
import { testData } from '../../../../testdata/src/ssr'

const getData = query(async () => {
'use server'
return await testData()
}, 'ssr-throughput-data')

export const route = {
preload: () => getData(),
}

export default function SSRThroughputPage() {
const data = createAsync(() => getData())

return (
<table>
<tbody>
<For each={data()}>
{(entry) => (
<tr>
<td>{entry.id}</td>
<td>{entry.name}</td>
</tr>
)}
</For>
</tbody>
</table>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { testData } from '../../../../testdata/src/ssr'

export const load = async () => {
return { entries: await testData() }
}
16 changes: 16 additions & 0 deletions packages/app-sveltekit/src/routes/ssr-throughput/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import type { PageData } from './$types'

let { data }: { data: PageData } = $props()
</script>

<table>
<tbody>
{#each data.entries as entry}
<tr>
<td>{entry.id}</td>
<td>{entry.name}</td>
</tr>
{/each}
</tbody>
</table>
21 changes: 21 additions & 0 deletions packages/app-tanstack-start-react/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as SsrThroughputRouteImport } from './routes/ssr-throughput'
import { Route as ServerSideRenderedRouteImport } from './routes/server-side-rendered'
import { Route as ClientSideRenderedRouteImport } from './routes/client-side-rendered'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ServerSideRenderedIdRouteImport } from './routes/server-side-rendered_.$id'
import { Route as ClientSideRenderedIdRouteImport } from './routes/client-side-rendered_.$id'

const SsrThroughputRoute = SsrThroughputRouteImport.update({
id: '/ssr-throughput',
path: '/ssr-throughput',
getParentRoute: () => rootRouteImport,
} as any)
const ServerSideRenderedRoute = ServerSideRenderedRouteImport.update({
id: '/server-side-rendered',
path: '/server-side-rendered',
Expand Down Expand Up @@ -45,13 +51,15 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/client-side-rendered': typeof ClientSideRenderedRoute
'/server-side-rendered': typeof ServerSideRenderedRoute
'/ssr-throughput': typeof SsrThroughputRoute
'/client-side-rendered/$id': typeof ClientSideRenderedIdRoute
'/server-side-rendered/$id': typeof ServerSideRenderedIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/client-side-rendered': typeof ClientSideRenderedRoute
'/server-side-rendered': typeof ServerSideRenderedRoute
'/ssr-throughput': typeof SsrThroughputRoute
'/client-side-rendered/$id': typeof ClientSideRenderedIdRoute
'/server-side-rendered/$id': typeof ServerSideRenderedIdRoute
}
Expand All @@ -60,6 +68,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/client-side-rendered': typeof ClientSideRenderedRoute
'/server-side-rendered': typeof ServerSideRenderedRoute
'/ssr-throughput': typeof SsrThroughputRoute
'/client-side-rendered_/$id': typeof ClientSideRenderedIdRoute
'/server-side-rendered_/$id': typeof ServerSideRenderedIdRoute
}
Expand All @@ -69,20 +78,23 @@ export interface FileRouteTypes {
| '/'
| '/client-side-rendered'
| '/server-side-rendered'
| '/ssr-throughput'
| '/client-side-rendered/$id'
| '/server-side-rendered/$id'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/client-side-rendered'
| '/server-side-rendered'
| '/ssr-throughput'
| '/client-side-rendered/$id'
| '/server-side-rendered/$id'
id:
| '__root__'
| '/'
| '/client-side-rendered'
| '/server-side-rendered'
| '/ssr-throughput'
| '/client-side-rendered_/$id'
| '/server-side-rendered_/$id'
fileRoutesById: FileRoutesById
Expand All @@ -91,12 +103,20 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ClientSideRenderedRoute: typeof ClientSideRenderedRoute
ServerSideRenderedRoute: typeof ServerSideRenderedRoute
SsrThroughputRoute: typeof SsrThroughputRoute
ClientSideRenderedIdRoute: typeof ClientSideRenderedIdRoute
ServerSideRenderedIdRoute: typeof ServerSideRenderedIdRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/ssr-throughput': {
id: '/ssr-throughput'
path: '/ssr-throughput'
fullPath: '/ssr-throughput'
preLoaderRoute: typeof SsrThroughputRouteImport
parentRoute: typeof rootRouteImport
}
'/server-side-rendered': {
id: '/server-side-rendered'
path: '/server-side-rendered'
Expand Down Expand Up @@ -139,6 +159,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ClientSideRenderedRoute: ClientSideRenderedRoute,
ServerSideRenderedRoute: ServerSideRenderedRoute,
SsrThroughputRoute: SsrThroughputRoute,
ClientSideRenderedIdRoute: ClientSideRenderedIdRoute,
ServerSideRenderedIdRoute: ServerSideRenderedIdRoute,
}
Expand Down
24 changes: 24 additions & 0 deletions packages/app-tanstack-start-react/src/routes/ssr-throughput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createFileRoute } from '@tanstack/react-router'
import { testData } from '../../../testdata/src/ssr'

export const Route = createFileRoute('/ssr-throughput')({
component: SSRThroughputPage,
loader: async () => await testData(),
})

function SSRThroughputPage() {
const data = Route.useLoaderData()

return (
<table>
<tbody>
{data.map((entry) => (
<tr key={entry.id}>
<td>{entry.id}</td>
<td>{entry.name}</td>
</tr>
))}
</tbody>
</table>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ import MethodologyNotes from '../components/MethodologyNotes.astro'
---

<MethodologyNotes>
<li>Each framework renders a table of 1000 rows with two UUID columns</li>
<li>
Mock HTTP requests bypass TCP overhead for accurate rendering measurement
Each framework renders the dedicated <code>/ssr-throughput</code> route with a
table of 1000 rows and UUID id/name columns
</li>
<li>
This route intentionally does not render the exact same table as the browser
SSR and load tests: it omits detail links and framework link components so
router, prefetch, and navigation metadata do not dominate the
request-handler throughput measurement
</li>
<li>
Mock HTTP requests bypass TCP overhead so this measures request-handler
rendering throughput rather than full server throughput
</li>
<li>Data is loaded asynchronously to simulate real-world data fetching</li>
<li>
Expand All @@ -20,17 +30,17 @@ import MethodologyNotes from '../components/MethodologyNotes.astro'
>
</li>
<li>
Astro, Nuxt, and SvelteKit handle Node.js HTTP requests natively. React
Router, SolidStart, and TanStack Start use Web APIs internally, so
benchmarks include the cost of their Node.js adapter layers (<code
>@react-router/node</code
>, h3, and srvx respectively)
Frameworks are invoked through their production request handlers where
possible. Web API handlers are called with <code>Request</code> objects; Node.js
handlers are called with mock <code>IncomingMessage</code> and <code
>ServerResponse</code
> objects.
</li>
<li>
Next.js defaults to React Server Components (RSC), a different rendering
model than traditional server-rendered React. To keep the comparison fair,
Next.js uses <code>"use client"</code> to opt out of RSC and use traditional server
rendering + hydration like most of the other frameworks
Next.js renders the throughput table as a client component, matching the
setup from PR #94, so the benchmark compares traditional server-rendered
React + hydration work instead of making Next.js render every table row as
React Server Components
</li>
<li>
Inspired by <a
Expand Down
8 changes: 8 additions & 0 deletions packages/stats-generator/src/baseline-html.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { TableEntry } from '../../testdata/src/ssr.ts'

export function renderSSRThroughputHtml(entries: TableEntry[]): string {
const rows = entries
.map((entry) => `<tr><td>${entry.id}</td><td>${entry.name}</td></tr>`)
.join('')

return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Baseline HTML SSR Throughput</title></head><body><table><tbody>${rows}</tbody></table></body></html>`
}

export function renderBaselineHtml(entries: TableEntry[]): string {
const rows = entries
.map(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { testData } from '../../../../testdata/src/ssr.ts'
import { renderBaselineHtml } from '../../baseline-html.ts'
import { renderSSRThroughputHtml } from '../../baseline-html.ts'
import type { NodeServerRenderHandler, ServerRenderHandler } from '../types.ts'

export async function buildBaselineHtmlHandler(): Promise<ServerRenderHandler> {
const handler: NodeServerRenderHandler = async (_req, res) => {
const entries = await testData()
const html = renderBaselineHtml(entries)
const html = renderSSRThroughputHtml(entries)

res.writeHead(200)
res.end(html)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ export async function importWithCapturedServer<T>(
let server: HttpServer | HttpsServer | undefined

const listen: typeof HttpServer.prototype.listen = function (
this: HttpServer,
this: HttpServer | HttpsServer,
) {
server = this
// Do not invoke listen callbacks here; no socket is bound, so server.address() is null.
return this
} as typeof HttpServer.prototype.listen

Expand Down
Loading
Loading