diff --git a/benchmarks/ssr/README.md b/benchmarks/ssr/README.md index 9fb429ecfe0..625e1006c5b 100644 --- a/benchmarks/ssr/README.md +++ b/benchmarks/ssr/README.md @@ -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///` - isolated scenario apps + +Scenario app layout: + +```text +scenarios/// + 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--:build:ssr --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/ssr--:test:types:ssr --outputStyle=stream --skipRemoteCache +``` + +Use `react`, `solid`, or `vue` for ``. The baseline projects use `@benchmarks/ssr-` 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`. diff --git a/benchmarks/ssr/bench-utils.ts b/benchmarks/ssr/bench-utils.ts index 14f8a56ddb5..03b52361837 100644 --- a/benchmarks/ssr/bench-utils.ts +++ b/benchmarks/ssr/bench-utils.ts @@ -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: { @@ -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)}` } @@ -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> = [] + 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) +} diff --git a/benchmarks/ssr/package.json b/benchmarks/ssr/package.json index 747cd37d8d4..40cd97df5e9 100644 --- a/benchmarks/ssr/package.json +++ b/benchmarks/ssr/package.json @@ -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:^", @@ -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", @@ -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" + } ] } } diff --git a/benchmarks/ssr/react/project.json b/benchmarks/ssr/react/project.json new file mode 100644 index 00000000000..d7f674cc833 --- /dev/null +++ b/benchmarks/ssr/react/project.json @@ -0,0 +1,31 @@ +{ + "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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/assets/react/project.json b/benchmarks/ssr/scenarios/assets/react/project.json new file mode 100644 index 00000000000..a2d5a6c8bae --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-assets-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/assets/react/speed.bench.ts b/benchmarks/ssr/scenarios/assets/react/speed.bench.ts new file mode 100644 index 00000000000..a54042cd868 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/speed.bench.ts @@ -0,0 +1,31 @@ +import { bench, describe } from 'vitest' +import { + assertAssetsScenario, + assetsBenchOptions, + runAssetsInlineLoop, + runAssetsLinkedControlLoop, + type StartRequestHandler, +} from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertAssetsScenario(handler) + +describe('ssr', () => { + bench( + 'ssr assets inline-css cdn (react)', + () => runAssetsInlineLoop(handler), + assetsBenchOptions, + ) + bench( + 'ssr assets linked-css control (react)', + () => runAssetsLinkedControlLoop(handler), + assetsBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/assets/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/assets/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..201df5fd3bf --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/routeTree.gen.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 AXRouteImport } from './routes/a.$x' +import { Route as AXYRouteImport } from './routes/a.$x.$y' + +const AXRoute = AXRouteImport.update({ + id: '/a/$x', + path: '/a/$x', + getParentRoute: () => rootRouteImport, +} as any) +const AXYRoute = AXYRouteImport.update({ + id: '/$y', + path: '/$y', + getParentRoute: () => AXRoute, +} as any) + +export interface FileRoutesByFullPath { + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRoutesByTo { + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a/$x' | '/a/$x/$y' + fileRoutesByTo: FileRoutesByTo + to: '/a/$x' | '/a/$x/$y' + id: '__root__' | '/a/$x' | '/a/$x/$y' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + AXRoute: typeof AXRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/a/$x': { + id: '/a/$x' + path: '/a/$x' + fullPath: '/a/$x' + preLoaderRoute: typeof AXRouteImport + parentRoute: typeof rootRouteImport + } + '/a/$x/$y': { + id: '/a/$x/$y' + path: '/$y' + fullPath: '/a/$x/$y' + preLoaderRoute: typeof AXYRouteImport + parentRoute: typeof AXRoute + } + } +} + +interface AXRouteChildren { + AXYRoute: typeof AXYRoute +} + +const AXRouteChildren: AXRouteChildren = { + AXYRoute: AXYRoute, +} + +const AXRouteWithChildren = AXRoute._addFileChildren(AXRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + AXRoute: AXRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/assets/react/src/router.tsx b/benchmarks/ssr/scenarios/assets/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/assets/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/assets/react/src/routes/__root.tsx new file mode 100644 index 00000000000..8c46c7060a9 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/routes/__root.tsx @@ -0,0 +1,31 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: 'SSR assets benchmark' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/assets/react/src/routes/a.$x.$y.tsx b/benchmarks/ssr/scenarios/assets/react/src/routes/a.$x.$y.tsx new file mode 100644 index 00000000000..be709e53e69 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/routes/a.$x.$y.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import '../styles/assets-leaf.css' + +export const Route = createFileRoute('/a/$x/$y')({ + head: ({ params }) => ({ + meta: [{ title: `SSR Assets ${params.x} ${params.y}` }], + links: Array.from({ length: 3 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/asset-preload/${params.y}-${index}.png`, + })), + }), + component: LeafComponent, +}) + +function LeafComponent() { + const { x, y } = Route.useParams() + + return ( +
+

+ assets-leaf-{x}-{y} +

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/assets/react/src/routes/a.$x.tsx b/benchmarks/ssr/scenarios/assets/react/src/routes/a.$x.tsx new file mode 100644 index 00000000000..ecc7de680a7 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/routes/a.$x.tsx @@ -0,0 +1,25 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import '../styles/assets-a.css' + +export const Route = createFileRoute('/a/$x')({ + head: ({ params }) => ({ + meta: [{ title: `SSR Assets ${params.x}` }], + links: Array.from({ length: 2 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/asset-preload/${params.x}-${index}.png`, + })), + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + const { x } = Route.useParams() + + return ( +
+

assets-level-a-{x}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/assets/react/src/server.ts b/benchmarks/ssr/scenarios/assets/react/src/server.ts new file mode 100644 index 00000000000..64fb21708cc --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/server.ts @@ -0,0 +1,45 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/react-start/server' +import { createServerEntry } from '@tanstack/react-start/server-entry' +import type { + TransformAssets, + TransformAssetsFn, +} from '@tanstack/react-start/server' + +type CreateTransformContext = + | { warmup: true } + | { request: Request; warmup: false } + +const cdnOrigin = 'https://cdn.example.com' + +const createCdnTransform = (prefix: string): TransformAssetsFn => { + return ({ url }) => `${prefix}${url}` +} + +const transformAssets: TransformAssets = { + createTransform: (ctx: CreateTransformContext) => { + const prefix = ctx.warmup + ? cdnOrigin + : (ctx.request.headers.get('x-assets-cdn') ?? cdnOrigin) + + return createCdnTransform(prefix) + }, + cache: false, +} + +const handler = createStartHandler({ + handler: defaultStreamHandler, + inlineCss: true, + transformAssets, +}) + +export default createServerEntry({ + fetch(request) { + return handler(request, { + inlineCss: request.headers.get('x-inline-css') !== 'false', + responseLinkHeader: true, + }) + }, +}) diff --git a/benchmarks/ssr/scenarios/assets/react/src/styles/assets-a.css b/benchmarks/ssr/scenarios/assets/react/src/styles/assets-a.css new file mode 100644 index 00000000000..d3511ddd859 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/styles/assets-a.css @@ -0,0 +1,8 @@ +.assets-level-a { + color: #1f2937; + background-image: url('/asset-bg-a.svg'); +} + +.assets-level-a::before { + content: 'assets-level-a-css'; +} diff --git a/benchmarks/ssr/scenarios/assets/react/src/styles/assets-leaf.css b/benchmarks/ssr/scenarios/assets/react/src/styles/assets-leaf.css new file mode 100644 index 00000000000..55e051b9a58 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/src/styles/assets-leaf.css @@ -0,0 +1,8 @@ +.assets-leaf { + border-color: #2563eb; + background-image: url('/asset-bg-leaf.svg'); +} + +.assets-leaf::before { + content: 'assets-leaf-css'; +} diff --git a/benchmarks/ssr/scenarios/assets/react/tsconfig.json b/benchmarks/ssr/scenarios/assets/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/assets/react/vite.config.ts b/benchmarks/ssr/scenarios/assets/react/vite.config.ts new file mode 100644 index 00000000000..91534d3de87 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + server: { + build: { + inlineCss: { enabled: true, transformAssets: true }, + }, + }, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr assets (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/assets/shared.ts b/benchmarks/ssr/scenarios/assets/shared.ts new file mode 100644 index 00000000000..00edb95e376 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/shared.ts @@ -0,0 +1,82 @@ +import { expect } from 'vitest' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const origin = 'http://localhost' +const cdnOrigin = 'https://cdn.example.com' + +const inlineRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const linkedRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + 'x-inline-css': 'false', + }, +} satisfies RequestInit + +export const assetsBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +function buildAssetsRequest(random: () => number, requestInit: RequestInit) { + return new Request( + `${origin}/a/${randomSegment(random)}/${randomSegment(random)}`, + requestInit, + ) +} + +export function runAssetsInlineLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: (random) => buildAssetsRequest(random, inlineRequestInit), + }) +} + +export function runAssetsLinkedControlLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: (random) => buildAssetsRequest(random, linkedRequestInit), + }) +} + +export async function assertAssetsScenario(handler: StartRequestHandler) { + const inlineResponse = await handler.fetch( + new Request(`${origin}/a/sanity-x/sanity-y`, inlineRequestInit), + ) + const inlineBody = await inlineResponse.text() + const inlineLinkHeader = inlineResponse.headers.get('link') ?? '' + + expect(inlineResponse.status).toBe(200) + expect(inlineBody).toContain('SSR Assets sanity-x sanity-y') + expect(inlineBody).toContain(' { + bench( + 'ssr assets inline-css cdn (solid)', + () => runAssetsInlineLoop(handler), + assetsBenchOptions, + ) + bench( + 'ssr assets linked-css control (solid)', + () => runAssetsLinkedControlLoop(handler), + assetsBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/assets/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/assets/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..f6f7ffe57eb --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/routeTree.gen.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 AXRouteImport } from './routes/a.$x' +import { Route as AXYRouteImport } from './routes/a.$x.$y' + +const AXRoute = AXRouteImport.update({ + id: '/a/$x', + path: '/a/$x', + getParentRoute: () => rootRouteImport, +} as any) +const AXYRoute = AXYRouteImport.update({ + id: '/$y', + path: '/$y', + getParentRoute: () => AXRoute, +} as any) + +export interface FileRoutesByFullPath { + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRoutesByTo { + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a/$x' | '/a/$x/$y' + fileRoutesByTo: FileRoutesByTo + to: '/a/$x' | '/a/$x/$y' + id: '__root__' | '/a/$x' | '/a/$x/$y' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + AXRoute: typeof AXRouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/a/$x': { + id: '/a/$x' + path: '/a/$x' + fullPath: '/a/$x' + preLoaderRoute: typeof AXRouteImport + parentRoute: typeof rootRouteImport + } + '/a/$x/$y': { + id: '/a/$x/$y' + path: '/$y' + fullPath: '/a/$x/$y' + preLoaderRoute: typeof AXYRouteImport + parentRoute: typeof AXRoute + } + } +} + +interface AXRouteChildren { + AXYRoute: typeof AXYRoute +} + +const AXRouteChildren: AXRouteChildren = { + AXYRoute: AXYRoute, +} + +const AXRouteWithChildren = AXRoute._addFileChildren(AXRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + AXRoute: AXRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/assets/solid/src/router.tsx b/benchmarks/ssr/scenarios/assets/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/assets/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/assets/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..ca760406ce5 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/routes/__root.tsx @@ -0,0 +1,31 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: 'SSR assets benchmark' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/assets/solid/src/routes/a.$x.$y.tsx b/benchmarks/ssr/scenarios/assets/solid/src/routes/a.$x.$y.tsx new file mode 100644 index 00000000000..2a3d35cb68c --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/routes/a.$x.$y.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' +import '../styles/assets-leaf.css' + +export const Route = createFileRoute('/a/$x/$y')({ + head: ({ params }) => ({ + meta: [{ title: `SSR Assets ${params.x} ${params.y}` }], + links: Array.from({ length: 3 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/asset-preload/${params.y}-${index}.png`, + })), + }), + component: LeafComponent, +}) + +function LeafComponent() { + const params = Route.useParams() + + return ( +
+

+ assets-leaf-{params().x}-{params().y} +

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/assets/solid/src/routes/a.$x.tsx b/benchmarks/ssr/scenarios/assets/solid/src/routes/a.$x.tsx new file mode 100644 index 00000000000..2209addcbeb --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/routes/a.$x.tsx @@ -0,0 +1,25 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import '../styles/assets-a.css' + +export const Route = createFileRoute('/a/$x')({ + head: ({ params }) => ({ + meta: [{ title: `SSR Assets ${params.x}` }], + links: Array.from({ length: 2 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/asset-preload/${params.x}-${index}.png`, + })), + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + const params = Route.useParams() + + return ( +
+

assets-level-a-{params().x}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/assets/solid/src/server.ts b/benchmarks/ssr/scenarios/assets/solid/src/server.ts new file mode 100644 index 00000000000..541e776a794 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/server.ts @@ -0,0 +1,45 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/solid-start/server' +import { createServerEntry } from '@tanstack/solid-start/server-entry' +import type { + TransformAssets, + TransformAssetsFn, +} from '@tanstack/solid-start/server' + +type CreateTransformContext = + | { warmup: true } + | { request: Request; warmup: false } + +const cdnOrigin = 'https://cdn.example.com' + +const createCdnTransform = (prefix: string): TransformAssetsFn => { + return ({ url }) => `${prefix}${url}` +} + +const transformAssets: TransformAssets = { + createTransform: (ctx: CreateTransformContext) => { + const prefix = ctx.warmup + ? cdnOrigin + : (ctx.request.headers.get('x-assets-cdn') ?? cdnOrigin) + + return createCdnTransform(prefix) + }, + cache: false, +} + +const handler = createStartHandler({ + handler: defaultStreamHandler, + inlineCss: true, + transformAssets, +}) + +export default createServerEntry({ + fetch(request) { + return handler(request, { + inlineCss: request.headers.get('x-inline-css') !== 'false', + responseLinkHeader: true, + }) + }, +}) diff --git a/benchmarks/ssr/scenarios/assets/solid/src/styles/assets-a.css b/benchmarks/ssr/scenarios/assets/solid/src/styles/assets-a.css new file mode 100644 index 00000000000..d3511ddd859 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/styles/assets-a.css @@ -0,0 +1,8 @@ +.assets-level-a { + color: #1f2937; + background-image: url('/asset-bg-a.svg'); +} + +.assets-level-a::before { + content: 'assets-level-a-css'; +} diff --git a/benchmarks/ssr/scenarios/assets/solid/src/styles/assets-leaf.css b/benchmarks/ssr/scenarios/assets/solid/src/styles/assets-leaf.css new file mode 100644 index 00000000000..55e051b9a58 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/src/styles/assets-leaf.css @@ -0,0 +1,8 @@ +.assets-leaf { + border-color: #2563eb; + background-image: url('/asset-bg-leaf.svg'); +} + +.assets-leaf::before { + content: 'assets-leaf-css'; +} diff --git a/benchmarks/ssr/scenarios/assets/solid/tsconfig.json b/benchmarks/ssr/scenarios/assets/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/assets/solid/vite.config.ts b/benchmarks/ssr/scenarios/assets/solid/vite.config.ts new file mode 100644 index 00000000000..788ac1f5b85 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/solid/vite.config.ts @@ -0,0 +1,39 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + server: { + build: { + inlineCss: { enabled: true, transformAssets: true }, + }, + }, + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr assets (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/assets/vue/project.json b/benchmarks/ssr/scenarios/assets/vue/project.json new file mode 100644 index 00000000000..9667ff902c2 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-assets-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/assets/vue/speed.bench.ts b/benchmarks/ssr/scenarios/assets/vue/speed.bench.ts new file mode 100644 index 00000000000..01eb3b9d5a7 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/speed.bench.ts @@ -0,0 +1,31 @@ +import { bench, describe } from 'vitest' +import { + assertAssetsScenario, + assetsBenchOptions, + runAssetsInlineLoop, + runAssetsLinkedControlLoop, + type StartRequestHandler, +} from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertAssetsScenario(handler) + +describe('ssr', () => { + bench( + 'ssr assets inline-css cdn (vue)', + () => runAssetsInlineLoop(handler), + assetsBenchOptions, + ) + bench( + 'ssr assets linked-css control (vue)', + () => runAssetsLinkedControlLoop(handler), + assetsBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/assets/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/assets/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..2a6eb91f507 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/routeTree.gen.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 AXRouteImport } from './routes/a.$x' +import { Route as AXYRouteImport } from './routes/a.$x.$y' + +const AXRoute = AXRouteImport.update({ + id: '/a/$x', + path: '/a/$x', + getParentRoute: () => rootRouteImport, +} as any) +const AXYRoute = AXYRouteImport.update({ + id: '/$y', + path: '/$y', + getParentRoute: () => AXRoute, +} as any) + +export interface FileRoutesByFullPath { + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRoutesByTo { + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a/$x': typeof AXRouteWithChildren + '/a/$x/$y': typeof AXYRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a/$x' | '/a/$x/$y' + fileRoutesByTo: FileRoutesByTo + to: '/a/$x' | '/a/$x/$y' + id: '__root__' | '/a/$x' | '/a/$x/$y' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + AXRoute: typeof AXRouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/a/$x': { + id: '/a/$x' + path: '/a/$x' + fullPath: '/a/$x' + preLoaderRoute: typeof AXRouteImport + parentRoute: typeof rootRouteImport + } + '/a/$x/$y': { + id: '/a/$x/$y' + path: '/$y' + fullPath: '/a/$x/$y' + preLoaderRoute: typeof AXYRouteImport + parentRoute: typeof AXRoute + } + } +} + +interface AXRouteChildren { + AXYRoute: typeof AXYRoute +} + +const AXRouteChildren: AXRouteChildren = { + AXYRoute: AXYRoute, +} + +const AXRouteWithChildren = AXRoute._addFileChildren(AXRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + AXRoute: AXRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/assets/vue/src/router.tsx b/benchmarks/ssr/scenarios/assets/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/assets/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/assets/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..760545d2488 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/routes/__root.tsx @@ -0,0 +1,33 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: 'SSR assets benchmark' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/assets/vue/src/routes/a.$x.$y.tsx b/benchmarks/ssr/scenarios/assets/vue/src/routes/a.$x.$y.tsx new file mode 100644 index 00000000000..a20e292938b --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/routes/a.$x.$y.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/vue-router' +import '../styles/assets-leaf.css' + +export const Route = createFileRoute('/a/$x/$y')({ + head: ({ params }) => ({ + meta: [{ title: `SSR Assets ${params.x} ${params.y}` }], + links: Array.from({ length: 3 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/asset-preload/${params.y}-${index}.png`, + })), + }), + component: LeafComponent, +}) + +function LeafComponent() { + const params = Route.useParams() + + return ( +
+

+ assets-leaf-{params.value.x}-{params.value.y} +

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/assets/vue/src/routes/a.$x.tsx b/benchmarks/ssr/scenarios/assets/vue/src/routes/a.$x.tsx new file mode 100644 index 00000000000..f8038f6f284 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/routes/a.$x.tsx @@ -0,0 +1,25 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import '../styles/assets-a.css' + +export const Route = createFileRoute('/a/$x')({ + head: ({ params }) => ({ + meta: [{ title: `SSR Assets ${params.x}` }], + links: Array.from({ length: 2 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/asset-preload/${params.x}-${index}.png`, + })), + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + const params = Route.useParams() + + return ( +
+

assets-level-a-{params.value.x}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/assets/vue/src/server.ts b/benchmarks/ssr/scenarios/assets/vue/src/server.ts new file mode 100644 index 00000000000..fd1e83d6b8d --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/server.ts @@ -0,0 +1,45 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/vue-start/server' +import { createServerEntry } from '@tanstack/vue-start/server-entry' +import type { + TransformAssets, + TransformAssetsFn, +} from '@tanstack/vue-start/server' + +type CreateTransformContext = + | { warmup: true } + | { request: Request; warmup: false } + +const cdnOrigin = 'https://cdn.example.com' + +const createCdnTransform = (prefix: string): TransformAssetsFn => { + return ({ url }) => `${prefix}${url}` +} + +const transformAssets: TransformAssets = { + createTransform: (ctx: CreateTransformContext) => { + const prefix = ctx.warmup + ? cdnOrigin + : (ctx.request.headers.get('x-assets-cdn') ?? cdnOrigin) + + return createCdnTransform(prefix) + }, + cache: false, +} + +const handler = createStartHandler({ + handler: defaultStreamHandler, + inlineCss: true, + transformAssets, +}) + +export default createServerEntry({ + fetch(request) { + return handler(request, { + inlineCss: request.headers.get('x-inline-css') !== 'false', + responseLinkHeader: true, + }) + }, +}) diff --git a/benchmarks/ssr/scenarios/assets/vue/src/styles/assets-a.css b/benchmarks/ssr/scenarios/assets/vue/src/styles/assets-a.css new file mode 100644 index 00000000000..d3511ddd859 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/styles/assets-a.css @@ -0,0 +1,8 @@ +.assets-level-a { + color: #1f2937; + background-image: url('/asset-bg-a.svg'); +} + +.assets-level-a::before { + content: 'assets-level-a-css'; +} diff --git a/benchmarks/ssr/scenarios/assets/vue/src/styles/assets-leaf.css b/benchmarks/ssr/scenarios/assets/vue/src/styles/assets-leaf.css new file mode 100644 index 00000000000..55e051b9a58 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/src/styles/assets-leaf.css @@ -0,0 +1,8 @@ +.assets-leaf { + border-color: #2563eb; + background-image: url('/asset-bg-leaf.svg'); +} + +.assets-leaf::before { + content: 'assets-leaf-css'; +} diff --git a/benchmarks/ssr/scenarios/assets/vue/tsconfig.json b/benchmarks/ssr/scenarios/assets/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/assets/vue/vite.config.ts b/benchmarks/ssr/scenarios/assets/vue/vite.config.ts new file mode 100644 index 00000000000..8beb38c972a --- /dev/null +++ b/benchmarks/ssr/scenarios/assets/vue/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + server: { + build: { + inlineCss: { enabled: true, transformAssets: true }, + }, + }, + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr assets (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/before-load/react/project.json b/benchmarks/ssr/scenarios/before-load/react/project.json new file mode 100644 index 00000000000..df2004f1f51 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-before-load-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/before-load/react/speed.bench.ts b/benchmarks/ssr/scenarios/before-load/react/speed.bench.ts new file mode 100644 index 00000000000..09073687c03 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertBeforeLoadScenario, + beforeLoadBenchOptions, + runBeforeLoadLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertBeforeLoadScenario(handler) + +describe('ssr', () => { + bench( + 'ssr before-load chain (react)', + () => runBeforeLoadLoop(handler), + beforeLoadBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/before-load/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/before-load/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..068b5a54335 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 ARouteImport } from './routes/$a' +import { Route as ABRouteImport } from './routes/$a.$b' +import { Route as ABCRouteImport } from './routes/$a.$b.$c' + +const ARoute = ARouteImport.update({ + id: '/$a', + path: '/$a', + getParentRoute: () => rootRouteImport, +} as any) +const ABRoute = ABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => ARoute, +} as any) +const ABCRoute = ABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => ABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/$a': { + id: '/$a' + path: '/$a' + fullPath: '/$a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + '/$a/$b': { + id: '/$a/$b' + path: '/$b' + fullPath: '/$a/$b' + preLoaderRoute: typeof ABRouteImport + parentRoute: typeof ARoute + } + '/$a/$b/$c': { + id: '/$a/$b/$c' + path: '/$c' + fullPath: '/$a/$b/$c' + preLoaderRoute: typeof ABCRouteImport + parentRoute: typeof ABRoute + } + } +} + +interface ABRouteChildren { + ABCRoute: typeof ABCRoute +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRoute, +} + +const ABRouteWithChildren = ABRoute._addFileChildren(ABRouteChildren) + +interface ARouteChildren { + ABRoute: typeof ABRouteWithChildren +} + +const ARouteChildren: ARouteChildren = { + ABRoute: ABRouteWithChildren, +} + +const ARouteWithChildren = ARoute._addFileChildren(ARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/before-load/react/src/router.tsx b/benchmarks/ssr/scenarios/before-load/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..71719a4f882 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.$b.$c.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makeBeforeLoadMarker, type BeforeLoadContext } from '../../../shared' + +export const Route = createFileRoute('/$a/$b/$c')({ + beforeLoad: ({ params, context }) => { + const parent = context as BeforeLoadContext + + return { + chainToken: `${parent.chainToken}.${params.c}`, + ctxC: params.c, + } + }, + loader: ({ context }) => ({ + marker: makeBeforeLoadMarker(context as BeforeLoadContext), + }), + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + + return
{data.marker}
+} diff --git a/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.$b.tsx b/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..1a148e05fac --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.$b.tsx @@ -0,0 +1,18 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import type { BeforeLoadContext } from '../../../shared' + +export const Route = createFileRoute('/$a/$b')({ + beforeLoad: ({ params, context }) => { + const parent = context as BeforeLoadContext + + return { + chainToken: `${parent.chainToken}.${params.b}`, + ctxB: params.b, + } + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + return +} diff --git a/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.tsx b/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.tsx new file mode 100644 index 00000000000..b5dcca33c36 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/src/routes/$a.tsx @@ -0,0 +1,13 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/$a')({ + beforeLoad: ({ params }) => ({ + chainToken: params.a, + ctxA: params.a, + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + return +} diff --git a/benchmarks/ssr/scenarios/before-load/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/before-load/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/before-load/react/tsconfig.json b/benchmarks/ssr/scenarios/before-load/react/tsconfig.json new file mode 100644 index 00000000000..232763bbe1d --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared.ts", + "../shared-bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/before-load/react/vite.config.ts b/benchmarks/ssr/scenarios/before-load/react/vite.config.ts new file mode 100644 index 00000000000..2767b857875 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr before-load (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/before-load/shared-bench.ts b/benchmarks/ssr/scenarios/before-load/shared-bench.ts new file mode 100644 index 00000000000..6d94ef666c9 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/shared-bench.ts @@ -0,0 +1,63 @@ +import { makeBeforeLoadMarker } from './shared' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const beforeLoadLoopIterations = 20 + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildBeforeLoadRequest(random: () => number, index: number) { + const suffix = index.toString(36) + const a = `${randomSegment(random)}-${suffix}` + const b = `${randomSegment(random)}-${suffix}` + const c = `${randomSegment(random)}-${suffix}` + + return new Request(`http://localhost/${a}/${b}/${c}`, requestInit) +} + +export async function assertBeforeLoadScenario(handler: StartRequestHandler) { + const response = await handler.fetch( + new Request('http://localhost/a-sanity/b-sanity/c-sanity', requestInit), + ) + const body = await response.text() + const expectedMarker = makeBeforeLoadMarker({ + ctxA: 'a-sanity', + ctxB: 'b-sanity', + ctxC: 'c-sanity', + chainToken: 'a-sanity.b-sanity.c-sanity', + }) + + if (response.status !== 200) { + throw new Error( + `Expected beforeLoad setup status 200, received ${response.status}`, + ) + } + + if (!body.includes(expectedMarker)) { + throw new Error( + `Expected beforeLoad setup response to include ${expectedMarker}`, + ) + } +} + +export const beforeLoadBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runBeforeLoadLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: beforeLoadLoopIterations, + buildRequest: buildBeforeLoadRequest, + }) +} diff --git a/benchmarks/ssr/scenarios/before-load/shared.ts b/benchmarks/ssr/scenarios/before-load/shared.ts new file mode 100644 index 00000000000..3c895aca030 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/shared.ts @@ -0,0 +1,10 @@ +export interface BeforeLoadContext { + chainToken?: string + ctxA?: string + ctxB?: string + ctxC?: string +} + +export function makeBeforeLoadMarker(context: BeforeLoadContext) { + return `ctx:${context.ctxA}:${context.ctxB}:${context.ctxC}:${context.chainToken}` +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/project.json b/benchmarks/ssr/scenarios/before-load/solid/project.json new file mode 100644 index 00000000000..804cd9c1b02 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-before-load-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/speed.bench.ts b/benchmarks/ssr/scenarios/before-load/solid/speed.bench.ts new file mode 100644 index 00000000000..5f6e8baf8a4 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertBeforeLoadScenario, + beforeLoadBenchOptions, + runBeforeLoadLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertBeforeLoadScenario(handler) + +describe('ssr', () => { + bench( + 'ssr before-load chain (solid)', + () => runBeforeLoadLoop(handler), + beforeLoadBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/before-load/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/before-load/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..7b299cfcd1e --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 ARouteImport } from './routes/$a' +import { Route as ABRouteImport } from './routes/$a.$b' +import { Route as ABCRouteImport } from './routes/$a.$b.$c' + +const ARoute = ARouteImport.update({ + id: '/$a', + path: '/$a', + getParentRoute: () => rootRouteImport, +} as any) +const ABRoute = ABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => ARoute, +} as any) +const ABCRoute = ABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => ABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/$a': { + id: '/$a' + path: '/$a' + fullPath: '/$a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + '/$a/$b': { + id: '/$a/$b' + path: '/$b' + fullPath: '/$a/$b' + preLoaderRoute: typeof ABRouteImport + parentRoute: typeof ARoute + } + '/$a/$b/$c': { + id: '/$a/$b/$c' + path: '/$c' + fullPath: '/$a/$b/$c' + preLoaderRoute: typeof ABCRouteImport + parentRoute: typeof ABRoute + } + } +} + +interface ABRouteChildren { + ABCRoute: typeof ABCRoute +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRoute, +} + +const ABRouteWithChildren = ABRoute._addFileChildren(ABRouteChildren) + +interface ARouteChildren { + ABRoute: typeof ABRouteWithChildren +} + +const ARouteChildren: ARouteChildren = { + ABRoute: ABRouteWithChildren, +} + +const ARouteWithChildren = ARoute._addFileChildren(ARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/src/router.tsx b/benchmarks/ssr/scenarios/before-load/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..7966bcc4c20 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.$b.$c.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makeBeforeLoadMarker, type BeforeLoadContext } from '../../../shared' + +export const Route = createFileRoute('/$a/$b/$c')({ + beforeLoad: ({ params, context }) => { + const parent = context as BeforeLoadContext + + return { + chainToken: `${parent.chainToken}.${params.c}`, + ctxC: params.c, + } + }, + loader: ({ context }) => ({ + marker: makeBeforeLoadMarker(context as BeforeLoadContext), + }), + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + + return
{data().marker}
+} diff --git a/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.$b.tsx b/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..90656dc7a1c --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.$b.tsx @@ -0,0 +1,18 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import type { BeforeLoadContext } from '../../../shared' + +export const Route = createFileRoute('/$a/$b')({ + beforeLoad: ({ params, context }) => { + const parent = context as BeforeLoadContext + + return { + chainToken: `${parent.chainToken}.${params.b}`, + ctxB: params.b, + } + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + return +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.tsx b/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.tsx new file mode 100644 index 00000000000..6336f566404 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/src/routes/$a.tsx @@ -0,0 +1,13 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/$a')({ + beforeLoad: ({ params }) => ({ + chainToken: params.a, + ctxA: params.a, + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + return +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/before-load/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/tsconfig.json b/benchmarks/ssr/scenarios/before-load/solid/tsconfig.json new file mode 100644 index 00000000000..09952927430 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared.ts", + "../shared-bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/before-load/solid/vite.config.ts b/benchmarks/ssr/scenarios/before-load/solid/vite.config.ts new file mode 100644 index 00000000000..ea0177e0597 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr before-load (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/before-load/vue/project.json b/benchmarks/ssr/scenarios/before-load/vue/project.json new file mode 100644 index 00000000000..92d724821e0 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-before-load-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/speed.bench.ts b/benchmarks/ssr/scenarios/before-load/vue/speed.bench.ts new file mode 100644 index 00000000000..d2cfd6814b0 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertBeforeLoadScenario, + beforeLoadBenchOptions, + runBeforeLoadLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertBeforeLoadScenario(handler) + +describe('ssr', () => { + bench( + 'ssr before-load chain (vue)', + () => runBeforeLoadLoop(handler), + beforeLoadBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/before-load/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/before-load/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..24ee6c3b9d6 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 ARouteImport } from './routes/$a' +import { Route as ABRouteImport } from './routes/$a.$b' +import { Route as ABCRouteImport } from './routes/$a.$b.$c' + +const ARoute = ARouteImport.update({ + id: '/$a', + path: '/$a', + getParentRoute: () => rootRouteImport, +} as any) +const ABRoute = ABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => ARoute, +} as any) +const ABCRoute = ABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => ABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/$a': { + id: '/$a' + path: '/$a' + fullPath: '/$a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + '/$a/$b': { + id: '/$a/$b' + path: '/$b' + fullPath: '/$a/$b' + preLoaderRoute: typeof ABRouteImport + parentRoute: typeof ARoute + } + '/$a/$b/$c': { + id: '/$a/$b/$c' + path: '/$c' + fullPath: '/$a/$b/$c' + preLoaderRoute: typeof ABCRouteImport + parentRoute: typeof ABRoute + } + } +} + +interface ABRouteChildren { + ABCRoute: typeof ABCRoute +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRoute, +} + +const ABRouteWithChildren = ABRoute._addFileChildren(ABRouteChildren) + +interface ARouteChildren { + ABRoute: typeof ABRouteWithChildren +} + +const ARouteChildren: ARouteChildren = { + ABRoute: ABRouteWithChildren, +} + +const ARouteWithChildren = ARoute._addFileChildren(ARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/src/router.tsx b/benchmarks/ssr/scenarios/before-load/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..118e16553b2 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.$b.$c.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makeBeforeLoadMarker, type BeforeLoadContext } from '../../../shared' + +export const Route = createFileRoute('/$a/$b/$c')({ + beforeLoad: ({ params, context }) => { + const parent = context as BeforeLoadContext + + return { + chainToken: `${parent.chainToken}.${params.c}`, + ctxC: params.c, + } + }, + loader: ({ context }) => ({ + marker: makeBeforeLoadMarker(context as BeforeLoadContext), + }), + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + + return
{data.value.marker}
+} diff --git a/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.$b.tsx b/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..43ea0dcf873 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.$b.tsx @@ -0,0 +1,18 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import type { BeforeLoadContext } from '../../../shared' + +export const Route = createFileRoute('/$a/$b')({ + beforeLoad: ({ params, context }) => { + const parent = context as BeforeLoadContext + + return { + chainToken: `${parent.chainToken}.${params.b}`, + ctxB: params.b, + } + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + return +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.tsx b/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.tsx new file mode 100644 index 00000000000..7f036dceaca --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/src/routes/$a.tsx @@ -0,0 +1,13 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/$a')({ + beforeLoad: ({ params }) => ({ + chainToken: params.a, + ctxA: params.a, + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + return +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/before-load/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/tsconfig.json b/benchmarks/ssr/scenarios/before-load/vue/tsconfig.json new file mode 100644 index 00000000000..33e5cc442d7 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared.ts", + "../shared-bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/before-load/vue/vite.config.ts b/benchmarks/ssr/scenarios/before-load/vue/vite.config.ts new file mode 100644 index 00000000000..181200a0041 --- /dev/null +++ b/benchmarks/ssr/scenarios/before-load/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr before-load (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/control-flow/react/project.json b/benchmarks/ssr/scenarios/control-flow/react/project.json new file mode 100644 index 00000000000..80086788ebc --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-control-flow-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/speed.bench.ts b/benchmarks/ssr/scenarios/control-flow/react/speed.bench.ts new file mode 100644 index 00000000000..4fca8eb4771 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/speed.bench.ts @@ -0,0 +1,53 @@ +import { bench, describe } from 'vitest' +import { + assertControlFlowSanity, + controlFlowBenchOptions, + runErrorLoop, + runNotFoundLoop, + runRedirectLoop, + runRouteHeadersLoop, + runUnmatchedLoop, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertControlFlowSanity(handler) + +describe('ssr', () => { + bench( + 'ssr redirect (react)', + () => runRedirectLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr not-found (react)', + () => runNotFoundLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow error 500 (react)', + () => runErrorLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow unmatched 404 (react)', + () => runUnmatchedLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow route headers (react)', + () => runRouteHeadersLoop(handler), + controlFlowBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/control-flow/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..ed682bf62fa --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routeTree.gen.ts @@ -0,0 +1,177 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 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' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$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', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + HeadersIdRoute: typeof HeadersIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/headers/$id': { + id: '/headers/$id' + path: '/headers/$id' + fullPath: '/headers/$id' + preLoaderRoute: typeof HeadersIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + HeadersIdRoute: HeadersIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/router.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/__root.tsx new file mode 100644 index 00000000000..e08ff063615 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/__root.tsx @@ -0,0 +1,30 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: RootNotFoundComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootNotFoundComponent() { + return
root-not-found-marker
+} + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/boom.$id.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/boom.$id.tsx new file mode 100644 index 00000000000..eb47d340707 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
control-flow-error-boundary
+} + +function BoomComponent() { + return null +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/from.$id.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/from.$id.tsx new file mode 100644 index 00000000000..e9289fce306 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id } }) + }, + component: FromComponent, +}) + +function FromComponent() { + return null +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/headers.$id.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/headers.$id.tsx new file mode 100644 index 00000000000..412d056d859 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/headers.$id.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/headers/$id')({ + headers: ({ params }) => ({ + 'x-bench-route-header': `route-header-${params.id}`, + 'x-bench-route-static': 'control-flow-route-headers', + }), + component: HeadersComponent, +}) + +function HeadersComponent() { + const params = Route.useParams() + + return
{`headers-${params.id}`}
+} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/index.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/index.tsx new file mode 100644 index 00000000000..e9886f60c45 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
control-flow-index
+} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/missing.$id.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/missing.$id.tsx new file mode 100644 index 00000000000..b7d760e562a --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/missing.$id.tsx @@ -0,0 +1,12 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + component: MissingComponent, +}) + +function MissingComponent() { + return null +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/src/routes/target.$id.tsx b/benchmarks/ssr/scenarios/control-flow/react/src/routes/target.$id.tsx new file mode 100644 index 00000000000..1c13394cb24 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params.id}`}
+} diff --git a/benchmarks/ssr/scenarios/control-flow/react/tsconfig.json b/benchmarks/ssr/scenarios/control-flow/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/control-flow/react/vite.config.ts b/benchmarks/ssr/scenarios/control-flow/react/vite.config.ts new file mode 100644 index 00000000000..b15f7bc5306 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr control-flow (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/control-flow/shared.ts b/benchmarks/ssr/scenarios/control-flow/shared.ts new file mode 100644 index 00000000000..db74f786623 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/shared.ts @@ -0,0 +1,225 @@ +import { expect } from 'vitest' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const redirectLoopIterations = 100 +const notFoundLoopIterations = 20 +const errorLoopIterations = 20 +const unmatchedLoopIterations = 40 +const routeHeadersLoopIterations = 20 + +// Pinned to the current built handler responses for these control-flow routes. +const OK_STATUS = 200 +const REDIRECT_STATUS = 307 +const NOT_FOUND_STATUS = 404 +const ERROR_STATUS = 500 +const ROUTE_HEADERS_STATIC_VALUE = 'control-flow-route-headers' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +export const controlFlowBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +function buildRedirectRequest(random: () => number) { + return new Request( + `http://localhost/from/${randomSegment(random)}`, + requestInit, + ) +} + +function buildNotFoundRequest(random: () => number) { + return new Request( + `http://localhost/missing/${randomSegment(random)}`, + requestInit, + ) +} + +function buildErrorRequest(random: () => number) { + return new Request( + `http://localhost/boom/${randomSegment(random)}`, + requestInit, + ) +} + +function buildUnmatchedRequest(random: () => number) { + return new Request( + `http://localhost/definitely-not-a-route/${randomSegment(random)}`, + requestInit, + ) +} + +function buildRouteHeadersRequest(random: () => number) { + return new Request( + `http://localhost/headers/${randomSegment(random)}`, + requestInit, + ) +} + +function getRequestId(request: Request) { + const id = new URL(request.url).pathname.split('/').pop() + + if (!id) { + throw new Error(`expected request id in ${request.url}`) + } + + return id +} + +function validateRedirectResponse(response: Response) { + if (response.status !== REDIRECT_STATUS) { + throw new Error(`expected ${REDIRECT_STATUS}, got ${response.status}`) + } +} + +function validateNotFoundResponse(response: Response) { + if (response.status !== NOT_FOUND_STATUS) { + throw new Error(`expected ${NOT_FOUND_STATUS}, got ${response.status}`) + } +} + +function validateErrorResponse(response: Response) { + if (response.status !== ERROR_STATUS) { + throw new Error(`expected ${ERROR_STATUS}, got ${response.status}`) + } +} + +function validateUnmatchedResponse(response: Response) { + if (response.status !== NOT_FOUND_STATUS) { + throw new Error(`expected ${NOT_FOUND_STATUS}, got ${response.status}`) + } +} + +function validateRouteHeadersResponse(response: Response, request: Request) { + if (response.status !== OK_STATUS) { + throw new Error(`expected ${OK_STATUS}, got ${response.status}`) + } + + const id = getRequestId(request) + const routeHeader = response.headers.get('x-bench-route-header') + + if (routeHeader !== `route-header-${id}`) { + throw new Error(`expected route header for ${id}, got ${routeHeader}`) + } + + const staticHeader = response.headers.get('x-bench-route-static') + + if (staticHeader !== ROUTE_HEADERS_STATIC_VALUE) { + throw new Error( + `expected route static header ${ROUTE_HEADERS_STATIC_VALUE}, got ${staticHeader}`, + ) + } +} + +export function runRedirectLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: redirectLoopIterations, + buildRequest: buildRedirectRequest, + validateResponse: validateRedirectResponse, + }) +} + +export function runNotFoundLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: notFoundLoopIterations, + buildRequest: buildNotFoundRequest, + validateResponse: validateNotFoundResponse, + }) +} + +export function runErrorLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: errorLoopIterations, + buildRequest: buildErrorRequest, + validateResponse: validateErrorResponse, + }) +} + +export function runUnmatchedLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: unmatchedLoopIterations, + buildRequest: buildUnmatchedRequest, + validateResponse: validateUnmatchedResponse, + }) +} + +export function runRouteHeadersLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: routeHeadersLoopIterations, + buildRequest: buildRouteHeadersRequest, + validateResponse: validateRouteHeadersResponse, + }) +} + +export async function assertControlFlowSanity(handler: StartRequestHandler) { + const redirectId = 'sanity-redirect' + const redirectResponse = await handler.fetch( + new Request(`http://localhost/from/${redirectId}`, requestInit), + ) + + expect(redirectResponse.status).toBe(REDIRECT_STATUS) + expect(redirectResponse.headers.get('location')).toBe(`/target/${redirectId}`) + + const notFoundResponse = await handler.fetch( + new Request('http://localhost/missing/sanity-missing', requestInit), + ) + const notFoundBody = await notFoundResponse.text() + + expect(notFoundResponse.status).toBe(NOT_FOUND_STATUS) + expect(notFoundBody).toContain('root-not-found-marker') + + const errorId = 'sanity-error' + const errorResponse = await handler.fetch( + new Request(`http://localhost/boom/${errorId}`, requestInit), + ) + const errorBody = await errorResponse.text() + + expect(errorResponse.status).toBe(ERROR_STATUS) + expect(errorBody).toContain('data-bench="error-boundary"') + const firstScriptIndex = errorBody.indexOf(' { + bench( + 'ssr redirect (solid)', + () => runRedirectLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr not-found (solid)', + () => runNotFoundLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow error 500 (solid)', + () => runErrorLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow unmatched 404 (solid)', + () => runUnmatchedLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow route headers (solid)', + () => runRouteHeadersLoop(handler), + controlFlowBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/control-flow/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..5e7342c172c --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routeTree.gen.ts @@ -0,0 +1,177 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 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' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$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', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + HeadersIdRoute: typeof HeadersIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/headers/$id': { + id: '/headers/$id' + path: '/headers/$id' + fullPath: '/headers/$id' + preLoaderRoute: typeof HeadersIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + HeadersIdRoute: HeadersIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/router.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..dc8b783e76d --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/__root.tsx @@ -0,0 +1,30 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: RootNotFoundComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootNotFoundComponent() { + return
root-not-found-marker
+} + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/boom.$id.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/boom.$id.tsx new file mode 100644 index 00000000000..0bcc4dc02b8 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
control-flow-error-boundary
+} + +function BoomComponent() { + return null +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/from.$id.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/from.$id.tsx new file mode 100644 index 00000000000..a319375db3f --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id } }) + }, + component: FromComponent, +}) + +function FromComponent() { + return null +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/headers.$id.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/headers.$id.tsx new file mode 100644 index 00000000000..9d87b4a3547 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/headers.$id.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/headers/$id')({ + headers: ({ params }) => ({ + 'x-bench-route-header': `route-header-${params.id}`, + 'x-bench-route-static': 'control-flow-route-headers', + }), + component: HeadersComponent, +}) + +function HeadersComponent() { + const params = Route.useParams() + + return
{`headers-${params().id}`}
+} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/index.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/index.tsx new file mode 100644 index 00000000000..96b26a2d6c5 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
control-flow-index
+} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/missing.$id.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/missing.$id.tsx new file mode 100644 index 00000000000..e5c42b56dca --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/missing.$id.tsx @@ -0,0 +1,12 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + component: MissingComponent, +}) + +function MissingComponent() { + return null +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/src/routes/target.$id.tsx b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/target.$id.tsx new file mode 100644 index 00000000000..4196fc11839 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params().id}`}
+} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/tsconfig.json b/benchmarks/ssr/scenarios/control-flow/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/control-flow/solid/vite.config.ts b/benchmarks/ssr/scenarios/control-flow/solid/vite.config.ts new file mode 100644 index 00000000000..0bcd175f606 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr control-flow (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/control-flow/vue/project.json b/benchmarks/ssr/scenarios/control-flow/vue/project.json new file mode 100644 index 00000000000..e0e014e5881 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-control-flow-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/speed.bench.ts b/benchmarks/ssr/scenarios/control-flow/vue/speed.bench.ts new file mode 100644 index 00000000000..99bb46dfb36 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/speed.bench.ts @@ -0,0 +1,53 @@ +import { bench, describe } from 'vitest' +import { + assertControlFlowSanity, + controlFlowBenchOptions, + runErrorLoop, + runNotFoundLoop, + runRedirectLoop, + runRouteHeadersLoop, + runUnmatchedLoop, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertControlFlowSanity(handler) + +describe('ssr', () => { + bench( + 'ssr redirect (vue)', + () => runRedirectLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr not-found (vue)', + () => runNotFoundLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow error 500 (vue)', + () => runErrorLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow unmatched 404 (vue)', + () => runUnmatchedLoop(handler), + controlFlowBenchOptions, + ) + + bench( + 'ssr control-flow route headers (vue)', + () => runRouteHeadersLoop(handler), + controlFlowBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/control-flow/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..f5063d1c09d --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routeTree.gen.ts @@ -0,0 +1,177 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 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' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$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', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/headers/$id': typeof HeadersIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/headers/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + HeadersIdRoute: typeof HeadersIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/headers/$id': { + id: '/headers/$id' + path: '/headers/$id' + fullPath: '/headers/$id' + preLoaderRoute: typeof HeadersIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + HeadersIdRoute: HeadersIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/router.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..815466918ab --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/__root.tsx @@ -0,0 +1,32 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: RootNotFoundComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootNotFoundComponent() { + return
root-not-found-marker
+} + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/boom.$id.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/boom.$id.tsx new file mode 100644 index 00000000000..dfae636ae71 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
control-flow-error-boundary
+} + +function BoomComponent() { + return <> +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/from.$id.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/from.$id.tsx new file mode 100644 index 00000000000..dfb90a51f80 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id } }) + }, + component: FromComponent, +}) + +function FromComponent() { + return <> +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/headers.$id.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/headers.$id.tsx new file mode 100644 index 00000000000..ac2ec01b52f --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/headers.$id.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/headers/$id')({ + headers: ({ params }) => ({ + 'x-bench-route-header': `route-header-${params.id}`, + 'x-bench-route-static': 'control-flow-route-headers', + }), + component: HeadersComponent, +}) + +function HeadersComponent() { + const params = Route.useParams() + + return
{`headers-${params.value.id}`}
+} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/index.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/index.tsx new file mode 100644 index 00000000000..51a503a1d18 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
control-flow-index
+} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/missing.$id.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/missing.$id.tsx new file mode 100644 index 00000000000..b15333f5a3c --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/missing.$id.tsx @@ -0,0 +1,12 @@ +import { createFileRoute, notFound } from '@tanstack/vue-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + component: MissingComponent, +}) + +function MissingComponent() { + return <> +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/src/routes/target.$id.tsx b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/target.$id.tsx new file mode 100644 index 00000000000..17ade6bf5b7 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params.value.id}`}
+} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/tsconfig.json b/benchmarks/ssr/scenarios/control-flow/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/control-flow/vue/vite.config.ts b/benchmarks/ssr/scenarios/control-flow/vue/vite.config.ts new file mode 100644 index 00000000000..2c978a7a71e --- /dev/null +++ b/benchmarks/ssr/scenarios/control-flow/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr control-flow (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/bench.ts b/benchmarks/ssr/scenarios/global-middleware/bench.ts new file mode 100644 index 00000000000..4c7249e9353 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/bench.ts @@ -0,0 +1,290 @@ +import { toJSONAsync } from 'seroval' +import { + createDeterministicRandom, + randomSegment, + runRequestLoop, +} from '../../bench-utils' +import { + expectedFunctionTotal, + expectedRequestTotal, + makeDocumentMarker, + makeServerFnMarker, + makeServerRouteMarker, +} from './shared' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +type Payload = { + q: string + n: number + nested: { list: Array } +} + +type FnUrls = { + post: string +} + +export interface GlobalMiddlewareBenchContext { + urls: FnUrls + bodies: Array + expectedFnMarker: string +} + +const benchmarkSeed = 0xdecafbad +const origin = 'http://localhost' +const tssContentTypeFramed = 'application/x-tss-framed' +const xTssSerialized = 'x-tss-serialized' +const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json` +const documentRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit +const apiRequestInit = { + method: 'GET', + headers: { + accept: 'application/json', + }, +} satisfies RequestInit +const postHeaders = { + 'x-tsr-serverFn': 'true', + 'sec-fetch-site': 'same-origin', + accept: acceptHeader, + 'content-type': 'application/json', +} satisfies HeadersInit +const documentLoopIterations = 20 +const apiLoopIterations = 100 + +export const globalMiddlewareBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} as const + +function createPayloads() { + const random = createDeterministicRandom(benchmarkSeed) + + return Array.from({ length: 10 }, (_, index): Payload => { + const queryParts = Array.from({ length: 6 }, () => randomSegment(random)) + + return { + q: `q-${index}-${queryParts.join('-')}`, + n: 100 + index, + nested: { + list: Array.from( + { length: 5 }, + () => `l-${index}-${randomSegment(random)}-${randomSegment(random)}`, + ), + }, + } + }) +} + +async function createBodies(payloads: Array) { + return await Promise.all( + payloads.map(async (payload) => + JSON.stringify(await toJSONAsync({ data: payload })), + ), + ) +} + +async function discoverUrls(handler: StartRequestHandler) { + const response = await handler.fetch(new Request(`${origin}/api/fn-urls`)) + + if (response.status === 404) { + throw new Error('URL discovery route returned 404 for /api/fn-urls') + } + + if (response.status !== 200) { + throw new Error( + `URL discovery failed with status ${response.status}: ${await response.text()}`, + ) + } + + const urls = (await response.json()) as Partial + + if (typeof urls.post !== 'string') { + throw new Error( + `URL discovery returned invalid payload: ${JSON.stringify(urls)}`, + ) + } + + return { post: urls.post } +} + +function buildDocumentRequest(random: () => number, index: number) { + return new Request( + `${origin}/page/${randomSegment(random)}-${index.toString(36)}`, + documentRequestInit, + ) +} + +function buildServerRouteRequest(random: () => number, index: number) { + return new Request( + `${origin}/api/ping/${randomSegment(random)}-${index.toString(36)}`, + apiRequestInit, + ) +} + +function buildPostRequest(urls: FnUrls, bodies: Array, index: number) { + return new Request(`${origin}${urls.post}`, { + method: 'POST', + headers: postHeaders, + body: bodies[index % bodies.length], + }) +} + +export async function setupGlobalMiddlewareBench( + handler: StartRequestHandler, +): Promise { + const urls = await discoverUrls(handler) + const payloads = createPayloads() + const bodies = await createBodies(payloads) + const expectedFnMarker = makeServerFnMarker(payloads[0]!.q, { + requestTrace: 'req.r1.r2.r3', + requestTotal: expectedRequestTotal, + functionTrace: 'fn.f1.f2', + functionTotal: expectedFunctionTotal, + }) + + return { urls, bodies, expectedFnMarker } +} + +async function assertDocumentResponse(handler: StartRequestHandler) { + const id = 'page-sanity' + const response = await handler.fetch( + new Request(`${origin}/page/${id}`, documentRequestInit), + ) + const body = await response.text() + const expectedMarker = makeDocumentMarker(id, { + requestTrace: 'req.r1.r2.r3', + requestTotal: expectedRequestTotal, + }) + + if (response.status !== 200) { + throw new Error(`Expected document status 200, received ${response.status}`) + } + + if (!body.includes(expectedMarker)) { + throw new Error( + `Expected document response to include global middleware marker ${expectedMarker}`, + ) + } +} + +async function assertServerRouteResponse(handler: StartRequestHandler) { + const id = 'route-sanity' + const response = await handler.fetch( + new Request(`${origin}/api/ping/${id}`, apiRequestInit), + ) + + if (response.status !== 200) { + throw new Error( + `Expected server route status 200, received ${response.status}`, + ) + } + + const contentType = response.headers.get('content-type') + + if (!contentType?.includes('application/json')) { + throw new Error( + `Expected JSON server route response, received ${contentType}`, + ) + } + + const body = (await response.json()) as { + marker?: string + requestTotal?: number + } + const expectedMarker = makeServerRouteMarker(id, { + requestTrace: 'req.r1.r2.r3', + requestTotal: expectedRequestTotal, + }) + + if (body.marker !== expectedMarker) { + throw new Error( + `Expected server route marker ${expectedMarker}, received ${body.marker}`, + ) + } + + if (body.requestTotal !== expectedRequestTotal) { + throw new Error( + `Expected server route requestTotal ${expectedRequestTotal}, received ${body.requestTotal}`, + ) + } +} + +async function assertServerFnResponse( + handler: StartRequestHandler, + context: GlobalMiddlewareBenchContext, +) { + const response = await handler.fetch( + buildPostRequest(context.urls, context.bodies, 0), + ) + const text = await response.text() + + if (response.status === 403) { + throw new Error('Server function sanity check failed with 403') + } + + if (response.status === 404) { + throw new Error('Server function sanity check failed with 404') + } + + if (response.status !== 200) { + throw new Error( + `Server function sanity check failed with status ${response.status}: ${text}`, + ) + } + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`Server function response missing ${xTssSerialized} header`) + } + + if (!text.includes(context.expectedFnMarker)) { + throw new Error( + `Expected server function response to include ${context.expectedFnMarker}: ${text}`, + ) + } +} + +export async function assertGlobalMiddlewareScenario( + handler: StartRequestHandler, + context: GlobalMiddlewareBenchContext, +) { + await assertDocumentResponse(handler) + await assertServerFnResponse(handler, context) + await assertServerRouteResponse(handler) +} + +export function runGlobalMiddlewareDocumentLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: documentLoopIterations, + buildRequest: buildDocumentRequest, + }) +} + +export function runGlobalMiddlewareServerFnLoop( + handler: StartRequestHandler, + context: GlobalMiddlewareBenchContext, +) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: apiLoopIterations, + buildRequest: (_random, index) => + buildPostRequest(context.urls, context.bodies, index), + }) +} + +export function runGlobalMiddlewareServerRouteLoop( + handler: StartRequestHandler, +) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: apiLoopIterations, + buildRequest: buildServerRouteRequest, + }) +} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/project.json b/benchmarks/ssr/scenarios/global-middleware/react/project.json new file mode 100644 index 00000000000..bded6925fd8 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-global-middleware-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/speed.bench.ts b/benchmarks/ssr/scenarios/global-middleware/react/speed.bench.ts new file mode 100644 index 00000000000..8288322e70d --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/speed.bench.ts @@ -0,0 +1,42 @@ +import { bench, describe } from 'vitest' +import { + assertGlobalMiddlewareScenario, + globalMiddlewareBenchOptions, + runGlobalMiddlewareDocumentLoop, + runGlobalMiddlewareServerFnLoop, + 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 document (react)', + () => runGlobalMiddlewareDocumentLoop(handler), + globalMiddlewareBenchOptions, + ) + + bench( + 'ssr global-mw server-fn (react)', + () => runGlobalMiddlewareServerFnLoop(handler, context), + globalMiddlewareBenchOptions, + ) + + bench( + 'ssr global-mw server-route (react)', + () => runGlobalMiddlewareServerRouteLoop(handler), + globalMiddlewareBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/fns.ts b/benchmarks/ssr/scenarios/global-middleware/react/src/fns.ts new file mode 100644 index 00000000000..68cfd992c60 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/fns.ts @@ -0,0 +1,34 @@ +import { createServerFn } from '@tanstack/react-start' +import { makeServerFnMarker, type GlobalMiddlewareContext } from '../../shared' + +type Payload = { q: string; n: number; nested: { list: Array } } + +const validate = (input: unknown): Payload => { + const payload = input as Payload + + if ( + typeof payload?.q !== 'string' || + typeof payload?.n !== 'number' || + !Array.isArray(payload?.nested?.list) + ) { + throw new Error('invalid payload') + } + + return payload +} + +export const echoPost = createServerFn({ method: 'POST' }) + .validator(validate) + .handler(({ data, context }) => { + const middlewareContext = (context ?? {}) as GlobalMiddlewareContext + + return { + echoed: data, + marker: makeServerFnMarker(data.q, middlewareContext), + sum: + data.n + + (middlewareContext.requestTotal ?? 0) + + (middlewareContext.functionTotal ?? 0), + list: data.nested.list.map((item) => `global-${item}`), + } + }) diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/global-middleware/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..aa916f08002 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/routeTree.gen.ts @@ -0,0 +1,123 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' +import { Route as ApiPingIdRouteImport } from './routes/api.ping.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) +const ApiPingIdRoute = ApiPingIdRouteImport.update({ + id: '/api/ping/$id', + path: '/api/ping/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + id: '__root__' | '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute + PageIdRoute: typeof PageIdRoute + ApiPingIdRoute: typeof ApiPingIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + '/api/ping/$id': { + id: '/api/ping/$id' + path: '/api/ping/$id' + fullPath: '/api/ping/$id' + preLoaderRoute: typeof ApiPingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, + PageIdRoute: PageIdRoute, + ApiPingIdRoute: ApiPingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.ts' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/router.tsx b/benchmarks/ssr/scenarios/global-middleware/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..bcb21119307 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/api.fn-urls.ts @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import { echoPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => Response.json({ post: echoPost.url }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/routes/api.ping.$id.ts b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/api.ping.$id.ts new file mode 100644 index 00000000000..434833f018e --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/api.ping.$id.ts @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + makeServerRouteMarker, + type GlobalMiddlewareContext, +} from '../../../shared' + +export const Route = createFileRoute('/api/ping/$id')({ + server: { + handlers: { + GET: ({ params, context }) => { + const middlewareContext = (context ?? {}) as GlobalMiddlewareContext + + return Response.json({ + id: params.id, + marker: makeServerRouteMarker(params.id, middlewareContext), + requestTrace: middlewareContext.requestTrace, + requestTotal: middlewareContext.requestTotal, + }) + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/routes/index.tsx b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/index.tsx new file mode 100644 index 00000000000..a0d3aec8690 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import { echoPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
global-middleware
+} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/routes/page.$id.tsx b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/page.$id.tsx new file mode 100644 index 00000000000..6b7a42c12ee --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/routes/page.$id.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + getGlobalMiddlewareContext, + makeDocumentMarker, + type GlobalMiddlewareContext, +} from '../../../shared' + +export const Route = createFileRoute('/page/$id')({ + beforeLoad: ({ serverContext }) => ({ + globalMiddlewareContext: (serverContext ?? {}) as GlobalMiddlewareContext, + }), + loader: ({ params, context }) => ({ + marker: makeDocumentMarker(params.id, getGlobalMiddlewareContext(context)), + }), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return
{data.marker}
+} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/src/start.ts b/benchmarks/ssr/scenarios/global-middleware/react/src/start.ts new file mode 100644 index 00000000000..6b59e8035b5 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/src/start.ts @@ -0,0 +1,84 @@ +import { createMiddleware, createStart } from '@tanstack/react-start' +import type { GlobalMiddlewareContext } from '../../shared' + +function appendTrace( + trace: string | undefined, + fallback: string, + label: string, +) { + return `${trace ?? fallback}.${label}` +} + +const requestMiddlewareA = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r1'), + requestTotal: (ctx.requestTotal ?? 0) + 1, + }, + }) + }, +) + +const requestMiddlewareB = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r2'), + requestTotal: (ctx.requestTotal ?? 0) + 2, + }, + }) + }, +) + +const requestMiddlewareC = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r3'), + requestTotal: (ctx.requestTotal ?? 0) + 3, + }, + }) + }, +) + +const functionMiddlewareA = createMiddleware({ type: 'function' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + functionTrace: appendTrace(ctx.functionTrace, 'fn', 'f1'), + functionTotal: (ctx.functionTotal ?? 0) + 10, + }, + }) + }, +) + +const functionMiddlewareB = createMiddleware({ type: 'function' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + functionTrace: appendTrace(ctx.functionTrace, 'fn', 'f2'), + functionTotal: (ctx.functionTotal ?? 0) + 20, + }, + }) + }, +) + +export const startInstance = createStart(() => ({ + requestMiddleware: [ + requestMiddlewareA, + requestMiddlewareB, + requestMiddlewareC, + ], + functionMiddleware: [functionMiddlewareA, functionMiddlewareB], +})) diff --git a/benchmarks/ssr/scenarios/global-middleware/react/tsconfig.json b/benchmarks/ssr/scenarios/global-middleware/react/tsconfig.json new file mode 100644 index 00000000000..0c0192daaae --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared.ts", + "../bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/global-middleware/react/vite.config.ts b/benchmarks/ssr/scenarios/global-middleware/react/vite.config.ts new file mode 100644 index 00000000000..71c8ff3719a --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr global-middleware (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/shared.ts b/benchmarks/ssr/scenarios/global-middleware/shared.ts new file mode 100644 index 00000000000..f90fa9b233d --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/shared.ts @@ -0,0 +1,47 @@ +export interface GlobalMiddlewareContext { + requestTrace?: string + requestTotal?: number + functionTrace?: string + functionTotal?: number +} + +export interface GlobalMiddlewareRouteContext extends GlobalMiddlewareContext { + serverContext?: GlobalMiddlewareContext + globalMiddlewareContext?: GlobalMiddlewareContext +} + +export const expectedRequestTrace = 'req.r1.r2.r3' +export const expectedRequestTotal = 6 +export const expectedFunctionTrace = 'fn.f1.f2' +export const expectedFunctionTotal = 30 + +export function makeDocumentMarker( + id: string, + context: GlobalMiddlewareContext, +) { + return `document:${context.requestTrace}:${id}:${context.requestTotal}` +} + +export function getGlobalMiddlewareContext(context: unknown) { + const routeContext = (context ?? {}) as GlobalMiddlewareRouteContext + + return ( + routeContext.globalMiddlewareContext ?? + routeContext.serverContext ?? + routeContext + ) +} + +export function makeServerRouteMarker( + id: string, + context: GlobalMiddlewareContext, +) { + return `route:${context.requestTrace}:${id}:${context.requestTotal}` +} + +export function makeServerFnMarker( + q: string, + context: GlobalMiddlewareContext, +) { + return `fn:${context.requestTrace}:${context.functionTrace}:${q}:${context.requestTotal}:${context.functionTotal}` +} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/project.json b/benchmarks/ssr/scenarios/global-middleware/solid/project.json new file mode 100644 index 00000000000..d325c155279 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-global-middleware-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/speed.bench.ts b/benchmarks/ssr/scenarios/global-middleware/solid/speed.bench.ts new file mode 100644 index 00000000000..019735eb40d --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/speed.bench.ts @@ -0,0 +1,42 @@ +import { bench, describe } from 'vitest' +import { + assertGlobalMiddlewareScenario, + globalMiddlewareBenchOptions, + runGlobalMiddlewareDocumentLoop, + runGlobalMiddlewareServerFnLoop, + 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 document (solid)', + () => runGlobalMiddlewareDocumentLoop(handler), + globalMiddlewareBenchOptions, + ) + + bench( + 'ssr global-mw server-fn (solid)', + () => runGlobalMiddlewareServerFnLoop(handler, context), + globalMiddlewareBenchOptions, + ) + + bench( + 'ssr global-mw server-route (solid)', + () => runGlobalMiddlewareServerRouteLoop(handler), + globalMiddlewareBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/fns.ts b/benchmarks/ssr/scenarios/global-middleware/solid/src/fns.ts new file mode 100644 index 00000000000..667baf9f54a --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/fns.ts @@ -0,0 +1,34 @@ +import { createServerFn } from '@tanstack/solid-start' +import { makeServerFnMarker, type GlobalMiddlewareContext } from '../../shared' + +type Payload = { q: string; n: number; nested: { list: Array } } + +const validate = (input: unknown): Payload => { + const payload = input as Payload + + if ( + typeof payload?.q !== 'string' || + typeof payload?.n !== 'number' || + !Array.isArray(payload?.nested?.list) + ) { + throw new Error('invalid payload') + } + + return payload +} + +export const echoPost = createServerFn({ method: 'POST' }) + .validator(validate) + .handler(({ data, context }) => { + const middlewareContext = (context ?? {}) as GlobalMiddlewareContext + + return { + echoed: data, + marker: makeServerFnMarker(data.q, middlewareContext), + sum: + data.n + + (middlewareContext.requestTotal ?? 0) + + (middlewareContext.functionTotal ?? 0), + list: data.nested.list.map((item) => `global-${item}`), + } + }) diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/global-middleware/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..28d816525e1 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/routeTree.gen.ts @@ -0,0 +1,123 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' +import { Route as ApiPingIdRouteImport } from './routes/api.ping.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) +const ApiPingIdRoute = ApiPingIdRouteImport.update({ + id: '/api/ping/$id', + path: '/api/ping/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + id: '__root__' | '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute + PageIdRoute: typeof PageIdRoute + ApiPingIdRoute: typeof ApiPingIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + '/api/ping/$id': { + id: '/api/ping/$id' + path: '/api/ping/$id' + fullPath: '/api/ping/$id' + preLoaderRoute: typeof ApiPingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, + PageIdRoute: PageIdRoute, + ApiPingIdRoute: ApiPingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.ts' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/router.tsx b/benchmarks/ssr/scenarios/global-middleware/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..7166ea3030b --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/api.fn-urls.ts @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { echoPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => Response.json({ post: echoPost.url }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/api.ping.$id.ts b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/api.ping.$id.ts new file mode 100644 index 00000000000..b92bcea5b06 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/api.ping.$id.ts @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + makeServerRouteMarker, + type GlobalMiddlewareContext, +} from '../../../shared' + +export const Route = createFileRoute('/api/ping/$id')({ + server: { + handlers: { + GET: ({ params, context }) => { + const middlewareContext = (context ?? {}) as GlobalMiddlewareContext + + return Response.json({ + id: params.id, + marker: makeServerRouteMarker(params.id, middlewareContext), + requestTrace: middlewareContext.requestTrace, + requestTotal: middlewareContext.requestTotal, + }) + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/index.tsx b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/index.tsx new file mode 100644 index 00000000000..c293004539b --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { echoPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
global-middleware
+} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/page.$id.tsx b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/page.$id.tsx new file mode 100644 index 00000000000..a5781b1a6a9 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/routes/page.$id.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + getGlobalMiddlewareContext, + makeDocumentMarker, + type GlobalMiddlewareContext, +} from '../../../shared' + +export const Route = createFileRoute('/page/$id')({ + beforeLoad: ({ serverContext }) => ({ + globalMiddlewareContext: (serverContext ?? {}) as GlobalMiddlewareContext, + }), + loader: ({ params, context }) => ({ + marker: makeDocumentMarker(params.id, getGlobalMiddlewareContext(context)), + }), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return
{data().marker}
+} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/src/start.ts b/benchmarks/ssr/scenarios/global-middleware/solid/src/start.ts new file mode 100644 index 00000000000..8c4168d574f --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/src/start.ts @@ -0,0 +1,84 @@ +import { createMiddleware, createStart } from '@tanstack/solid-start' +import type { GlobalMiddlewareContext } from '../../shared' + +function appendTrace( + trace: string | undefined, + fallback: string, + label: string, +) { + return `${trace ?? fallback}.${label}` +} + +const requestMiddlewareA = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r1'), + requestTotal: (ctx.requestTotal ?? 0) + 1, + }, + }) + }, +) + +const requestMiddlewareB = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r2'), + requestTotal: (ctx.requestTotal ?? 0) + 2, + }, + }) + }, +) + +const requestMiddlewareC = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r3'), + requestTotal: (ctx.requestTotal ?? 0) + 3, + }, + }) + }, +) + +const functionMiddlewareA = createMiddleware({ type: 'function' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + functionTrace: appendTrace(ctx.functionTrace, 'fn', 'f1'), + functionTotal: (ctx.functionTotal ?? 0) + 10, + }, + }) + }, +) + +const functionMiddlewareB = createMiddleware({ type: 'function' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + functionTrace: appendTrace(ctx.functionTrace, 'fn', 'f2'), + functionTotal: (ctx.functionTotal ?? 0) + 20, + }, + }) + }, +) + +export const startInstance = createStart(() => ({ + requestMiddleware: [ + requestMiddlewareA, + requestMiddlewareB, + requestMiddlewareC, + ], + functionMiddleware: [functionMiddlewareA, functionMiddlewareB], +})) diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/tsconfig.json b/benchmarks/ssr/scenarios/global-middleware/solid/tsconfig.json new file mode 100644 index 00000000000..5ad8f505b6c --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared.ts", + "../bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/global-middleware/solid/vite.config.ts b/benchmarks/ssr/scenarios/global-middleware/solid/vite.config.ts new file mode 100644 index 00000000000..e8af6933edc --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr global-middleware (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/project.json b/benchmarks/ssr/scenarios/global-middleware/vue/project.json new file mode 100644 index 00000000000..6dbc003aae3 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-global-middleware-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/speed.bench.ts b/benchmarks/ssr/scenarios/global-middleware/vue/speed.bench.ts new file mode 100644 index 00000000000..0b3ed9dd7a2 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/speed.bench.ts @@ -0,0 +1,42 @@ +import { bench, describe } from 'vitest' +import { + assertGlobalMiddlewareScenario, + globalMiddlewareBenchOptions, + runGlobalMiddlewareDocumentLoop, + runGlobalMiddlewareServerFnLoop, + 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 document (vue)', + () => runGlobalMiddlewareDocumentLoop(handler), + globalMiddlewareBenchOptions, + ) + + bench( + 'ssr global-mw server-fn (vue)', + () => runGlobalMiddlewareServerFnLoop(handler, context), + globalMiddlewareBenchOptions, + ) + + bench( + 'ssr global-mw server-route (vue)', + () => runGlobalMiddlewareServerRouteLoop(handler), + globalMiddlewareBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/fns.ts b/benchmarks/ssr/scenarios/global-middleware/vue/src/fns.ts new file mode 100644 index 00000000000..557f6ee849d --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/fns.ts @@ -0,0 +1,34 @@ +import { createServerFn } from '@tanstack/vue-start' +import { makeServerFnMarker, type GlobalMiddlewareContext } from '../../shared' + +type Payload = { q: string; n: number; nested: { list: Array } } + +const validate = (input: unknown): Payload => { + const payload = input as Payload + + if ( + typeof payload?.q !== 'string' || + typeof payload?.n !== 'number' || + !Array.isArray(payload?.nested?.list) + ) { + throw new Error('invalid payload') + } + + return payload +} + +export const echoPost = createServerFn({ method: 'POST' }) + .validator(validate) + .handler(({ data, context }) => { + const middlewareContext = (context ?? {}) as GlobalMiddlewareContext + + return { + echoed: data, + marker: makeServerFnMarker(data.q, middlewareContext), + sum: + data.n + + (middlewareContext.requestTotal ?? 0) + + (middlewareContext.functionTotal ?? 0), + list: data.nested.list.map((item) => `global-${item}`), + } + }) diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/global-middleware/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..aae3329420d --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/routeTree.gen.ts @@ -0,0 +1,123 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' +import { Route as ApiPingIdRouteImport } from './routes/api.ping.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) +const ApiPingIdRoute = ApiPingIdRouteImport.update({ + id: '/api/ping/$id', + path: '/api/ping/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/page/$id': typeof PageIdRoute + '/api/ping/$id': typeof ApiPingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + id: '__root__' | '/' | '/api/fn-urls' | '/page/$id' | '/api/ping/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute + PageIdRoute: typeof PageIdRoute + ApiPingIdRoute: typeof ApiPingIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + '/api/ping/$id': { + id: '/api/ping/$id' + path: '/api/ping/$id' + fullPath: '/api/ping/$id' + preLoaderRoute: typeof ApiPingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, + PageIdRoute: PageIdRoute, + ApiPingIdRoute: ApiPingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.ts' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/router.tsx b/benchmarks/ssr/scenarios/global-middleware/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..af51ea6aa3c --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/api.fn-urls.ts @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { echoPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => Response.json({ post: echoPost.url }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/api.ping.$id.ts b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/api.ping.$id.ts new file mode 100644 index 00000000000..f599ce1e499 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/api.ping.$id.ts @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { + makeServerRouteMarker, + type GlobalMiddlewareContext, +} from '../../../shared' + +export const Route = createFileRoute('/api/ping/$id')({ + server: { + handlers: { + GET: ({ params, context }) => { + const middlewareContext = (context ?? {}) as GlobalMiddlewareContext + + return Response.json({ + id: params.id, + marker: makeServerRouteMarker(params.id, middlewareContext), + requestTrace: middlewareContext.requestTrace, + requestTotal: middlewareContext.requestTotal, + }) + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/index.tsx b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/index.tsx new file mode 100644 index 00000000000..85d3de69a43 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { echoPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
global-middleware
+} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/page.$id.tsx b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/page.$id.tsx new file mode 100644 index 00000000000..2010d0cd2ab --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/routes/page.$id.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { + getGlobalMiddlewareContext, + makeDocumentMarker, + type GlobalMiddlewareContext, +} from '../../../shared' + +export const Route = createFileRoute('/page/$id')({ + beforeLoad: ({ serverContext }) => ({ + globalMiddlewareContext: (serverContext ?? {}) as GlobalMiddlewareContext, + }), + loader: ({ params, context }) => ({ + marker: makeDocumentMarker(params.id, getGlobalMiddlewareContext(context)), + }), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return
{data.value.marker}
+} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/src/start.ts b/benchmarks/ssr/scenarios/global-middleware/vue/src/start.ts new file mode 100644 index 00000000000..dc3f142e27f --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/src/start.ts @@ -0,0 +1,84 @@ +import { createMiddleware, createStart } from '@tanstack/vue-start' +import type { GlobalMiddlewareContext } from '../../shared' + +function appendTrace( + trace: string | undefined, + fallback: string, + label: string, +) { + return `${trace ?? fallback}.${label}` +} + +const requestMiddlewareA = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r1'), + requestTotal: (ctx.requestTotal ?? 0) + 1, + }, + }) + }, +) + +const requestMiddlewareB = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r2'), + requestTotal: (ctx.requestTotal ?? 0) + 2, + }, + }) + }, +) + +const requestMiddlewareC = createMiddleware({ type: 'request' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + requestTrace: appendTrace(ctx.requestTrace, 'req', 'r3'), + requestTotal: (ctx.requestTotal ?? 0) + 3, + }, + }) + }, +) + +const functionMiddlewareA = createMiddleware({ type: 'function' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + functionTrace: appendTrace(ctx.functionTrace, 'fn', 'f1'), + functionTotal: (ctx.functionTotal ?? 0) + 10, + }, + }) + }, +) + +const functionMiddlewareB = createMiddleware({ type: 'function' }).server( + ({ next, context }) => { + const ctx = (context ?? {}) as GlobalMiddlewareContext + + return next({ + context: { + functionTrace: appendTrace(ctx.functionTrace, 'fn', 'f2'), + functionTotal: (ctx.functionTotal ?? 0) + 20, + }, + }) + }, +) + +export const startInstance = createStart(() => ({ + requestMiddleware: [ + requestMiddlewareA, + requestMiddlewareB, + requestMiddlewareC, + ], + functionMiddleware: [functionMiddlewareA, functionMiddlewareB], +})) diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/tsconfig.json b/benchmarks/ssr/scenarios/global-middleware/vue/tsconfig.json new file mode 100644 index 00000000000..70d62162e67 --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared.ts", + "../bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/global-middleware/vue/vite.config.ts b/benchmarks/ssr/scenarios/global-middleware/vue/vite.config.ts new file mode 100644 index 00000000000..6dd55adab3a --- /dev/null +++ b/benchmarks/ssr/scenarios/global-middleware/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr global-middleware (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/head/react/project.json b/benchmarks/ssr/scenarios/head/react/project.json new file mode 100644 index 00000000000..093046fe398 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-head-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/head/react/speed.bench.ts b/benchmarks/ssr/scenarios/head/react/speed.bench.ts new file mode 100644 index 00000000000..19158b32a91 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/speed.bench.ts @@ -0,0 +1,17 @@ +import { bench, describe } from 'vitest' +import { assertHeadSanity, headBenchOptions, runHeadLoop } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertHeadSanity(handler) + +describe('ssr', () => { + bench('ssr head (react)', () => runHeadLoop(handler), headBenchOptions) +}) diff --git a/benchmarks/ssr/scenarios/head/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/head/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..3786e2137d8 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 HARouteImport } from './routes/h.$a' +import { Route as HABRouteImport } from './routes/h.$a.$b' +import { Route as HABCRouteImport } from './routes/h.$a.$b.$c' + +const HARoute = HARouteImport.update({ + id: '/h/$a', + path: '/h/$a', + getParentRoute: () => rootRouteImport, +} as any) +const HABRoute = HABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => HARoute, +} as any) +const HABCRoute = HABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => HABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRoutesByTo { + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + id: '__root__' | '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + HARoute: typeof HARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/h/$a': { + id: '/h/$a' + path: '/h/$a' + fullPath: '/h/$a' + preLoaderRoute: typeof HARouteImport + parentRoute: typeof rootRouteImport + } + '/h/$a/$b': { + id: '/h/$a/$b' + path: '/$b' + fullPath: '/h/$a/$b' + preLoaderRoute: typeof HABRouteImport + parentRoute: typeof HARoute + } + '/h/$a/$b/$c': { + id: '/h/$a/$b/$c' + path: '/$c' + fullPath: '/h/$a/$b/$c' + preLoaderRoute: typeof HABCRouteImport + parentRoute: typeof HABRoute + } + } +} + +interface HABRouteChildren { + HABCRoute: typeof HABCRoute +} + +const HABRouteChildren: HABRouteChildren = { + HABCRoute: HABCRoute, +} + +const HABRouteWithChildren = HABRoute._addFileChildren(HABRouteChildren) + +interface HARouteChildren { + HABRoute: typeof HABRouteWithChildren +} + +const HARouteChildren: HARouteChildren = { + HABRoute: HABRouteWithChildren, +} + +const HARouteWithChildren = HARoute._addFileChildren(HARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + HARoute: HARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/head/react/src/router.tsx b/benchmarks/ssr/scenarios/head/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/head/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/head/react/src/routes/__root.tsx new file mode 100644 index 00000000000..93f68352ab4 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/src/routes/__root.tsx @@ -0,0 +1,35 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: 'SSR head benchmark' }, + { name: 'description', content: 'Head-heavy SSR benchmark scenario' }, + { name: 'theme-color', content: '#111827' }, + { property: 'og:type', content: 'website' }, + ], + }), + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.$b.$c.tsx b/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.$b.$c.tsx new file mode 100644 index 00000000000..ee393494529 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.$b.$c.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a/$b/$c')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L3 ${params.a} ${params.b} ${params.c}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-3-meta-${index}`, + content: + index === 0 ? `shared-${params.c}-level-3` : `c-${params.c}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.c}-${index}.png`, + })), + }), + component: LevelCComponent, +}) + +function LevelCComponent() { + return

head-level-c

+} diff --git a/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.$b.tsx b/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.$b.tsx new file mode 100644 index 00000000000..b0658c87eba --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.$b.tsx @@ -0,0 +1,31 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a/$b')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L2 ${params.a} ${params.b}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-2-meta-${index}`, + content: + index === 0 ? `shared-${params.b}-level-2` : `c-${params.b}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.b}-${index}.png`, + })), + }), + component: LevelBComponent, +}) + +function LevelBComponent() { + return ( + <> +

head-level-b

+ + + ) +} diff --git a/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.tsx b/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.tsx new file mode 100644 index 00000000000..acd5f3c3ce7 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/src/routes/h.$a.tsx @@ -0,0 +1,31 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L1 ${params.a}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-1-meta-${index}`, + content: + index === 0 ? `shared-${params.a}-level-1` : `c-${params.a}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.a}-${index}.png`, + })), + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + return ( + <> +

head-level-a

+ + + ) +} diff --git a/benchmarks/ssr/scenarios/head/react/tsconfig.json b/benchmarks/ssr/scenarios/head/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/head/react/vite.config.ts b/benchmarks/ssr/scenarios/head/react/vite.config.ts new file mode 100644 index 00000000000..56d69443acd --- /dev/null +++ b/benchmarks/ssr/scenarios/head/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr head (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/head/shared.ts b/benchmarks/ssr/scenarios/head/shared.ts new file mode 100644 index 00000000000..817b64e9299 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/shared.ts @@ -0,0 +1,52 @@ +import { expect } from 'vitest' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const origin = 'http://localhost' +const dedupedMetaName = 'head-benchmark-shared' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +export const headBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +function buildHeadRequest(random: () => number) { + return new Request( + `${origin}/h/${randomSegment(random)}/${randomSegment(random)}/${randomSegment(random)}`, + requestInit, + ) +} + +export function runHeadLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: buildHeadRequest, + }) +} + +export async function assertHeadSanity(handler: StartRequestHandler) { + const a = 'sanity-a' + const b = 'sanity-b' + const c = 'sanity-c' + const response = await handler.fetch( + new Request(`${origin}/h/${a}/${b}/${c}`, requestInit), + ) + const body = await response.text() + const dedupedMetaCount = body.split(dedupedMetaName).length - 1 + + expect(response.status).toBe(200) + expect(body).toContain(`c-${c}-9`) + expect(dedupedMetaCount).toBe(1) + expect(body).toContain(`SSR Head L3 ${a} ${b} ${c}`) +} diff --git a/benchmarks/ssr/scenarios/head/solid/project.json b/benchmarks/ssr/scenarios/head/solid/project.json new file mode 100644 index 00000000000..c6df39998bb --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-head-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/head/solid/speed.bench.ts b/benchmarks/ssr/scenarios/head/solid/speed.bench.ts new file mode 100644 index 00000000000..ad5bf52809d --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/speed.bench.ts @@ -0,0 +1,17 @@ +import { bench, describe } from 'vitest' +import { assertHeadSanity, headBenchOptions, runHeadLoop } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertHeadSanity(handler) + +describe('ssr', () => { + bench('ssr head (solid)', () => runHeadLoop(handler), headBenchOptions) +}) diff --git a/benchmarks/ssr/scenarios/head/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/head/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..ceba45e94dd --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 HARouteImport } from './routes/h.$a' +import { Route as HABRouteImport } from './routes/h.$a.$b' +import { Route as HABCRouteImport } from './routes/h.$a.$b.$c' + +const HARoute = HARouteImport.update({ + id: '/h/$a', + path: '/h/$a', + getParentRoute: () => rootRouteImport, +} as any) +const HABRoute = HABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => HARoute, +} as any) +const HABCRoute = HABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => HABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRoutesByTo { + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + id: '__root__' | '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + HARoute: typeof HARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/h/$a': { + id: '/h/$a' + path: '/h/$a' + fullPath: '/h/$a' + preLoaderRoute: typeof HARouteImport + parentRoute: typeof rootRouteImport + } + '/h/$a/$b': { + id: '/h/$a/$b' + path: '/$b' + fullPath: '/h/$a/$b' + preLoaderRoute: typeof HABRouteImport + parentRoute: typeof HARoute + } + '/h/$a/$b/$c': { + id: '/h/$a/$b/$c' + path: '/$c' + fullPath: '/h/$a/$b/$c' + preLoaderRoute: typeof HABCRouteImport + parentRoute: typeof HABRoute + } + } +} + +interface HABRouteChildren { + HABCRoute: typeof HABCRoute +} + +const HABRouteChildren: HABRouteChildren = { + HABCRoute: HABCRoute, +} + +const HABRouteWithChildren = HABRoute._addFileChildren(HABRouteChildren) + +interface HARouteChildren { + HABRoute: typeof HABRouteWithChildren +} + +const HARouteChildren: HARouteChildren = { + HABRoute: HABRouteWithChildren, +} + +const HARouteWithChildren = HARoute._addFileChildren(HARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + HARoute: HARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/head/solid/src/router.tsx b/benchmarks/ssr/scenarios/head/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/head/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/head/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..2dde9d0ddf4 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/src/routes/__root.tsx @@ -0,0 +1,35 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: 'SSR head benchmark' }, + { name: 'description', content: 'Head-heavy SSR benchmark scenario' }, + { name: 'theme-color', content: '#111827' }, + { property: 'og:type', content: 'website' }, + ], + }), + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.$b.$c.tsx b/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.$b.$c.tsx new file mode 100644 index 00000000000..7ec132e9a61 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.$b.$c.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a/$b/$c')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L3 ${params.a} ${params.b} ${params.c}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-3-meta-${index}`, + content: + index === 0 ? `shared-${params.c}-level-3` : `c-${params.c}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.c}-${index}.png`, + })), + }), + component: LevelCComponent, +}) + +function LevelCComponent() { + return

head-level-c

+} diff --git a/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.$b.tsx b/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.$b.tsx new file mode 100644 index 00000000000..25d443ad4ea --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.$b.tsx @@ -0,0 +1,31 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a/$b')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L2 ${params.a} ${params.b}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-2-meta-${index}`, + content: + index === 0 ? `shared-${params.b}-level-2` : `c-${params.b}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.b}-${index}.png`, + })), + }), + component: LevelBComponent, +}) + +function LevelBComponent() { + return ( + <> +

head-level-b

+ + + ) +} diff --git a/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.tsx b/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.tsx new file mode 100644 index 00000000000..6c994ffed67 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/src/routes/h.$a.tsx @@ -0,0 +1,31 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L1 ${params.a}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-1-meta-${index}`, + content: + index === 0 ? `shared-${params.a}-level-1` : `c-${params.a}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.a}-${index}.png`, + })), + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + return ( + <> +

head-level-a

+ + + ) +} diff --git a/benchmarks/ssr/scenarios/head/solid/tsconfig.json b/benchmarks/ssr/scenarios/head/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/head/solid/vite.config.ts b/benchmarks/ssr/scenarios/head/solid/vite.config.ts new file mode 100644 index 00000000000..f7b97a44d13 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr head (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/head/vue/project.json b/benchmarks/ssr/scenarios/head/vue/project.json new file mode 100644 index 00000000000..62b17b1216d --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-head-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/head/vue/speed.bench.ts b/benchmarks/ssr/scenarios/head/vue/speed.bench.ts new file mode 100644 index 00000000000..5d8243b5889 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/speed.bench.ts @@ -0,0 +1,17 @@ +import { bench, describe } from 'vitest' +import { assertHeadSanity, headBenchOptions, runHeadLoop } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertHeadSanity(handler) + +describe('ssr', () => { + bench('ssr head (vue)', () => runHeadLoop(handler), headBenchOptions) +}) diff --git a/benchmarks/ssr/scenarios/head/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/head/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..9ec1029664c --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 HARouteImport } from './routes/h.$a' +import { Route as HABRouteImport } from './routes/h.$a.$b' +import { Route as HABCRouteImport } from './routes/h.$a.$b.$c' + +const HARoute = HARouteImport.update({ + id: '/h/$a', + path: '/h/$a', + getParentRoute: () => rootRouteImport, +} as any) +const HABRoute = HABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => HARoute, +} as any) +const HABCRoute = HABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => HABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRoutesByTo { + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/h/$a': typeof HARouteWithChildren + '/h/$a/$b': typeof HABRouteWithChildren + '/h/$a/$b/$c': typeof HABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + id: '__root__' | '/h/$a' | '/h/$a/$b' | '/h/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + HARoute: typeof HARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/h/$a': { + id: '/h/$a' + path: '/h/$a' + fullPath: '/h/$a' + preLoaderRoute: typeof HARouteImport + parentRoute: typeof rootRouteImport + } + '/h/$a/$b': { + id: '/h/$a/$b' + path: '/$b' + fullPath: '/h/$a/$b' + preLoaderRoute: typeof HABRouteImport + parentRoute: typeof HARoute + } + '/h/$a/$b/$c': { + id: '/h/$a/$b/$c' + path: '/$c' + fullPath: '/h/$a/$b/$c' + preLoaderRoute: typeof HABCRouteImport + parentRoute: typeof HABRoute + } + } +} + +interface HABRouteChildren { + HABCRoute: typeof HABCRoute +} + +const HABRouteChildren: HABRouteChildren = { + HABCRoute: HABCRoute, +} + +const HABRouteWithChildren = HABRoute._addFileChildren(HABRouteChildren) + +interface HARouteChildren { + HABRoute: typeof HABRouteWithChildren +} + +const HARouteChildren: HARouteChildren = { + HABRoute: HABRouteWithChildren, +} + +const HARouteWithChildren = HARoute._addFileChildren(HARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + HARoute: HARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/head/vue/src/router.tsx b/benchmarks/ssr/scenarios/head/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/head/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/head/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..9b95d2c48e8 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/src/routes/__root.tsx @@ -0,0 +1,37 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: 'SSR head benchmark' }, + { name: 'description', content: 'Head-heavy SSR benchmark scenario' }, + { name: 'theme-color', content: '#111827' }, + { property: 'og:type', content: 'website' }, + ], + }), + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.$b.$c.tsx b/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.$b.$c.tsx new file mode 100644 index 00000000000..91b8f03f7dc --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.$b.$c.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a/$b/$c')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L3 ${params.a} ${params.b} ${params.c}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-3-meta-${index}`, + content: + index === 0 ? `shared-${params.c}-level-3` : `c-${params.c}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.c}-${index}.png`, + })), + }), + component: LevelCComponent, +}) + +function LevelCComponent() { + return

head-level-c

+} diff --git a/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.$b.tsx b/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.$b.tsx new file mode 100644 index 00000000000..e92a619435c --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.$b.tsx @@ -0,0 +1,31 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a/$b')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L2 ${params.a} ${params.b}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-2-meta-${index}`, + content: + index === 0 ? `shared-${params.b}-level-2` : `c-${params.b}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.b}-${index}.png`, + })), + }), + component: LevelBComponent, +}) + +function LevelBComponent() { + return ( + <> +

head-level-b

+ + + ) +} diff --git a/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.tsx b/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.tsx new file mode 100644 index 00000000000..ddf407e9ae2 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/src/routes/h.$a.tsx @@ -0,0 +1,31 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const dedupedMetaName = 'head-benchmark-shared' + +export const Route = createFileRoute('/h/$a')({ + head: ({ params }) => ({ + meta: [ + { title: `SSR Head L1 ${params.a}` }, + ...Array.from({ length: 10 }, (_, index) => ({ + name: index === 0 ? dedupedMetaName : `level-1-meta-${index}`, + content: + index === 0 ? `shared-${params.a}-level-1` : `c-${params.a}-${index}`, + })), + ], + links: Array.from({ length: 4 }, (_, index) => ({ + rel: 'preload', + as: 'image', + href: `/img/${params.a}-${index}.png`, + })), + }), + component: LevelAComponent, +}) + +function LevelAComponent() { + return ( + <> +

head-level-a

+ + + ) +} diff --git a/benchmarks/ssr/scenarios/head/vue/tsconfig.json b/benchmarks/ssr/scenarios/head/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/head/vue/vite.config.ts b/benchmarks/ssr/scenarios/head/vue/vite.config.ts new file mode 100644 index 00000000000..e020012560c --- /dev/null +++ b/benchmarks/ssr/scenarios/head/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr head (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/loaders/react/project.json b/benchmarks/ssr/scenarios/loaders/react/project.json new file mode 100644 index 00000000000..0455f80c874 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-loaders-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/loaders/react/speed.bench.ts b/benchmarks/ssr/scenarios/loaders/react/speed.bench.ts new file mode 100644 index 00000000000..a5eecfc6487 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/speed.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { + assertLoadersSanity, + benchOptions, + runLoadersLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertLoadersSanity(handler) + +describe('ssr', () => { + bench('ssr loaders (react)', () => runLoadersLoop(handler), benchOptions) +}) diff --git a/benchmarks/ssr/scenarios/loaders/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/loaders/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..068b5a54335 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 ARouteImport } from './routes/$a' +import { Route as ABRouteImport } from './routes/$a.$b' +import { Route as ABCRouteImport } from './routes/$a.$b.$c' + +const ARoute = ARouteImport.update({ + id: '/$a', + path: '/$a', + getParentRoute: () => rootRouteImport, +} as any) +const ABRoute = ABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => ARoute, +} as any) +const ABCRoute = ABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => ABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/$a': { + id: '/$a' + path: '/$a' + fullPath: '/$a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + '/$a/$b': { + id: '/$a/$b' + path: '/$b' + fullPath: '/$a/$b' + preLoaderRoute: typeof ABRouteImport + parentRoute: typeof ARoute + } + '/$a/$b/$c': { + id: '/$a/$b/$c' + path: '/$c' + fullPath: '/$a/$b/$c' + preLoaderRoute: typeof ABCRouteImport + parentRoute: typeof ABRoute + } + } +} + +interface ABRouteChildren { + ABCRoute: typeof ABCRoute +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRoute, +} + +const ABRouteWithChildren = ABRoute._addFileChildren(ABRouteChildren) + +interface ARouteChildren { + ABRoute: typeof ABRouteWithChildren +} + +const ARouteChildren: ARouteChildren = { + ABRoute: ABRouteWithChildren, +} + +const ARouteWithChildren = ARoute._addFileChildren(ARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/loaders/react/src/router.tsx b/benchmarks/ssr/scenarios/loaders/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..746a26aeb68 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.$b.$c.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a/$b/$c')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxC: `v-${params.c}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.c, deps.page) + }, + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.meta.label}

+
    + {data.items.slice(0, 10).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.$b.tsx b/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..79c10c06b09 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.$b.tsx @@ -0,0 +1,33 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a/$b')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxB: `v-${params.b}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.b, deps.page) + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.meta.label}

+
    + {data.items.slice(0, 10).map((item) => ( +
  • {item.name}
  • + ))} +
+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.tsx b/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.tsx new file mode 100644 index 00000000000..980a495c32d --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/src/routes/$a.tsx @@ -0,0 +1,33 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxA: `v-${params.a}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.a, deps.page) + }, + component: LevelAComponent, +}) + +function LevelAComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.meta.label}

+
    + {data.items.slice(0, 10).map((item) => ( +
  • {item.name}
  • + ))} +
+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/loaders/react/src/routes/__root.tsx new file mode 100644 index 00000000000..001639cb399 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/src/routes/__root.tsx @@ -0,0 +1,28 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s: Record) => ({ + page: typeof s.page === 'number' ? s.page : 0, + tags: Array.isArray(s.tags) ? (s.tags as Array) : [], + }), +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/loaders/react/tsconfig.json b/benchmarks/ssr/scenarios/loaders/react/tsconfig.json new file mode 100644 index 00000000000..8800818162e --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/loaders/react/vite.config.ts b/benchmarks/ssr/scenarios/loaders/react/vite.config.ts new file mode 100644 index 00000000000..5c4dcaa40d4 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr loaders (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/loaders/shared-bench.ts b/benchmarks/ssr/scenarios/loaders/shared-bench.ts new file mode 100644 index 00000000000..5e7156a28f0 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/shared-bench.ts @@ -0,0 +1,72 @@ +import { makeLevelData } from './shared-data' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildLoaderRequest(random: () => number, index: number) { + const suffix = index.toString(36) + const a = `${randomSegment(random)}-${suffix}` + const b = `${randomSegment(random)}-${suffix}` + const c = `${randomSegment(random)}-${suffix}` + const page = Math.floor(random() * 10) + const tags = JSON.stringify([ + `t-${randomSegment(random)}-${suffix}`, + `t-${randomSegment(random)}-${suffix}`, + ]) + + return new Request( + `http://localhost/${a}/${b}/${c}?page=${page}&tags=${encodeURIComponent(tags)}`, + requestInit, + ) +} + +export async function assertLoadersSanity(handler: StartRequestHandler) { + const page = 3 + const leafSource = 'c-sanity' + const tags = JSON.stringify(['t-x', 't-y']) + const response = await handler.fetch( + new Request( + `http://localhost/a-sanity/b-sanity/${leafSource}?page=${page}&tags=${encodeURIComponent(tags)}`, + requestInit, + ), + ) + const body = await response.text() + const leafMarker = makeLevelData(leafSource, page).items[0]?.name + + if (response.status !== 200) { + throw new Error( + `Expected setup request status 200, received ${response.status}`, + ) + } + + if (!leafMarker || !body.includes(leafMarker)) { + throw new Error('Expected setup response to include the leaf loader item') + } + + if (!body.includes('$_TSR')) { + throw new Error('Expected setup response to include the dehydration marker') + } +} + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runLoadersLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: buildLoaderRequest, + }) +} diff --git a/benchmarks/ssr/scenarios/loaders/shared-data.ts b/benchmarks/ssr/scenarios/loaders/shared-data.ts new file mode 100644 index 00000000000..d907f7d66cf --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/shared-data.ts @@ -0,0 +1,54 @@ +export interface LevelDataItem { + id: string + name: string + flags: number +} + +export interface LevelData { + items: Array + meta: { + source: string + page: number + label: string + } +} + +function hashLevelInput(input: string, salt: number) { + let value = salt >>> 0 + + for (let index = 0; index < input.length; index++) { + value = (value * 33 + input.charCodeAt(index) + index) >>> 0 + } + + for (let index = 0; index < 16; index++) { + value = (value ^ (value << 13)) >>> 0 + value = (value ^ (value >> 17)) >>> 0 + value = (value ^ (value << 5)) >>> 0 + } + + return value +} + +export function makeLevelData(source: string, page: number): LevelData { + const safePage = page | 0 + const seedInput = `${source}:${safePage}` + const seed = hashLevelInput(seedInput, 0x9e3779b9) + + return { + items: Array.from({ length: 50 }, (_, index) => { + const hash = hashLevelInput(`${seedInput}:${index}`, seed + index) + const hashLabel = (hash & 0xffff).toString(36) + + return { + id: `${index.toString(36)}-${hashLabel}`, + name: `${source}:${safePage}:item-${index}`, + flags: hash & 15, + } + }), + meta: { + source, + page: safePage, + label: `source ${source} page ${safePage}`, + }, + } +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/project.json b/benchmarks/ssr/scenarios/loaders/solid/project.json new file mode 100644 index 00000000000..2cb0e92acd5 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-loaders-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/speed.bench.ts b/benchmarks/ssr/scenarios/loaders/solid/speed.bench.ts new file mode 100644 index 00000000000..7b3f6247ea0 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/speed.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { + assertLoadersSanity, + benchOptions, + runLoadersLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertLoadersSanity(handler) + +describe('ssr', () => { + bench('ssr loaders (solid)', () => runLoadersLoop(handler), benchOptions) +}) diff --git a/benchmarks/ssr/scenarios/loaders/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/loaders/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..7b299cfcd1e --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 ARouteImport } from './routes/$a' +import { Route as ABRouteImport } from './routes/$a.$b' +import { Route as ABCRouteImport } from './routes/$a.$b.$c' + +const ARoute = ARouteImport.update({ + id: '/$a', + path: '/$a', + getParentRoute: () => rootRouteImport, +} as any) +const ABRoute = ABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => ARoute, +} as any) +const ABCRoute = ABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => ABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/$a': { + id: '/$a' + path: '/$a' + fullPath: '/$a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + '/$a/$b': { + id: '/$a/$b' + path: '/$b' + fullPath: '/$a/$b' + preLoaderRoute: typeof ABRouteImport + parentRoute: typeof ARoute + } + '/$a/$b/$c': { + id: '/$a/$b/$c' + path: '/$c' + fullPath: '/$a/$b/$c' + preLoaderRoute: typeof ABCRouteImport + parentRoute: typeof ABRoute + } + } +} + +interface ABRouteChildren { + ABCRoute: typeof ABCRoute +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRoute, +} + +const ABRouteWithChildren = ABRoute._addFileChildren(ABRouteChildren) + +interface ARouteChildren { + ABRoute: typeof ABRouteWithChildren +} + +const ARouteChildren: ARouteChildren = { + ABRoute: ABRouteWithChildren, +} + +const ARouteWithChildren = ARoute._addFileChildren(ARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/src/router.tsx b/benchmarks/ssr/scenarios/loaders/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..9ead32440ab --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.$b.$c.tsx @@ -0,0 +1,34 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a/$b/$c')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxC: `v-${params.c}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.c, deps.page) + }, + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().meta.label}

+
    + {data() + .items.slice(0, 10) + .map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.$b.tsx b/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..c0abf3da874 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.$b.tsx @@ -0,0 +1,35 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a/$b')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxB: `v-${params.b}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.b, deps.page) + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().meta.label}

+
    + {data() + .items.slice(0, 10) + .map((item) => ( +
  • {item.name}
  • + ))} +
+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.tsx b/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.tsx new file mode 100644 index 00000000000..d4608523b8c --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/src/routes/$a.tsx @@ -0,0 +1,35 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxA: `v-${params.a}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.a, deps.page) + }, + component: LevelAComponent, +}) + +function LevelAComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().meta.label}

+
    + {data() + .items.slice(0, 10) + .map((item) => ( +
  • {item.name}
  • + ))} +
+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/loaders/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..2f4b1de4106 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/src/routes/__root.tsx @@ -0,0 +1,28 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s: Record) => ({ + page: typeof s.page === 'number' ? s.page : 0, + tags: Array.isArray(s.tags) ? (s.tags as Array) : [], + }), +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/tsconfig.json b/benchmarks/ssr/scenarios/loaders/solid/tsconfig.json new file mode 100644 index 00000000000..47f264ba4f5 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/loaders/solid/vite.config.ts b/benchmarks/ssr/scenarios/loaders/solid/vite.config.ts new file mode 100644 index 00000000000..527ca977c53 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr loaders (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/loaders/vue/project.json b/benchmarks/ssr/scenarios/loaders/vue/project.json new file mode 100644 index 00000000000..2a98cb57f94 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-loaders-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/speed.bench.ts b/benchmarks/ssr/scenarios/loaders/vue/speed.bench.ts new file mode 100644 index 00000000000..fb39e0652c0 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/speed.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { + assertLoadersSanity, + benchOptions, + runLoadersLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertLoadersSanity(handler) + +describe('ssr', () => { + bench('ssr loaders (vue)', () => runLoadersLoop(handler), benchOptions) +}) diff --git a/benchmarks/ssr/scenarios/loaders/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/loaders/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..24ee6c3b9d6 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 ARouteImport } from './routes/$a' +import { Route as ABRouteImport } from './routes/$a.$b' +import { Route as ABCRouteImport } from './routes/$a.$b.$c' + +const ARoute = ARouteImport.update({ + id: '/$a', + path: '/$a', + getParentRoute: () => rootRouteImport, +} as any) +const ABRoute = ABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => ARoute, +} as any) +const ABCRoute = ABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => ABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/$a': { + id: '/$a' + path: '/$a' + fullPath: '/$a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + '/$a/$b': { + id: '/$a/$b' + path: '/$b' + fullPath: '/$a/$b' + preLoaderRoute: typeof ABRouteImport + parentRoute: typeof ARoute + } + '/$a/$b/$c': { + id: '/$a/$b/$c' + path: '/$c' + fullPath: '/$a/$b/$c' + preLoaderRoute: typeof ABCRouteImport + parentRoute: typeof ABRoute + } + } +} + +interface ABRouteChildren { + ABCRoute: typeof ABCRoute +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRoute, +} + +const ABRouteWithChildren = ABRoute._addFileChildren(ABRouteChildren) + +interface ARouteChildren { + ABRoute: typeof ABRouteWithChildren +} + +const ARouteChildren: ARouteChildren = { + ABRoute: ABRouteWithChildren, +} + +const ARouteWithChildren = ARoute._addFileChildren(ARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/src/router.tsx b/benchmarks/ssr/scenarios/loaders/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..de927a8092f --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.$b.$c.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a/$b/$c')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxC: `v-${params.c}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.c, deps.page) + }, + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.meta.label}

+
    + {data.value.items.slice(0, 10).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.$b.tsx b/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..e1a5f52ae2d --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.$b.tsx @@ -0,0 +1,33 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a/$b')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxB: `v-${params.b}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.b, deps.page) + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.meta.label}

+
    + {data.value.items.slice(0, 10).map((item) => ( +
  • {item.name}
  • + ))} +
+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.tsx b/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.tsx new file mode 100644 index 00000000000..b197a8edf15 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/src/routes/$a.tsx @@ -0,0 +1,33 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { makeLevelData } from '../../../shared-data' + +export const Route = createFileRoute('/$a')({ + beforeLoad: ({ params, context }) => { + void context + + return { ctxA: `v-${params.a}` } + }, + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ params, deps, context }) => { + void context + + return makeLevelData(params.a, deps.page) + }, + component: LevelAComponent, +}) + +function LevelAComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.meta.label}

+
    + {data.value.items.slice(0, 10).map((item) => ( +
  • {item.name}
  • + ))} +
+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/loaders/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..0479fb1af42 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/src/routes/__root.tsx @@ -0,0 +1,30 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s: Record) => ({ + page: typeof s.page === 'number' ? s.page : 0, + tags: Array.isArray(s.tags) ? (s.tags as Array) : [], + }), +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/tsconfig.json b/benchmarks/ssr/scenarios/loaders/vue/tsconfig.json new file mode 100644 index 00000000000..c302f637701 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/loaders/vue/vite.config.ts b/benchmarks/ssr/scenarios/loaders/vue/vite.config.ts new file mode 100644 index 00000000000..b7a4d1b48b6 --- /dev/null +++ b/benchmarks/ssr/scenarios/loaders/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr loaders (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/rewrites/react/project.json b/benchmarks/ssr/scenarios/rewrites/react/project.json new file mode 100644 index 00000000000..84d4846ec5e --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-rewrites-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/speed.bench.ts b/benchmarks/ssr/scenarios/rewrites/react/speed.bench.ts new file mode 100644 index 00000000000..d7025ce0f9d --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/speed.bench.ts @@ -0,0 +1,31 @@ +import { bench, describe } from 'vitest' +import { + assertRewriteScenario, + rewriteBenchOptions, + runRewriteLocalizedLoop, + runRewritePassthroughLoop, + type StartRequestHandler, +} from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertRewriteScenario(handler) + +describe('ssr', () => { + bench( + 'ssr rewrite localized (react)', + () => runRewriteLocalizedLoop(handler), + rewriteBenchOptions, + ) + bench( + 'ssr rewrite passthrough (react)', + () => runRewritePassthroughLoop(handler), + rewriteBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/rewrites/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/rewrites/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..e893d06a575 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/src/routeTree.gen.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/p/$a' | '/p/$a/$b' + id: '__root__' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/src/router.tsx b/benchmarks/ssr/scenarios/rewrites/react/src/router.tsx new file mode 100644 index 00000000000..e9e0b2ce9a3 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/src/router.tsx @@ -0,0 +1,42 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const localePrefixRe = /^\/(fr|de|es)(\/.*)$/ + +export function getRouter() { + return createRouter({ + routeTree, + basepath: '/app', + defaultPreload: false, + scrollRestoration: false, + rewrite: { + input: ({ url }) => { + const match = url.pathname.match(localePrefixRe) + + if (match) { + url.pathname = match[2]! + url.searchParams.set('_locale', match[1]!) + return url + } + + return undefined + }, + output: ({ url }) => { + const locale = url.searchParams.get('_locale') + + if (locale) { + url.searchParams.delete('_locale') + url.pathname = `/${locale}${url.pathname}` + } + + return url + }, + }, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/rewrites/react/src/routes/__root.tsx new file mode 100644 index 00000000000..7cdcfe1a374 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s: Record) => ({ + _locale: typeof s._locale === 'string' ? s._locale : undefined, + }), +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/src/routes/p.$a.$b.tsx b/benchmarks/ssr/scenarios/rewrites/react/src/routes/p.$a.$b.tsx new file mode 100644 index 00000000000..756109b844f --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/src/routes/p.$a.$b.tsx @@ -0,0 +1,39 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/p/$a/$b')({ + loader: ({ params }) => ({ + a: params.a, + b: params.b, + nextB: `next-${params.b}`, + }), + component: LeafComponent, +}) + +function LeafComponent() { + const data = Route.useLoaderData() + + return ( +
+

+ rewrite-leaf {data.a} {data.b} +

+ + leaf-self-link + + + leaf-next-link + + + leaf-parent-link + +
+ ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/src/routes/p.$a.tsx b/benchmarks/ssr/scenarios/rewrites/react/src/routes/p.$a.tsx new file mode 100644 index 00000000000..7b0a778a18a --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/src/routes/p.$a.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/p/$a')({ + loader: ({ params }) => ({ + a: params.a, + nextB: `branch-${params.a}`, + }), + component: ParentComponent, +}) + +function ParentComponent() { + const data = Route.useLoaderData() + + return ( +
+

rewrite-parent {data.a}

+ + parent-branch-link + + +
+ ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/tsconfig.json b/benchmarks/ssr/scenarios/rewrites/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/rewrites/react/vite.config.ts b/benchmarks/ssr/scenarios/rewrites/react/vite.config.ts new file mode 100644 index 00000000000..5bf098275bd --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/react/vite.config.ts @@ -0,0 +1,32 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + router: { + basepath: '/app', + }, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr rewrites (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/rewrites/shared.ts b/benchmarks/ssr/scenarios/rewrites/shared.ts new file mode 100644 index 00000000000..f197d88de28 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/shared.ts @@ -0,0 +1,85 @@ +import { expect } from 'vitest' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const origin = 'http://localhost' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +export const rewriteBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +function buildLocalizedRequest(random: () => number) { + return new Request( + `${origin}/app/fr/p/${randomSegment(random)}/${randomSegment(random)}`, + requestInit, + ) +} + +function buildPassthroughRequest(random: () => number) { + return new Request( + `${origin}/app/p/${randomSegment(random)}/${randomSegment(random)}`, + requestInit, + ) +} + +export function runRewriteLocalizedLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: buildLocalizedRequest, + }) +} + +export function runRewritePassthroughLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: buildPassthroughRequest, + }) +} + +export async function assertRewriteScenario(handler: StartRequestHandler) { + const localizedResponse = await handler.fetch( + new Request(`${origin}/app/fr/p/sanity-a/sanity-b`, requestInit), + ) + const localizedBody = await localizedResponse.text() + + expect(localizedResponse.status).toBe(200) + expect(localizedBody).toContain('rewrite-leaf') + expect(localizedBody).toContain('sanity-a') + expect(localizedBody).toContain('sanity-b') + expect(localizedBody).toContain('href="/app/fr/p/sanity-a/sanity-b"') + expect(localizedBody).toContain('href="/app/fr/p/sanity-a/next-sanity-b"') + + const passthroughResponse = await handler.fetch( + new Request(`${origin}/app/p/sanity-a/sanity-b`, requestInit), + ) + const passthroughBody = await passthroughResponse.text() + + expect(passthroughResponse.status).toBe(200) + expect(passthroughBody).toContain('rewrite-leaf') + expect(passthroughBody).toContain('sanity-a') + expect(passthroughBody).toContain('sanity-b') + expect(passthroughBody).toContain('href="/app/fr/p/sanity-a/sanity-b"') + expect(passthroughBody).toContain('href="/app/fr/p/sanity-a/next-sanity-b"') + + const unprefixedResponse = await handler.fetch( + new Request(`${origin}/p/sanity-a/sanity-b`, requestInit), + ) + await unprefixedResponse.text() + + expect(unprefixedResponse.status).toBe(307) + expect(unprefixedResponse.headers.get('location')).toBe( + '/app/p/sanity-a/sanity-b', + ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/project.json b/benchmarks/ssr/scenarios/rewrites/solid/project.json new file mode 100644 index 00000000000..7c993a14049 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-rewrites-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/speed.bench.ts b/benchmarks/ssr/scenarios/rewrites/solid/speed.bench.ts new file mode 100644 index 00000000000..7aa4bd0b182 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/speed.bench.ts @@ -0,0 +1,31 @@ +import { bench, describe } from 'vitest' +import { + assertRewriteScenario, + rewriteBenchOptions, + runRewriteLocalizedLoop, + runRewritePassthroughLoop, + type StartRequestHandler, +} from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertRewriteScenario(handler) + +describe('ssr', () => { + bench( + 'ssr rewrite localized (solid)', + () => runRewriteLocalizedLoop(handler), + rewriteBenchOptions, + ) + bench( + 'ssr rewrite passthrough (solid)', + () => runRewritePassthroughLoop(handler), + rewriteBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/rewrites/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/rewrites/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..b3b6b24a381 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/src/routeTree.gen.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/p/$a' | '/p/$a/$b' + id: '__root__' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/src/router.tsx b/benchmarks/ssr/scenarios/rewrites/solid/src/router.tsx new file mode 100644 index 00000000000..9806e59d0e3 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/src/router.tsx @@ -0,0 +1,42 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +const localePrefixRe = /^\/(fr|de|es)(\/.*)$/ + +export function getRouter() { + return createRouter({ + routeTree, + basepath: '/app', + defaultPreload: false, + scrollRestoration: false, + rewrite: { + input: ({ url }) => { + const match = url.pathname.match(localePrefixRe) + + if (match) { + url.pathname = match[2]! + url.searchParams.set('_locale', match[1]!) + return url + } + + return undefined + }, + output: ({ url }) => { + const locale = url.searchParams.get('_locale') + + if (locale) { + url.searchParams.delete('_locale') + url.pathname = `/${locale}${url.pathname}` + } + + return url + }, + }, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/rewrites/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..1a9cd6c423f --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s: Record) => ({ + _locale: typeof s._locale === 'string' ? s._locale : undefined, + }), +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx b/benchmarks/ssr/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx new file mode 100644 index 00000000000..eacb45a8417 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx @@ -0,0 +1,39 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/p/$a/$b')({ + loader: ({ params }) => ({ + a: params.a, + b: params.b, + nextB: `next-${params.b}`, + }), + component: LeafComponent, +}) + +function LeafComponent() { + const data = Route.useLoaderData() + + return ( +
+

+ rewrite-leaf {data().a} {data().b} +

+ + leaf-self-link + + + leaf-next-link + + + leaf-parent-link + +
+ ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/src/routes/p.$a.tsx b/benchmarks/ssr/scenarios/rewrites/solid/src/routes/p.$a.tsx new file mode 100644 index 00000000000..1845e3ebbad --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/src/routes/p.$a.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/p/$a')({ + loader: ({ params }) => ({ + a: params.a, + nextB: `branch-${params.a}`, + }), + component: ParentComponent, +}) + +function ParentComponent() { + const data = Route.useLoaderData() + + return ( +
+

rewrite-parent {data().a}

+ + parent-branch-link + + +
+ ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/tsconfig.json b/benchmarks/ssr/scenarios/rewrites/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/rewrites/solid/vite.config.ts b/benchmarks/ssr/scenarios/rewrites/solid/vite.config.ts new file mode 100644 index 00000000000..ae9f925cb72 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/solid/vite.config.ts @@ -0,0 +1,37 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + router: { + basepath: '/app', + }, + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr rewrites (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/rewrites/vue/project.json b/benchmarks/ssr/scenarios/rewrites/vue/project.json new file mode 100644 index 00000000000..10fd6f4f349 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-rewrites-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/speed.bench.ts b/benchmarks/ssr/scenarios/rewrites/vue/speed.bench.ts new file mode 100644 index 00000000000..0bbd896d711 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/speed.bench.ts @@ -0,0 +1,31 @@ +import { bench, describe } from 'vitest' +import { + assertRewriteScenario, + rewriteBenchOptions, + runRewriteLocalizedLoop, + runRewritePassthroughLoop, + type StartRequestHandler, +} from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertRewriteScenario(handler) + +describe('ssr', () => { + bench( + 'ssr rewrite localized (vue)', + () => runRewriteLocalizedLoop(handler), + rewriteBenchOptions, + ) + bench( + 'ssr rewrite passthrough (vue)', + () => runRewritePassthroughLoop(handler), + rewriteBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/rewrites/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/rewrites/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..4440c1cfda2 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/src/routeTree.gen.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/p/$a' | '/p/$a/$b' + id: '__root__' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/src/router.tsx b/benchmarks/ssr/scenarios/rewrites/vue/src/router.tsx new file mode 100644 index 00000000000..3a7389a5bea --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/src/router.tsx @@ -0,0 +1,42 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +const localePrefixRe = /^\/(fr|de|es)(\/.*)$/ + +export function getRouter() { + return createRouter({ + routeTree, + basepath: '/app', + defaultPreload: false, + scrollRestoration: false, + rewrite: { + input: ({ url }) => { + const match = url.pathname.match(localePrefixRe) + + if (match) { + url.pathname = match[2]! + url.searchParams.set('_locale', match[1]!) + return url + } + + return undefined + }, + output: ({ url }) => { + const locale = url.searchParams.get('_locale') + + if (locale) { + url.searchParams.delete('_locale') + url.pathname = `/${locale}${url.pathname}` + } + + return url + }, + }, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/rewrites/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..ed4cf1300df --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s: Record) => ({ + _locale: typeof s._locale === 'string' ? s._locale : undefined, + }), +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx b/benchmarks/ssr/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx new file mode 100644 index 00000000000..3fdb7014f45 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx @@ -0,0 +1,39 @@ +import { Link, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/p/$a/$b')({ + loader: ({ params }) => ({ + a: params.a, + b: params.b, + nextB: `next-${params.b}`, + }), + component: LeafComponent, +}) + +function LeafComponent() { + const data = Route.useLoaderData() + + return ( +
+

+ rewrite-leaf {data.value.a} {data.value.b} +

+ + leaf-self-link + + + leaf-next-link + + + leaf-parent-link + +
+ ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/src/routes/p.$a.tsx b/benchmarks/ssr/scenarios/rewrites/vue/src/routes/p.$a.tsx new file mode 100644 index 00000000000..acb74aabbd8 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/src/routes/p.$a.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/p/$a')({ + loader: ({ params }) => ({ + a: params.a, + nextB: `branch-${params.a}`, + }), + component: ParentComponent, +}) + +function ParentComponent() { + const data = Route.useLoaderData() + + return ( +
+

rewrite-parent {data.value.a}

+ + parent-branch-link + + +
+ ) +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/tsconfig.json b/benchmarks/ssr/scenarios/rewrites/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/rewrites/vue/vite.config.ts b/benchmarks/ssr/scenarios/rewrites/vue/vite.config.ts new file mode 100644 index 00000000000..f3b69dfe204 --- /dev/null +++ b/benchmarks/ssr/scenarios/rewrites/vue/vite.config.ts @@ -0,0 +1,32 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + router: { + basepath: '/app', + }, + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr rewrites (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/project.json b/benchmarks/ssr/scenarios/selective-ssr/react/project.json new file mode 100644 index 00000000000..77858e2246b --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-selective-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/speed.bench.ts b/benchmarks/ssr/scenarios/selective-ssr/react/speed.bench.ts new file mode 100644 index 00000000000..e0e8a0e3c45 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/speed.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { + assertSelectiveSanity, + benchOptions, + runSelectiveLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertSelectiveSanity(handler) + +describe('ssr', () => { + bench('ssr selective (react)', () => runSelectiveLoop(handler), benchOptions) +}) diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/selective-ssr/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..95395c294ec --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 MixARouteImport } from './routes/mix.$a' +import { Route as MixABRouteImport } from './routes/mix.$a.$b' +import { Route as MixABCRouteImport } from './routes/mix.$a.$b.$c' + +const MixARoute = MixARouteImport.update({ + id: '/mix/$a', + path: '/mix/$a', + getParentRoute: () => rootRouteImport, +} as any) +const MixABRoute = MixABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => MixARoute, +} as any) +const MixABCRoute = MixABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => MixABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRoutesByTo { + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + id: '__root__' | '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + MixARoute: typeof MixARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/mix/$a': { + id: '/mix/$a' + path: '/mix/$a' + fullPath: '/mix/$a' + preLoaderRoute: typeof MixARouteImport + parentRoute: typeof rootRouteImport + } + '/mix/$a/$b': { + id: '/mix/$a/$b' + path: '/$b' + fullPath: '/mix/$a/$b' + preLoaderRoute: typeof MixABRouteImport + parentRoute: typeof MixARoute + } + '/mix/$a/$b/$c': { + id: '/mix/$a/$b/$c' + path: '/$c' + fullPath: '/mix/$a/$b/$c' + preLoaderRoute: typeof MixABCRouteImport + parentRoute: typeof MixABRoute + } + } +} + +interface MixABRouteChildren { + MixABCRoute: typeof MixABCRoute +} + +const MixABRouteChildren: MixABRouteChildren = { + MixABCRoute: MixABCRoute, +} + +const MixABRouteWithChildren = MixABRoute._addFileChildren(MixABRouteChildren) + +interface MixARouteChildren { + MixABRoute: typeof MixABRouteWithChildren +} + +const MixARouteChildren: MixARouteChildren = { + MixABRoute: MixABRouteWithChildren, +} + +const MixARouteWithChildren = MixARoute._addFileChildren(MixARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + MixARoute: MixARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/src/router.tsx b/benchmarks/ssr/scenarios/selective-ssr/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/__root.tsx new file mode 100644 index 00000000000..1973ca20bb1 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/__root.tsx @@ -0,0 +1,25 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.$b.$c.tsx b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.$b.$c.tsx new file mode 100644 index 00000000000..e3df06149de --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.$b.$c.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a/$b/$c')({ + ssr: false, + loader: async ({ params }) => { + return { + marker: `level-c-loader-${params.c}`, + data: makeLevelData(`level-c-data-${params.c}`, 3), + } + }, + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`csr-rendered-${params.c}`}

+

{data.marker}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.$b.tsx b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.$b.tsx new file mode 100644 index 00000000000..3fe26dd70e5 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.$b.tsx @@ -0,0 +1,26 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a/$b')({ + ssr: 'data-only', + loader: async ({ params }) => { + return { + marker: `level-b-loader-${params.b}`, + data: makeLevelData(`level-b-data-${params.b}`, 2), + } + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`data-only-rendered-${params.b}`}

+

{data.marker}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.tsx b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.tsx new file mode 100644 index 00000000000..a06c9ad1b9b --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/src/routes/mix.$a.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a')({ + ssr: true, + loader: async ({ params }) => { + return makeLevelData(`level-a-loader-${params.a}`, 1) + }, + component: LevelAComponent, +}) + +function LevelAComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`level-a-rendered-${params.a}`}

+

{data.items[0]?.name}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/tsconfig.json b/benchmarks/ssr/scenarios/selective-ssr/react/tsconfig.json new file mode 100644 index 00000000000..399fe944f99 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../loaders/shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/react/vite.config.ts b/benchmarks/ssr/scenarios/selective-ssr/react/vite.config.ts new file mode 100644 index 00000000000..03ff77aa7b9 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr selective-ssr (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/selective-ssr/shared-bench.ts b/benchmarks/ssr/scenarios/selective-ssr/shared-bench.ts new file mode 100644 index 00000000000..37743641c20 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/shared-bench.ts @@ -0,0 +1,92 @@ +import { makeLevelData } from '../loaders/shared-data' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildSelectiveRequest(random: () => number) { + const a = randomSegment(random) + const b = randomSegment(random) + const c = randomSegment(random) + + return new Request(`http://localhost/mix/${a}/${b}/${c}`, requestInit) +} + +export async function assertSelectiveSanity(handler: StartRequestHandler) { + const a = 'a-sanity' + const b = 'b-sanity' + const c = 'c-sanity' + const response = await handler.fetch( + new Request(`http://localhost/mix/${a}/${b}/${c}`, requestInit), + ) + const body = await response.text() + const levelARenderedMarker = `level-a-rendered-${a}` + const levelALoaderMarker = makeLevelData(`level-a-loader-${a}`, 1).items[0] + ?.name + const dataOnlyRenderedMarker = `data-only-rendered-${b}` + const levelBLoaderMarker = `level-b-loader-${b}` + const csrRenderedMarker = `csr-rendered-${c}` + const levelCLoaderMarker = `level-c-loader-${c}` + + if (response.status !== 200) { + throw new Error( + `Expected setup request status 200, received ${response.status}`, + ) + } + + if (!body.includes(levelARenderedMarker)) { + throw new Error( + 'Expected setup response to include level-a rendered content', + ) + } + + if (!levelALoaderMarker || !body.includes(levelALoaderMarker)) { + throw new Error('Expected setup response to include level-a loader content') + } + + if (body.includes(dataOnlyRenderedMarker)) { + throw new Error( + 'Expected data-only route component to be absent from SSR HTML', + ) + } + + const hydrationIndex = body.indexOf('$_TSR') + + if (hydrationIndex === -1) { + throw new Error('Expected setup response to include the dehydration marker') + } + + if (!body.slice(hydrationIndex).includes(levelBLoaderMarker)) { + throw new Error('Expected level-b loader marker in the dehydration payload') + } + + if (body.includes(csrRenderedMarker)) { + throw new Error('Expected csr route component to be absent from SSR HTML') + } + + if (body.includes(levelCLoaderMarker)) { + throw new Error('Expected level-c loader marker to be absent from SSR HTML') + } +} + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runSelectiveLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: buildSelectiveRequest, + }) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/project.json b/benchmarks/ssr/scenarios/selective-ssr/solid/project.json new file mode 100644 index 00000000000..358cc973233 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-selective-ssr-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/speed.bench.ts b/benchmarks/ssr/scenarios/selective-ssr/solid/speed.bench.ts new file mode 100644 index 00000000000..6a2803d45f5 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/speed.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { + assertSelectiveSanity, + benchOptions, + runSelectiveLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertSelectiveSanity(handler) + +describe('ssr', () => { + bench('ssr selective (solid)', () => runSelectiveLoop(handler), benchOptions) +}) diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..22560063f6b --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 MixARouteImport } from './routes/mix.$a' +import { Route as MixABRouteImport } from './routes/mix.$a.$b' +import { Route as MixABCRouteImport } from './routes/mix.$a.$b.$c' + +const MixARoute = MixARouteImport.update({ + id: '/mix/$a', + path: '/mix/$a', + getParentRoute: () => rootRouteImport, +} as any) +const MixABRoute = MixABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => MixARoute, +} as any) +const MixABCRoute = MixABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => MixABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRoutesByTo { + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + id: '__root__' | '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + MixARoute: typeof MixARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/mix/$a': { + id: '/mix/$a' + path: '/mix/$a' + fullPath: '/mix/$a' + preLoaderRoute: typeof MixARouteImport + parentRoute: typeof rootRouteImport + } + '/mix/$a/$b': { + id: '/mix/$a/$b' + path: '/$b' + fullPath: '/mix/$a/$b' + preLoaderRoute: typeof MixABRouteImport + parentRoute: typeof MixARoute + } + '/mix/$a/$b/$c': { + id: '/mix/$a/$b/$c' + path: '/$c' + fullPath: '/mix/$a/$b/$c' + preLoaderRoute: typeof MixABCRouteImport + parentRoute: typeof MixABRoute + } + } +} + +interface MixABRouteChildren { + MixABCRoute: typeof MixABCRoute +} + +const MixABRouteChildren: MixABRouteChildren = { + MixABCRoute: MixABCRoute, +} + +const MixABRouteWithChildren = MixABRoute._addFileChildren(MixABRouteChildren) + +interface MixARouteChildren { + MixABRoute: typeof MixABRouteWithChildren +} + +const MixARouteChildren: MixARouteChildren = { + MixABRoute: MixABRouteWithChildren, +} + +const MixARouteWithChildren = MixARoute._addFileChildren(MixARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + MixARoute: MixARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/src/router.tsx b/benchmarks/ssr/scenarios/selective-ssr/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..15de858e78c --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/__root.tsx @@ -0,0 +1,25 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.$b.$c.tsx b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.$b.$c.tsx new file mode 100644 index 00000000000..f6a85bfdc2f --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.$b.$c.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a/$b/$c')({ + ssr: false, + loader: async ({ params }) => { + return { + marker: `level-c-loader-${params.c}`, + data: makeLevelData(`level-c-data-${params.c}`, 3), + } + }, + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`csr-rendered-${params().c}`}

+

{data().marker}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.$b.tsx b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.$b.tsx new file mode 100644 index 00000000000..f735b1395fa --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.$b.tsx @@ -0,0 +1,26 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a/$b')({ + ssr: 'data-only', + loader: async ({ params }) => { + return { + marker: `level-b-loader-${params.b}`, + data: makeLevelData(`level-b-data-${params.b}`, 2), + } + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`data-only-rendered-${params().b}`}

+

{data().marker}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.tsx b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.tsx new file mode 100644 index 00000000000..c8c80a7a6d0 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/src/routes/mix.$a.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a')({ + ssr: true, + loader: async ({ params }) => { + return makeLevelData(`level-a-loader-${params.a}`, 1) + }, + component: LevelAComponent, +}) + +function LevelAComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`level-a-rendered-${params().a}`}

+

{data().items[0]?.name}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/tsconfig.json b/benchmarks/ssr/scenarios/selective-ssr/solid/tsconfig.json new file mode 100644 index 00000000000..f386c3f9961 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../loaders/shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/solid/vite.config.ts b/benchmarks/ssr/scenarios/selective-ssr/solid/vite.config.ts new file mode 100644 index 00000000000..09b42f00e65 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr selective-ssr (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/project.json b/benchmarks/ssr/scenarios/selective-ssr/vue/project.json new file mode 100644 index 00000000000..9ff4d993237 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-selective-ssr-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/speed.bench.ts b/benchmarks/ssr/scenarios/selective-ssr/vue/speed.bench.ts new file mode 100644 index 00000000000..f3082a102d4 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/speed.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { + assertSelectiveSanity, + benchOptions, + runSelectiveLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertSelectiveSanity(handler) + +describe('ssr', () => { + bench('ssr selective (vue)', () => runSelectiveLoop(handler), benchOptions) +}) diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..b77209b48b2 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routeTree.gen.ts @@ -0,0 +1,120 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 MixARouteImport } from './routes/mix.$a' +import { Route as MixABRouteImport } from './routes/mix.$a.$b' +import { Route as MixABCRouteImport } from './routes/mix.$a.$b.$c' + +const MixARoute = MixARouteImport.update({ + id: '/mix/$a', + path: '/mix/$a', + getParentRoute: () => rootRouteImport, +} as any) +const MixABRoute = MixABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => MixARoute, +} as any) +const MixABCRoute = MixABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => MixABRoute, +} as any) + +export interface FileRoutesByFullPath { + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRoutesByTo { + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/mix/$a': typeof MixARouteWithChildren + '/mix/$a/$b': typeof MixABRouteWithChildren + '/mix/$a/$b/$c': typeof MixABCRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + fileRoutesByTo: FileRoutesByTo + to: '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + id: '__root__' | '/mix/$a' | '/mix/$a/$b' | '/mix/$a/$b/$c' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + MixARoute: typeof MixARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/mix/$a': { + id: '/mix/$a' + path: '/mix/$a' + fullPath: '/mix/$a' + preLoaderRoute: typeof MixARouteImport + parentRoute: typeof rootRouteImport + } + '/mix/$a/$b': { + id: '/mix/$a/$b' + path: '/$b' + fullPath: '/mix/$a/$b' + preLoaderRoute: typeof MixABRouteImport + parentRoute: typeof MixARoute + } + '/mix/$a/$b/$c': { + id: '/mix/$a/$b/$c' + path: '/$c' + fullPath: '/mix/$a/$b/$c' + preLoaderRoute: typeof MixABCRouteImport + parentRoute: typeof MixABRoute + } + } +} + +interface MixABRouteChildren { + MixABCRoute: typeof MixABCRoute +} + +const MixABRouteChildren: MixABRouteChildren = { + MixABCRoute: MixABCRoute, +} + +const MixABRouteWithChildren = MixABRoute._addFileChildren(MixABRouteChildren) + +interface MixARouteChildren { + MixABRoute: typeof MixABRouteWithChildren +} + +const MixARouteChildren: MixARouteChildren = { + MixABRoute: MixABRouteWithChildren, +} + +const MixARouteWithChildren = MixARoute._addFileChildren(MixARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + MixARoute: MixARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/src/router.tsx b/benchmarks/ssr/scenarios/selective-ssr/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..4b035198230 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.$b.$c.tsx b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.$b.$c.tsx new file mode 100644 index 00000000000..46d7541c2ba --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.$b.$c.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a/$b/$c')({ + ssr: false, + loader: async ({ params }) => { + return { + marker: `level-c-loader-${params.c}`, + data: makeLevelData(`level-c-data-${params.c}`, 3), + } + }, + component: LevelCComponent, +}) + +function LevelCComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`csr-rendered-${params.value.c}`}

+

{data.value.marker}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.$b.tsx b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.$b.tsx new file mode 100644 index 00000000000..bc685478cc2 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.$b.tsx @@ -0,0 +1,26 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a/$b')({ + ssr: 'data-only', + loader: async ({ params }) => { + return { + marker: `level-b-loader-${params.b}`, + data: makeLevelData(`level-b-data-${params.b}`, 2), + } + }, + component: LevelBComponent, +}) + +function LevelBComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`data-only-rendered-${params.value.b}`}

+

{data.value.marker}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.tsx b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.tsx new file mode 100644 index 00000000000..8ab4c7d54dc --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/src/routes/mix.$a.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { makeLevelData } from '../../../../loaders/shared-data' + +export const Route = createFileRoute('/mix/$a')({ + ssr: true, + loader: async ({ params }) => { + return makeLevelData(`level-a-loader-${params.a}`, 1) + }, + component: LevelAComponent, +}) + +function LevelAComponent() { + const data = Route.useLoaderData() + const params = Route.useParams() + + return ( +
+

{`level-a-rendered-${params.value.a}`}

+

{data.value.items[0]?.name}

+ +
+ ) +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/tsconfig.json b/benchmarks/ssr/scenarios/selective-ssr/vue/tsconfig.json new file mode 100644 index 00000000000..3f2809bd5a5 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../loaders/shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/selective-ssr/vue/vite.config.ts b/benchmarks/ssr/scenarios/selective-ssr/vue/vite.config.ts new file mode 100644 index 00000000000..76299a67fd3 --- /dev/null +++ b/benchmarks/ssr/scenarios/selective-ssr/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr selective-ssr (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/serialization/react/project.json b/benchmarks/ssr/scenarios/serialization/react/project.json new file mode 100644 index 00000000000..652f87cb446 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-serialization-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/serialization/react/speed.bench.ts b/benchmarks/ssr/scenarios/serialization/react/speed.bench.ts new file mode 100644 index 00000000000..95c91f4529a --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/speed.bench.ts @@ -0,0 +1,32 @@ +import { bench, describe } from 'vitest' +import { + assertSerializationScenario, + runPlainSerializationLoop, + runRichSerializationLoop, + serializationBenchOptions, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertSerializationScenario(handler) + +describe('ssr', () => { + bench( + 'ssr dehydrate rich types (react)', + () => runRichSerializationLoop(handler), + serializationBenchOptions, + ) + + bench( + 'ssr dehydrate plain control (react)', + () => runPlainSerializationLoop(handler), + serializationBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/serialization/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/serialization/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..92bf0bb7327 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/routeTree.gen.ts @@ -0,0 +1,87 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 RichIdRouteImport } from './routes/rich.$id' +import { Route as PlainIdRouteImport } from './routes/plain.$id' + +const RichIdRoute = RichIdRouteImport.update({ + id: '/rich/$id', + path: '/rich/$id', + getParentRoute: () => rootRouteImport, +} as any) +const PlainIdRoute = PlainIdRouteImport.update({ + id: '/plain/$id', + path: '/plain/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRoutesByTo { + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/plain/$id' | '/rich/$id' + fileRoutesByTo: FileRoutesByTo + to: '/plain/$id' | '/rich/$id' + id: '__root__' | '/plain/$id' | '/rich/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + PlainIdRoute: typeof PlainIdRoute + RichIdRoute: typeof RichIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/rich/$id': { + id: '/rich/$id' + path: '/rich/$id' + fullPath: '/rich/$id' + preLoaderRoute: typeof RichIdRouteImport + parentRoute: typeof rootRouteImport + } + '/plain/$id': { + id: '/plain/$id' + path: '/plain/$id' + fullPath: '/plain/$id' + preLoaderRoute: typeof PlainIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + PlainIdRoute: PlainIdRoute, + RichIdRoute: RichIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.tsx' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/serialization/react/src/router.tsx b/benchmarks/ssr/scenarios/serialization/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/serialization/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/serialization/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/serialization/react/src/routes/plain.$id.tsx b/benchmarks/ssr/scenarios/serialization/react/src/routes/plain.$id.tsx new file mode 100644 index 00000000000..88a8a0e11fc --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/routes/plain.$id.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makePlainSerializationData } from '../../../shared-data' + +export const Route = createFileRoute('/plain/$id')({ + loader: ({ params }) => makePlainSerializationData(params.id), + component: PlainComponent, +}) + +function PlainComponent() { + const data = Route.useLoaderData() + const firstMapEntry = data.lookup[0]?.[1] + const firstTag = data.tags[0] + + return ( +
+

{data.label}

+

{data.createdAt}

+

{firstMapEntry?.label}

+

{firstTag}

+

{data.count}

+

{data.nested[0]?.id}

+

{data.points[0]?.label}

+

{data.problem.message}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/serialization/react/src/routes/rich.$id.tsx b/benchmarks/ssr/scenarios/serialization/react/src/routes/rich.$id.tsx new file mode 100644 index 00000000000..a9bb264c482 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/routes/rich.$id.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makeRichSerializationData } from '../../../shared-data' + +export const Route = createFileRoute('/rich/$id')({ + loader: ({ params }) => makeRichSerializationData(params.id), + component: RichComponent, +}) + +function RichComponent() { + const data = Route.useLoaderData() + const firstMapEntry = data.lookup.get('k0') + const firstTag = Array.from(data.tags)[0] + + return ( +
+

{data.label}

+

{data.createdAt.toISOString()}

+

{firstMapEntry?.label}

+

{firstTag}

+

{data.count.toString()}

+

{data.nested[0]?.id}

+

{data.points[0]?.label}

+

{data.problem.message}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/serialization/react/src/serialization.ts b/benchmarks/ssr/scenarios/serialization/react/src/serialization.ts new file mode 100644 index 00000000000..0a73c69be15 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/serialization.ts @@ -0,0 +1,10 @@ +import { createSerializationAdapter } from '@tanstack/react-router' +import { BenchPoint, benchPointAdapterKey } from '../../shared-data' + +export const benchPointAdapter = createSerializationAdapter({ + key: benchPointAdapterKey, + test: (value): value is BenchPoint => value instanceof BenchPoint, + toSerializable: (point) => ({ x: point.x, y: point.y }), + fromSerializable: (value: { x: number; y: number }) => + new BenchPoint(value.x, value.y), +}) diff --git a/benchmarks/ssr/scenarios/serialization/react/src/start.tsx b/benchmarks/ssr/scenarios/serialization/react/src/start.tsx new file mode 100644 index 00000000000..ea4513df4db --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/src/start.tsx @@ -0,0 +1,9 @@ +import { createStart } from '@tanstack/react-start' +import { benchPointAdapter } from './serialization' + +export const startInstance = createStart(() => { + return { + defaultSsr: true, + serializationAdapters: [benchPointAdapter], + } +}) diff --git a/benchmarks/ssr/scenarios/serialization/react/tsconfig.json b/benchmarks/ssr/scenarios/serialization/react/tsconfig.json new file mode 100644 index 00000000000..9bfd9bcf62b --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-bench.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/serialization/react/vite.config.ts b/benchmarks/ssr/scenarios/serialization/react/vite.config.ts new file mode 100644 index 00000000000..6280d875794 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr serialization (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/serialization/shared-bench.ts b/benchmarks/ssr/scenarios/serialization/shared-bench.ts new file mode 100644 index 00000000000..86e964444c3 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/shared-bench.ts @@ -0,0 +1,103 @@ +import { benchPointAdapterKey, richDateIso } from './shared-data' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const plainSerializationLoopIterations = 20 + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildSerializationRequest( + route: 'plain' | 'rich', + random: () => number, + index: number, +) { + const suffix = index.toString(36) + const id = `${randomSegment(random)}-${suffix}` + + return new Request(`http://localhost/${route}/${id}`, requestInit) +} + +async function fetchScenarioBody( + handler: StartRequestHandler, + route: 'plain' | 'rich', +) { + const response = await handler.fetch( + new Request(`http://localhost/${route}/sanity`, requestInit), + ) + const body = await response.text() + + if (response.status !== 200) { + throw new Error( + `Expected ${route} sanity request status 200, received ${response.status}: ${body}`, + ) + } + + if (!body.includes('$_TSR')) { + throw new Error(`Expected ${route} response to include dehydration marker`) + } + + return body +} + +function assertIncludes(body: string, marker: string, label: string) { + if (!body.includes(marker)) { + throw new Error(`Expected ${label} response to include ${marker}`) + } +} + +function assertExcludes(body: string, marker: string, label: string) { + if (body.includes(marker)) { + throw new Error(`Expected ${label} response not to include ${marker}`) + } +} + +export async function assertSerializationScenario( + handler: StartRequestHandler, +) { + const richBody = await fetchScenarioBody(handler, 'rich') + const plainBody = await fetchScenarioBody(handler, 'plain') + + assertIncludes(richBody, 'rich-sanity', 'rich') + assertIncludes(richBody, benchPointAdapterKey, 'rich') + assertIncludes(richBody, richDateIso, 'rich') + assertIncludes(richBody, 'new Date', 'rich') + assertIncludes(richBody, 'new Map', 'rich') + assertIncludes(richBody, 'new Error', 'rich') + + assertIncludes(plainBody, 'plain-sanity', 'plain') + assertExcludes(plainBody, benchPointAdapterKey, 'plain') + assertExcludes(plainBody, 'new Date', 'plain') + assertExcludes(plainBody, 'new Map', 'plain') + assertExcludes(plainBody, 'new Error', 'plain') +} + +export const serializationBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runRichSerializationLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: (random, index) => + buildSerializationRequest('rich', random, index), + }) +} + +export function runPlainSerializationLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: plainSerializationLoopIterations, + buildRequest: (random, index) => + buildSerializationRequest('plain', random, index), + }) +} diff --git a/benchmarks/ssr/scenarios/serialization/shared-data.ts b/benchmarks/ssr/scenarios/serialization/shared-data.ts new file mode 100644 index 00000000000..dc1dc90b1f5 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/shared-data.ts @@ -0,0 +1,101 @@ +export class BenchPoint { + constructor( + public x: number, + public y: number, + ) {} + + get label() { + return `${this.x}:${this.y}` + } +} + +export interface PlainSerializationPayload { + label: string + createdAt: string + lookup: Array<[string, { index: number; label: string }]> + tags: Array + count: string + nested: Array<{ id: string; values: Array; flag: boolean }> + points: Array<{ x: number; y: number; label: string }> + problem: { message: string } +} + +export interface RichSerializationPayload { + label: string + createdAt: Date + lookup: Map + tags: Set + count: bigint + nested: Array<{ id: string; values: Array; flag: boolean }> + points: Array + problem: Error +} + +export const benchPointAdapterKey = '$bench/point' +export const richDateIso = '2024-02-03T04:05:06.000Z' + +function makeNested(id: string) { + return Array.from({ length: 16 }, (_, index) => ({ + id: `${id}-nested-${index}`, + values: [index, index + 1, index + 2], + flag: index % 2 === 0, + })) +} + +function makeLookup( + id: string, +): Array<[string, { index: number; label: string }]> { + return Array.from( + { length: 8 }, + (_, index): [string, { index: number; label: string }] => { + const key = `k${index}` + + return [key, { index, label: `${id}-map-${index}` }] + }, + ) +} + +function makeTags(id: string) { + return Array.from({ length: 8 }, (_, index) => `${id}-tag-${index}`) +} + +function makePlainPoints() { + return Array.from({ length: 4 }, (_, index) => { + const point = new BenchPoint(index * 10, index * 10 + 5) + + return { x: point.x, y: point.y, label: point.label } + }) +} + +export function makeRichSerializationData( + id: string, +): RichSerializationPayload { + return { + label: `rich-${id}`, + createdAt: new Date(richDateIso), + lookup: new Map(makeLookup(id)), + tags: new Set(makeTags(id)), + count: 9_007_199_254_740_993n, + nested: makeNested(id), + points: Array.from( + { length: 4 }, + (_, index) => new BenchPoint(index * 10, index * 10 + 5), + ), + problem: new Error(`rich-problem-${id}`), + } +} + +export function makePlainSerializationData( + id: string, +): PlainSerializationPayload { + return { + label: `plain-${id}`, + createdAt: richDateIso, + lookup: makeLookup(id), + tags: makeTags(id), + count: '9007199254740993', + nested: makeNested(id), + points: makePlainPoints(), + problem: { message: `rich-problem-${id}` }, + } +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/project.json b/benchmarks/ssr/scenarios/serialization/solid/project.json new file mode 100644 index 00000000000..80a4b146ac9 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-serialization-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/speed.bench.ts b/benchmarks/ssr/scenarios/serialization/solid/speed.bench.ts new file mode 100644 index 00000000000..a939cae51ff --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/speed.bench.ts @@ -0,0 +1,32 @@ +import { bench, describe } from 'vitest' +import { + assertSerializationScenario, + runPlainSerializationLoop, + runRichSerializationLoop, + serializationBenchOptions, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertSerializationScenario(handler) + +describe('ssr', () => { + bench( + 'ssr dehydrate rich types (solid)', + () => runRichSerializationLoop(handler), + serializationBenchOptions, + ) + + bench( + 'ssr dehydrate plain control (solid)', + () => runPlainSerializationLoop(handler), + serializationBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/serialization/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..69b63cffe9c --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/routeTree.gen.ts @@ -0,0 +1,87 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 RichIdRouteImport } from './routes/rich.$id' +import { Route as PlainIdRouteImport } from './routes/plain.$id' + +const RichIdRoute = RichIdRouteImport.update({ + id: '/rich/$id', + path: '/rich/$id', + getParentRoute: () => rootRouteImport, +} as any) +const PlainIdRoute = PlainIdRouteImport.update({ + id: '/plain/$id', + path: '/plain/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRoutesByTo { + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/plain/$id' | '/rich/$id' + fileRoutesByTo: FileRoutesByTo + to: '/plain/$id' | '/rich/$id' + id: '__root__' | '/plain/$id' | '/rich/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + PlainIdRoute: typeof PlainIdRoute + RichIdRoute: typeof RichIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/rich/$id': { + id: '/rich/$id' + path: '/rich/$id' + fullPath: '/rich/$id' + preLoaderRoute: typeof RichIdRouteImport + parentRoute: typeof rootRouteImport + } + '/plain/$id': { + id: '/plain/$id' + path: '/plain/$id' + fullPath: '/plain/$id' + preLoaderRoute: typeof PlainIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + PlainIdRoute: PlainIdRoute, + RichIdRoute: RichIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.tsx' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/router.tsx b/benchmarks/ssr/scenarios/serialization/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/serialization/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/routes/plain.$id.tsx b/benchmarks/ssr/scenarios/serialization/solid/src/routes/plain.$id.tsx new file mode 100644 index 00000000000..c16f73dc3f4 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/routes/plain.$id.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makePlainSerializationData } from '../../../shared-data' + +export const Route = createFileRoute('/plain/$id')({ + loader: ({ params }) => makePlainSerializationData(params.id), + component: PlainComponent, +}) + +function PlainComponent() { + const data = Route.useLoaderData() + const firstMapEntry = () => data().lookup[0]?.[1] + const firstTag = () => data().tags[0] + + return ( +
+

{data().label}

+

{data().createdAt}

+

{firstMapEntry()?.label}

+

{firstTag()}

+

{data().count}

+

{data().nested[0]?.id}

+

{data().points[0]?.label}

+

{data().problem.message}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/routes/rich.$id.tsx b/benchmarks/ssr/scenarios/serialization/solid/src/routes/rich.$id.tsx new file mode 100644 index 00000000000..e08b1e4834f --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/routes/rich.$id.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makeRichSerializationData } from '../../../shared-data' + +export const Route = createFileRoute('/rich/$id')({ + loader: ({ params }) => makeRichSerializationData(params.id), + component: RichComponent, +}) + +function RichComponent() { + const data = Route.useLoaderData() + const firstMapEntry = () => data().lookup.get('k0') + const firstTag = () => Array.from(data().tags)[0] + + return ( +
+

{data().label}

+

{data().createdAt.toISOString()}

+

{firstMapEntry()?.label}

+

{firstTag()}

+

{data().count.toString()}

+

{data().nested[0]?.id}

+

{data().points[0]?.label}

+

{data().problem.message}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/serialization.ts b/benchmarks/ssr/scenarios/serialization/solid/src/serialization.ts new file mode 100644 index 00000000000..ae84f12c9f3 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/serialization.ts @@ -0,0 +1,10 @@ +import { createSerializationAdapter } from '@tanstack/solid-router' +import { BenchPoint, benchPointAdapterKey } from '../../shared-data' + +export const benchPointAdapter = createSerializationAdapter({ + key: benchPointAdapterKey, + test: (value): value is BenchPoint => value instanceof BenchPoint, + toSerializable: (point) => ({ x: point.x, y: point.y }), + fromSerializable: (value: { x: number; y: number }) => + new BenchPoint(value.x, value.y), +}) diff --git a/benchmarks/ssr/scenarios/serialization/solid/src/start.tsx b/benchmarks/ssr/scenarios/serialization/solid/src/start.tsx new file mode 100644 index 00000000000..a539f2b1680 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/src/start.tsx @@ -0,0 +1,9 @@ +import { createStart } from '@tanstack/solid-start' +import { benchPointAdapter } from './serialization' + +export const startInstance = createStart(() => { + return { + defaultSsr: true, + serializationAdapters: [benchPointAdapter], + } +}) diff --git a/benchmarks/ssr/scenarios/serialization/solid/tsconfig.json b/benchmarks/ssr/scenarios/serialization/solid/tsconfig.json new file mode 100644 index 00000000000..d7feff5f61c --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-bench.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/serialization/solid/vite.config.ts b/benchmarks/ssr/scenarios/serialization/solid/vite.config.ts new file mode 100644 index 00000000000..67df3299e10 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr serialization (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/serialization/vue/project.json b/benchmarks/ssr/scenarios/serialization/vue/project.json new file mode 100644 index 00000000000..728b5c15cc4 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-serialization-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/speed.bench.ts b/benchmarks/ssr/scenarios/serialization/vue/speed.bench.ts new file mode 100644 index 00000000000..ee1e072286d --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/speed.bench.ts @@ -0,0 +1,32 @@ +import { bench, describe } from 'vitest' +import { + assertSerializationScenario, + runPlainSerializationLoop, + runRichSerializationLoop, + serializationBenchOptions, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertSerializationScenario(handler) + +describe('ssr', () => { + bench( + 'ssr dehydrate rich types (vue)', + () => runRichSerializationLoop(handler), + serializationBenchOptions, + ) + + bench( + 'ssr dehydrate plain control (vue)', + () => runPlainSerializationLoop(handler), + serializationBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/serialization/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..02268c2c3c6 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/routeTree.gen.ts @@ -0,0 +1,87 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 RichIdRouteImport } from './routes/rich.$id' +import { Route as PlainIdRouteImport } from './routes/plain.$id' + +const RichIdRoute = RichIdRouteImport.update({ + id: '/rich/$id', + path: '/rich/$id', + getParentRoute: () => rootRouteImport, +} as any) +const PlainIdRoute = PlainIdRouteImport.update({ + id: '/plain/$id', + path: '/plain/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRoutesByTo { + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/plain/$id': typeof PlainIdRoute + '/rich/$id': typeof RichIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/plain/$id' | '/rich/$id' + fileRoutesByTo: FileRoutesByTo + to: '/plain/$id' | '/rich/$id' + id: '__root__' | '/plain/$id' | '/rich/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + PlainIdRoute: typeof PlainIdRoute + RichIdRoute: typeof RichIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/rich/$id': { + id: '/rich/$id' + path: '/rich/$id' + fullPath: '/rich/$id' + preLoaderRoute: typeof RichIdRouteImport + parentRoute: typeof rootRouteImport + } + '/plain/$id': { + id: '/plain/$id' + path: '/plain/$id' + fullPath: '/plain/$id' + preLoaderRoute: typeof PlainIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + PlainIdRoute: PlainIdRoute, + RichIdRoute: RichIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.tsx' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/router.tsx b/benchmarks/ssr/scenarios/serialization/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/serialization/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/routes/plain.$id.tsx b/benchmarks/ssr/scenarios/serialization/vue/src/routes/plain.$id.tsx new file mode 100644 index 00000000000..d41d123f1f1 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/routes/plain.$id.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makePlainSerializationData } from '../../../shared-data' + +export const Route = createFileRoute('/plain/$id')({ + loader: ({ params }) => makePlainSerializationData(params.id), + component: PlainComponent, +}) + +function PlainComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.label}

+

{data.value.createdAt}

+

{data.value.lookup[0]?.[1].label}

+

{data.value.tags[0]}

+

{data.value.count}

+

{data.value.nested[0]?.id}

+

{data.value.points[0]?.label}

+

{data.value.problem.message}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/routes/rich.$id.tsx b/benchmarks/ssr/scenarios/serialization/vue/src/routes/rich.$id.tsx new file mode 100644 index 00000000000..d50a3f292de --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/routes/rich.$id.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makeRichSerializationData } from '../../../shared-data' + +export const Route = createFileRoute('/rich/$id')({ + loader: ({ params }) => makeRichSerializationData(params.id), + component: RichComponent, +}) + +function RichComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.label}

+

{data.value.createdAt.toISOString()}

+

{data.value.lookup.get('k0')?.label}

+

{Array.from(data.value.tags)[0]}

+

{data.value.count.toString()}

+

{data.value.nested[0]?.id}

+

{data.value.points[0]?.label}

+

{data.value.problem.message}

+
+ ) +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/serialization.ts b/benchmarks/ssr/scenarios/serialization/vue/src/serialization.ts new file mode 100644 index 00000000000..f8b2d8cb43a --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/serialization.ts @@ -0,0 +1,10 @@ +import { createSerializationAdapter } from '@tanstack/vue-router' +import { BenchPoint, benchPointAdapterKey } from '../../shared-data' + +export const benchPointAdapter = createSerializationAdapter({ + key: benchPointAdapterKey, + test: (value): value is BenchPoint => value instanceof BenchPoint, + toSerializable: (point) => ({ x: point.x, y: point.y }), + fromSerializable: (value: { x: number; y: number }) => + new BenchPoint(value.x, value.y), +}) diff --git a/benchmarks/ssr/scenarios/serialization/vue/src/start.tsx b/benchmarks/ssr/scenarios/serialization/vue/src/start.tsx new file mode 100644 index 00000000000..ee27feb0841 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/src/start.tsx @@ -0,0 +1,9 @@ +import { createStart } from '@tanstack/vue-start' +import { benchPointAdapter } from './serialization' + +export const startInstance = createStart(() => { + return { + defaultSsr: true, + serializationAdapters: [benchPointAdapter], + } +}) diff --git a/benchmarks/ssr/scenarios/serialization/vue/tsconfig.json b/benchmarks/ssr/scenarios/serialization/vue/tsconfig.json new file mode 100644 index 00000000000..4a7f0ef0928 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-bench.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/serialization/vue/vite.config.ts b/benchmarks/ssr/scenarios/serialization/vue/vite.config.ts new file mode 100644 index 00000000000..76f99750c32 --- /dev/null +++ b/benchmarks/ssr/scenarios/serialization/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr serialization (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/bench.ts b/benchmarks/ssr/scenarios/server-fn-transport/bench.ts new file mode 100644 index 00000000000..f19f49631bf --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/bench.ts @@ -0,0 +1,560 @@ +import { toJSONAsync } from 'seroval' +import { + createDeterministicRandom, + randomSegment, + runRequestLoop, +} from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +type FnUrls = { + form: string + raw: string + stream: string +} + +type MultipartPayloadSpec = { + alpha: string + beta: string + gamma: string + fileName: string + fileContents: string +} + +type MultipartPayload = MultipartPayloadSpec & { + body: ArrayBuffer + contentType: string +} + +type QueryPayload = { + query: string + expectedBody: string +} + +type FrameStats = { + jsonFrames: number + chunkFrames: number + endFrames: number + errorFrames: number + chunkBytes: number +} + +export type ServerFnTransportBenchContext = { + urls: FnUrls + multipartPayloads: Array + rawPayloads: Array + streamPayloads: Array + rawExpectedBodiesByUrl: Map +} + +const benchmarkSeed = 0xdecafbad +const origin = 'http://localhost' +const tssContentTypeFramed = 'application/x-tss-framed' +const xTssSerialized = 'x-tss-serialized' +const xTssRawResponse = 'x-tss-raw' +const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json` +const frameHeaderSize = 9 +const frameTypeJson = 0 +const frameTypeChunk = 1 +const frameTypeEnd = 2 +const frameTypeError = 3 +const streamChunkCount = 8 +const streamChunkSize = 1024 +const multipartFileSize = 1024 +const commonHeaders = { + 'x-tsr-serverFn': 'true', + 'sec-fetch-site': 'same-origin', + accept: acceptHeader, +} satisfies HeadersInit +const serverFnTransportRequestLoopOptions = { + seed: benchmarkSeed, + iterations: 100, +} as const + +export const serverFnTransportBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} as const + +function makeFixedSizeString(seed: string, size: number) { + let value = '' + + while (value.length < size) { + value += `${seed}:${value.length.toString(36)};` + } + + return value.slice(0, size) +} + +function createMultipartPayloadSpecs() { + const random = createDeterministicRandom(benchmarkSeed) + + return Array.from({ length: 10 }, (_, index): MultipartPayloadSpec => { + const alpha = `alpha-${index}-${randomSegment(random)}` + const beta = `beta-${index}-${randomSegment(random)}` + const gamma = `gamma-${index}-${randomSegment(random)}` + + return { + alpha, + beta, + gamma, + fileName: `bench-${index}.txt`, + fileContents: makeFixedSizeString( + `${alpha}:${beta}:${gamma}`, + multipartFileSize, + ), + } + }) +} + +async function createMultipartPayload( + spec: MultipartPayloadSpec, +): Promise { + const formData = new FormData() + formData.set('alpha', spec.alpha) + formData.set('beta', spec.beta) + formData.set('gamma', spec.gamma) + formData.set( + 'upload', + new File([spec.fileContents], spec.fileName, { type: 'text/plain' }), + ) + + const request = new Request(`${origin}/__multipart-encode`, { + method: 'POST', + body: formData, + }) + const contentType = request.headers.get('content-type') + + if (!contentType?.includes('multipart/form-data')) { + throw new Error(`Expected multipart content type, received ${contentType}`) + } + + return { + ...spec, + body: await request.arrayBuffer(), + contentType, + } +} + +async function createMultipartPayloads() { + return await Promise.all( + createMultipartPayloadSpecs().map((spec) => createMultipartPayload(spec)), + ) +} + +function createTransportInputs(prefix: string) { + const random = createDeterministicRandom(benchmarkSeed ^ prefix.length) + + return Array.from( + { length: 10 }, + (_, index) => + `${prefix}-${index}-${randomSegment(random)}-${randomSegment(random)}`, + ) +} + +async function createQueryPayloads( + inputs: Array, + expectedBody: (input: string) => string, +) { + return await Promise.all( + inputs.map(async (input): Promise => { + const body = JSON.stringify(await toJSONAsync({ data: input })) + const query = `?${new URLSearchParams({ payload: body })}` + + return { + query, + expectedBody: expectedBody(input), + } + }), + ) +} + +async function discoverUrls(handler: StartRequestHandler) { + const response = await handler.fetch(new Request(`${origin}/api/fn-urls`)) + + if (response.status === 404) { + throw new Error('URL discovery route returned 404 for /api/fn-urls') + } + + if (response.status !== 200) { + throw new Error( + `URL discovery failed with status ${response.status}: ${await response.text()}`, + ) + } + + const urls = (await response.json()) as Partial + + if ( + typeof urls.form !== 'string' || + typeof urls.raw !== 'string' || + typeof urls.stream !== 'string' + ) { + throw new Error( + `URL discovery returned invalid payload: ${JSON.stringify(urls)}`, + ) + } + + return { form: urls.form, raw: urls.raw, stream: urls.stream } +} + +function buildMultipartRequest( + urls: FnUrls, + payloads: Array, + index: number, +) { + const payload = payloads[index % payloads.length]! + + return new Request(`${origin}${urls.form}`, { + method: 'POST', + headers: { + ...commonHeaders, + 'content-type': payload.contentType, + }, + body: payload.body, + }) +} + +function buildRawResponseRequest( + urls: FnUrls, + payloads: Array, + index: number, +) { + const payload = payloads[index % payloads.length]! + + return new Request(`${origin}${urls.raw}${payload.query}`, { + method: 'GET', + headers: commonHeaders, + }) +} + +function buildRawStreamRequest( + urls: FnUrls, + payloads: Array, + index: number, +) { + const payload = payloads[index % payloads.length]! + + return new Request(`${origin}${urls.stream}${payload.query}`, { + method: 'GET', + headers: commonHeaders, + }) +} + +async function assertSerializedResponse({ + response, + label, +}: { + response: Response + label: string +}) { + const text = await response.text() + + if (response.status === 403) { + throw new Error( + `${label} sanity check failed with 403. Check CSRF headers.`, + ) + } + + if (response.status === 404) { + throw new Error( + `${label} sanity check failed with 404. The discovered server function URL is stale.`, + ) + } + + if (response.status !== 200) { + throw new Error( + `${label} sanity check failed with status ${response.status}: ${text}`, + ) + } + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`${label} sanity check missing ${xTssSerialized} header`) + } + + return text +} + +function assertMultipartBody(text: string, payload: MultipartPayload) { + const expectedMarkers = [ + payload.alpha, + payload.beta, + payload.gamma, + payload.fileName, + 'fileSize', + multipartFileSize.toString(), + ] + + for (const marker of expectedMarkers) { + if (!text.includes(marker)) { + throw new Error( + `Multipart sanity check missing marker ${marker}: ${text}`, + ) + } + } +} + +async function assertRawResponse(response: Response, expectedBody: string) { + const text = await response.text() + + if (response.status !== 200) { + throw new Error( + `raw-response sanity check failed with status ${response.status}: ${text}`, + ) + } + + if (response.headers.get(xTssRawResponse) !== 'true') { + throw new Error( + `raw-response sanity check missing ${xTssRawResponse} header`, + ) + } + + if (text !== expectedBody) { + throw new Error( + `raw-response sanity check expected ${expectedBody}, received ${text}`, + ) + } +} + +function readUint32(bytes: Uint8Array, offset: number) { + return ( + (((bytes[offset] ?? 0) << 24) | + ((bytes[offset + 1] ?? 0) << 16) | + ((bytes[offset + 2] ?? 0) << 8) | + (bytes[offset + 3] ?? 0)) >>> + 0 + ) +} + +function decodeFrameStats(buffer: ArrayBuffer): FrameStats { + const bytes = new Uint8Array(buffer) + let offset = 0 + const stats: FrameStats = { + jsonFrames: 0, + chunkFrames: 0, + endFrames: 0, + errorFrames: 0, + chunkBytes: 0, + } + + while (offset < bytes.length) { + if (offset + frameHeaderSize > bytes.length) { + throw new Error(`Incomplete frame header at byte ${offset}`) + } + + const type = bytes[offset] + const length = readUint32(bytes, offset + 5) + const payloadStart = offset + frameHeaderSize + const payloadEnd = payloadStart + length + + if (payloadEnd > bytes.length) { + throw new Error(`Incomplete frame payload at byte ${offset}`) + } + + if (type === frameTypeJson) { + stats.jsonFrames++ + } else if (type === frameTypeChunk) { + stats.chunkFrames++ + stats.chunkBytes += length + } else if (type === frameTypeEnd) { + stats.endFrames++ + } else if (type === frameTypeError) { + stats.errorFrames++ + } else { + throw new Error(`Unknown frame type ${type} at byte ${offset}`) + } + + offset = payloadEnd + } + + return stats +} + +async function assertRawStream(response: Response) { + if (response.status !== 200) { + throw new Error( + `raw-stream sanity check failed with status ${response.status}: ${await response.text()}`, + ) + } + + const contentType = response.headers.get('content-type') + if (!contentType?.includes(tssContentTypeFramed)) { + throw new Error( + `raw-stream sanity check expected framed content type, received ${contentType}`, + ) + } + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`raw-stream sanity check missing ${xTssSerialized} header`) + } + + const stats = decodeFrameStats(await response.arrayBuffer()) + const expectedChunkBytes = streamChunkCount * streamChunkSize + + if (stats.jsonFrames < 1) { + throw new Error('raw-stream sanity check expected at least one JSON frame') + } + + if (stats.chunkFrames !== streamChunkCount) { + throw new Error( + `raw-stream sanity check expected ${streamChunkCount} chunk frames, received ${stats.chunkFrames}`, + ) + } + + if (stats.endFrames !== 1) { + throw new Error( + `raw-stream sanity check expected one END frame, received ${stats.endFrames}`, + ) + } + + if (stats.errorFrames !== 0) { + throw new Error( + `raw-stream sanity check received ${stats.errorFrames} ERROR frames`, + ) + } + + if (stats.chunkBytes !== expectedChunkBytes) { + throw new Error( + `raw-stream sanity check expected ${expectedChunkBytes} chunk bytes, received ${stats.chunkBytes}`, + ) + } +} + +function validateStatus(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Request failed with non-200 status ${response.status} (${request.url})`, + ) + } +} + +function validateSerializedResponse(response: Response, request: Request) { + validateStatus(response, request) + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`Request missing ${xTssSerialized} header (${request.url})`) + } +} + +function validateRawResponse(response: Response, request: Request) { + validateStatus(response, request) + + if (response.headers.get(xTssRawResponse) !== 'true') { + throw new Error( + `Request missing ${xTssRawResponse} header (${request.url})`, + ) + } +} + +function validateRawStreamResponse(response: Response, request: Request) { + validateSerializedResponse(response, request) + + const contentType = response.headers.get('content-type') + if (!contentType?.includes(tssContentTypeFramed)) { + throw new Error( + `Request missing framed content type (${request.url}): ${contentType}`, + ) + } +} + +export async function setupServerFnTransportBench( + handler: StartRequestHandler, +) { + const urls = await discoverUrls(handler) + const multipartPayloads = await createMultipartPayloads() + const rawPayloads = await createQueryPayloads( + createTransportInputs('raw'), + (input) => `raw-${input}`, + ) + const streamPayloads = await createQueryPayloads( + createTransportInputs('stream'), + (input) => `stream-${input}`, + ) + const rawExpectedBodiesByUrl = new Map( + rawPayloads.map((payload) => [ + `${origin}${urls.raw}${payload.query}`, + payload.expectedBody, + ]), + ) + + return { + urls, + multipartPayloads, + rawPayloads, + streamPayloads, + rawExpectedBodiesByUrl, + } +} + +export async function assertServerFnTransportScenario( + handler: StartRequestHandler, + context: ServerFnTransportBenchContext, +) { + const multipartPayload = context.multipartPayloads[0]! + const multipartText = await assertSerializedResponse({ + response: await handler.fetch( + buildMultipartRequest(context.urls, context.multipartPayloads, 0), + ), + label: 'multipart', + }) + assertMultipartBody(multipartText, multipartPayload) + + await assertRawResponse( + await handler.fetch( + buildRawResponseRequest(context.urls, context.rawPayloads, 0), + ), + context.rawPayloads[0]!.expectedBody, + ) + + await assertRawStream( + await handler.fetch( + buildRawStreamRequest(context.urls, context.streamPayloads, 0), + ), + ) +} + +export function runServerFnMultipartRequestLoop( + handler: StartRequestHandler, + context: ServerFnTransportBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnTransportRequestLoopOptions, + buildRequest: (_random, index) => + buildMultipartRequest(context.urls, context.multipartPayloads, index), + validateResponse: validateSerializedResponse, + }) +} + +export function runServerFnRawResponseRequestLoop( + handler: StartRequestHandler, + context: ServerFnTransportBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnTransportRequestLoopOptions, + buildRequest: (_random, index) => + buildRawResponseRequest(context.urls, context.rawPayloads, index), + validateResponse: validateRawResponse, + validateBody: (body, _response, request) => { + const expectedBody = context.rawExpectedBodiesByUrl.get(request.url) + + if (body !== expectedBody) { + throw new Error( + `Expected raw response body ${expectedBody}, received ${body}`, + ) + } + }, + }) +} + +export function runServerFnRawStreamRequestLoop( + handler: StartRequestHandler, + context: ServerFnTransportBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnTransportRequestLoopOptions, + buildRequest: (_random, index) => + buildRawStreamRequest(context.urls, context.streamPayloads, index), + validateResponse: validateRawStreamResponse, + }) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/project.json b/benchmarks/ssr/scenarios/server-fn-transport/react/project.json new file mode 100644 index 00000000000..a3012549ca8 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-fn-transport-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/speed.bench.ts b/benchmarks/ssr/scenarios/server-fn-transport/react/speed.bench.ts new file mode 100644 index 00000000000..c168f6e61cb --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/speed.bench.ts @@ -0,0 +1,40 @@ +import { bench, describe } from 'vitest' +import { + assertServerFnTransportScenario, + runServerFnMultipartRequestLoop, + runServerFnRawResponseRequestLoop, + runServerFnRawStreamRequestLoop, + serverFnTransportBenchOptions, + setupServerFnTransportBench, +} from '../bench' +import type { StartRequestHandler } from '../bench' + +const { default: handler } = (await import( + /* @vite-ignore */ new URL('./dist/server/server.js', import.meta.url).href +)) as { + default: StartRequestHandler +} + +const context = await setupServerFnTransportBench(handler) + +await assertServerFnTransportScenario(handler, context) + +describe('ssr', () => { + bench( + 'ssr server-fn multipart (react)', + () => runServerFnMultipartRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) + + bench( + 'ssr server-fn raw-response (react)', + () => runServerFnRawResponseRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) + + bench( + 'ssr server-fn raw-stream (react)', + () => runServerFnRawStreamRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/src/fns.ts b/benchmarks/ssr/scenarios/server-fn-transport/react/src/fns.ts new file mode 100644 index 00000000000..79ecb50e873 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/src/fns.ts @@ -0,0 +1,101 @@ +import { RawStream, createServerFn } from '@tanstack/react-start' + +type FormEchoData = { + alpha: string + beta: string + gamma: string + upload: File +} + +const streamChunkCount = 8 +const streamChunkSize = 1024 + +function getStringField(formData: FormData, field: string) { + const value = formData.get(field) + + if (typeof value !== 'string') { + throw new Error(`Expected ${field} to be a string`) + } + + return value +} + +function validateFormData(input: unknown): FormEchoData { + if (!(input instanceof FormData)) { + throw new Error('Expected FormData input') + } + + const upload = input.get('upload') + if (!(upload instanceof File)) { + throw new Error('Expected upload to be a File') + } + + return { + alpha: getStringField(input, 'alpha'), + beta: getStringField(input, 'beta'), + gamma: getStringField(input, 'gamma'), + upload, + } +} + +function validateString(input: unknown) { + if (typeof input !== 'string' || input.length === 0) { + throw new Error('Expected non-empty string input') + } + + return input +} + +function createDeterministicStream(seed: string) { + return new ReadableStream({ + start(controller) { + for (let chunkIndex = 0; chunkIndex < streamChunkCount; chunkIndex++) { + const chunk = new Uint8Array(streamChunkSize) + + for (let byteIndex = 0; byteIndex < chunk.length; byteIndex++) { + chunk[byteIndex] = + (seed.charCodeAt(byteIndex % seed.length) + + byteIndex + + chunkIndex) & + 0xff + } + + controller.enqueue(chunk) + } + + controller.close() + }, + }) +} + +export const formEcho = createServerFn({ method: 'POST' }) + .validator(validateFormData) + .handler(async ({ data }) => { + const contents = await data.upload.text() + + return { + alpha: data.alpha, + beta: data.beta, + gamma: data.gamma, + fileName: data.upload.name, + fileSize: data.upload.size, + filePreview: contents.slice(0, 32), + } + }) + +export const rawResp = createServerFn({ method: 'GET' }) + .validator(validateString) + .handler(({ data }) => { + return new Response(`raw-${data}`, { + headers: { 'content-type': 'text/plain' }, + }) + }) + +export const streamOut = createServerFn({ method: 'GET' }) + .validator(validateString) + .handler(({ data }) => { + return { + label: `stream-${data}`, + data: new RawStream(createDeterministicStream(data)), + } + }) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..10e933da3dc --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/src/router.tsx b/benchmarks/ssr/scenarios/server-fn-transport/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..02260e50987 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/api.fn-urls.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { formEcho, rawResp, streamOut } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + form: formEcho.url, + raw: rawResp.url, + stream: streamOut.url, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/index.tsx new file mode 100644 index 00000000000..baa00263785 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { formEcho, rawResp, streamOut } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ server-fn transport benchmark +
+ ) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/tsconfig.json b/benchmarks/ssr/scenarios/server-fn-transport/react/tsconfig.json new file mode 100644 index 00000000000..08c349fd490 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/react/vite.config.ts b/benchmarks/ssr/scenarios/server-fn-transport/react/vite.config.ts new file mode 100644 index 00000000000..4c6293d5d74 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-fn-transport (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/project.json b/benchmarks/ssr/scenarios/server-fn-transport/solid/project.json new file mode 100644 index 00000000000..16fc151b11b --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-fn-transport-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/speed.bench.ts b/benchmarks/ssr/scenarios/server-fn-transport/solid/speed.bench.ts new file mode 100644 index 00000000000..b93d7d79a9d --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/speed.bench.ts @@ -0,0 +1,40 @@ +import { bench, describe } from 'vitest' +import { + assertServerFnTransportScenario, + runServerFnMultipartRequestLoop, + runServerFnRawResponseRequestLoop, + runServerFnRawStreamRequestLoop, + serverFnTransportBenchOptions, + setupServerFnTransportBench, +} from '../bench' +import type { StartRequestHandler } from '../bench' + +const { default: handler } = (await import( + /* @vite-ignore */ new URL('./dist/server/server.js', import.meta.url).href +)) as { + default: StartRequestHandler +} + +const context = await setupServerFnTransportBench(handler) + +await assertServerFnTransportScenario(handler, context) + +describe('ssr', () => { + bench( + 'ssr server-fn multipart (solid)', + () => runServerFnMultipartRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) + + bench( + 'ssr server-fn raw-response (solid)', + () => runServerFnRawResponseRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) + + bench( + 'ssr server-fn raw-stream (solid)', + () => runServerFnRawStreamRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/src/fns.ts b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/fns.ts new file mode 100644 index 00000000000..3c586fa2eef --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/fns.ts @@ -0,0 +1,101 @@ +import { RawStream, createServerFn } from '@tanstack/solid-start' + +type FormEchoData = { + alpha: string + beta: string + gamma: string + upload: File +} + +const streamChunkCount = 8 +const streamChunkSize = 1024 + +function getStringField(formData: FormData, field: string) { + const value = formData.get(field) + + if (typeof value !== 'string') { + throw new Error(`Expected ${field} to be a string`) + } + + return value +} + +function validateFormData(input: unknown): FormEchoData { + if (!(input instanceof FormData)) { + throw new Error('Expected FormData input') + } + + const upload = input.get('upload') + if (!(upload instanceof File)) { + throw new Error('Expected upload to be a File') + } + + return { + alpha: getStringField(input, 'alpha'), + beta: getStringField(input, 'beta'), + gamma: getStringField(input, 'gamma'), + upload, + } +} + +function validateString(input: unknown) { + if (typeof input !== 'string' || input.length === 0) { + throw new Error('Expected non-empty string input') + } + + return input +} + +function createDeterministicStream(seed: string) { + return new ReadableStream({ + start(controller) { + for (let chunkIndex = 0; chunkIndex < streamChunkCount; chunkIndex++) { + const chunk = new Uint8Array(streamChunkSize) + + for (let byteIndex = 0; byteIndex < chunk.length; byteIndex++) { + chunk[byteIndex] = + (seed.charCodeAt(byteIndex % seed.length) + + byteIndex + + chunkIndex) & + 0xff + } + + controller.enqueue(chunk) + } + + controller.close() + }, + }) +} + +export const formEcho = createServerFn({ method: 'POST' }) + .validator(validateFormData) + .handler(async ({ data }) => { + const contents = await data.upload.text() + + return { + alpha: data.alpha, + beta: data.beta, + gamma: data.gamma, + fileName: data.upload.name, + fileSize: data.upload.size, + filePreview: contents.slice(0, 32), + } + }) + +export const rawResp = createServerFn({ method: 'GET' }) + .validator(validateString) + .handler(({ data }) => { + return new Response(`raw-${data}`, { + headers: { 'content-type': 'text/plain' }, + }) + }) + +export const streamOut = createServerFn({ method: 'GET' }) + .validator(validateString) + .handler(({ data }) => { + return { + label: `stream-${data}`, + data: new RawStream(createDeterministicStream(data)), + } + }) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..298ac3168d2 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/src/router.tsx b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..353a732637f --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/api.fn-urls.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { formEcho, rawResp, streamOut } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + form: formEcho.url, + raw: rawResp.url, + stream: streamOut.url, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/index.tsx new file mode 100644 index 00000000000..6e0e5d8d6b3 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { formEcho, rawResp, streamOut } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ server-fn transport benchmark +
+ ) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/tsconfig.json b/benchmarks/ssr/scenarios/server-fn-transport/solid/tsconfig.json new file mode 100644 index 00000000000..54a0e49d93e --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/solid/vite.config.ts b/benchmarks/ssr/scenarios/server-fn-transport/solid/vite.config.ts new file mode 100644 index 00000000000..b71d03009ba --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-fn-transport (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/project.json b/benchmarks/ssr/scenarios/server-fn-transport/vue/project.json new file mode 100644 index 00000000000..abdd85c6b76 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-fn-transport-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/speed.bench.ts b/benchmarks/ssr/scenarios/server-fn-transport/vue/speed.bench.ts new file mode 100644 index 00000000000..6f803d16654 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/speed.bench.ts @@ -0,0 +1,40 @@ +import { bench, describe } from 'vitest' +import { + assertServerFnTransportScenario, + runServerFnMultipartRequestLoop, + runServerFnRawResponseRequestLoop, + runServerFnRawStreamRequestLoop, + serverFnTransportBenchOptions, + setupServerFnTransportBench, +} from '../bench' +import type { StartRequestHandler } from '../bench' + +const { default: handler } = (await import( + /* @vite-ignore */ new URL('./dist/server/server.js', import.meta.url).href +)) as { + default: StartRequestHandler +} + +const context = await setupServerFnTransportBench(handler) + +await assertServerFnTransportScenario(handler, context) + +describe('ssr', () => { + bench( + 'ssr server-fn multipart (vue)', + () => runServerFnMultipartRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) + + bench( + 'ssr server-fn raw-response (vue)', + () => runServerFnRawResponseRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) + + bench( + 'ssr server-fn raw-stream (vue)', + () => runServerFnRawStreamRequestLoop(handler, context), + serverFnTransportBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/src/fns.ts b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/fns.ts new file mode 100644 index 00000000000..f7e1a1217aa --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/fns.ts @@ -0,0 +1,101 @@ +import { RawStream, createServerFn } from '@tanstack/vue-start' + +type FormEchoData = { + alpha: string + beta: string + gamma: string + upload: File +} + +const streamChunkCount = 8 +const streamChunkSize = 1024 + +function getStringField(formData: FormData, field: string) { + const value = formData.get(field) + + if (typeof value !== 'string') { + throw new Error(`Expected ${field} to be a string`) + } + + return value +} + +function validateFormData(input: unknown): FormEchoData { + if (!(input instanceof FormData)) { + throw new Error('Expected FormData input') + } + + const upload = input.get('upload') + if (!(upload instanceof File)) { + throw new Error('Expected upload to be a File') + } + + return { + alpha: getStringField(input, 'alpha'), + beta: getStringField(input, 'beta'), + gamma: getStringField(input, 'gamma'), + upload, + } +} + +function validateString(input: unknown) { + if (typeof input !== 'string' || input.length === 0) { + throw new Error('Expected non-empty string input') + } + + return input +} + +function createDeterministicStream(seed: string) { + return new ReadableStream({ + start(controller) { + for (let chunkIndex = 0; chunkIndex < streamChunkCount; chunkIndex++) { + const chunk = new Uint8Array(streamChunkSize) + + for (let byteIndex = 0; byteIndex < chunk.length; byteIndex++) { + chunk[byteIndex] = + (seed.charCodeAt(byteIndex % seed.length) + + byteIndex + + chunkIndex) & + 0xff + } + + controller.enqueue(chunk) + } + + controller.close() + }, + }) +} + +export const formEcho = createServerFn({ method: 'POST' }) + .validator(validateFormData) + .handler(async ({ data }) => { + const contents = await data.upload.text() + + return { + alpha: data.alpha, + beta: data.beta, + gamma: data.gamma, + fileName: data.upload.name, + fileSize: data.upload.size, + filePreview: contents.slice(0, 32), + } + }) + +export const rawResp = createServerFn({ method: 'GET' }) + .validator(validateString) + .handler(({ data }) => { + return new Response(`raw-${data}`, { + headers: { 'content-type': 'text/plain' }, + }) + }) + +export const streamOut = createServerFn({ method: 'GET' }) + .validator(validateString) + .handler(({ data }) => { + return { + label: `stream-${data}`, + data: new RawStream(createDeterministicStream(data)), + } + }) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..b226f0a25ae --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/src/router.tsx b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..2db00972bbf --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/api.fn-urls.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { formEcho, rawResp, streamOut } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + form: formEcho.url, + raw: rawResp.url, + stream: streamOut.url, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/index.tsx new file mode 100644 index 00000000000..d53731eb23d --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { formEcho, rawResp, streamOut } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ server-fn transport benchmark +
+ ) +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/tsconfig.json b/benchmarks/ssr/scenarios/server-fn-transport/vue/tsconfig.json new file mode 100644 index 00000000000..29efa76d67c --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../bench.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-fn-transport/vue/vite.config.ts b/benchmarks/ssr/scenarios/server-fn-transport/vue/vite.config.ts new file mode 100644 index 00000000000..5d74904e147 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fn-transport/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-fn-transport (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fns/bench.ts b/benchmarks/ssr/scenarios/server-fns/bench.ts new file mode 100644 index 00000000000..1a51212df9d --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/bench.ts @@ -0,0 +1,523 @@ +import { fromCrossJSON, toJSONAsync } from 'seroval' +import { + createDeterministicRandom, + randomSegment, + runRequestLoop, +} from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' +import type { SerovalNode } from 'seroval' + +export type { StartRequestHandler } + +type Payload = { + q: string + n: number + nested: { list: Array } +} + +type FnUrls = { + get: string + post: string + redirect: string + notFound: string + context: string +} + +export type ServerFnBenchContext = { + urls: FnUrls + bodies: Array + getQueries: Array + expectedMarker: string + contextBodies: Array + contextExpectedMarkers: Array + contextExpectedStamps: Array +} + +const benchmarkSeed = 0xdecafbad +const origin = 'http://localhost' +const tssContentTypeFramed = 'application/x-tss-framed' +const xTssSerialized = 'x-tss-serialized' +const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json` +const commonHeaders = { + 'x-tsr-serverFn': 'true', + 'sec-fetch-site': 'same-origin', + accept: acceptHeader, +} satisfies HeadersInit +const postHeaders = { + ...commonHeaders, + 'content-type': 'application/json', +} satisfies HeadersInit +const documentRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit +const serverFnRequestLoopOptions = { + seed: benchmarkSeed, + iterations: 100, +} as const +const serverFnSsrRequestLoopOptions = { + seed: benchmarkSeed, + iterations: 20, +} as const + +export const serverFnBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} as const + +function createPayloads() { + const random = createDeterministicRandom(benchmarkSeed) + + return Array.from({ length: 10 }, (_, index): Payload => { + const queryParts = Array.from({ length: 6 }, () => randomSegment(random)) + + return { + q: `q-${index}-${queryParts.join('-')}`, + n: 100 + index, + nested: { + list: Array.from( + { length: 5 }, + () => `l-${index}-${randomSegment(random)}-${randomSegment(random)}`, + ), + }, + } + }) +} + +async function createBodies(payloads: Array) { + return await Promise.all( + payloads.map(async (payload) => + JSON.stringify(await toJSONAsync({ data: payload })), + ), + ) +} + +function createContextToken(payload: Payload) { + return `token-${payload.q}-${payload.nested.list[0]}` +} + +async function createContextFixtures(payloads: Array) { + return await Promise.all( + payloads.map(async (payload) => { + const token = createContextToken(payload) + + return { + body: JSON.stringify( + await toJSONAsync({ data: payload, context: { token } }), + ), + marker: `ctx-${token}-${payload.nested.list[0]}`, + stamp: `stamp-${token}`, + } + }), + ) +} + +function createGetQueries(bodies: Array) { + return bodies.map((body) => `?${new URLSearchParams({ payload: body })}`) +} + +async function discoverUrls(handler: StartRequestHandler) { + const response = await handler.fetch(new Request(`${origin}/api/fn-urls`)) + + if (response.status === 404) { + throw new Error('URL discovery route returned 404 for /api/fn-urls') + } + + if (response.status !== 200) { + throw new Error( + `URL discovery failed with status ${response.status}: ${await response.text()}`, + ) + } + + const urls = (await response.json()) as Partial + const requiredKeys = [ + 'get', + 'post', + 'redirect', + 'notFound', + 'context', + ] as const + + for (const key of requiredKeys) { + if (typeof urls[key] !== 'string') { + throw new Error( + `URL discovery returned invalid payload: ${JSON.stringify(urls)}`, + ) + } + } + + return urls as FnUrls +} + +function validateSerializedResponse(response: Response, label: string) { + if (response.status === 403) { + throw new Error( + `${label} sanity check failed with 403. Check CSRF headers.`, + ) + } + + if (response.status === 404) { + throw new Error( + `${label} sanity check failed with 404. The discovered server function URL is stale.`, + ) + } + + if (response.status !== 200) { + throw new Error(`${label} request failed with status ${response.status}`) + } + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`${label} response missing ${xTssSerialized} header`) + } +} + +function validateRedirectResponse(response: Response) { + if (response.status !== 200) { + throw new Error(`redirect request failed with status ${response.status}`) + } + + const contentType = response.headers.get('content-type') + + if (!contentType?.includes('application/json')) { + throw new Error(`redirect response was not JSON: ${contentType}`) + } +} + +function validateDocumentResponse(response: Response) { + if (response.status !== 200) { + throw new Error(`document request failed with status ${response.status}`) + } + + const contentType = response.headers.get('content-type') + + if (!contentType?.includes('text/html')) { + throw new Error(`document response was not HTML: ${contentType}`) + } +} + +async function readSerializedServerFnResult(response: Response, label: string) { + const text = await response.text() + + validateSerializedResponse(response, label) + + let json: unknown + + try { + json = JSON.parse(text) + } catch (error) { + throw new Error(`${label} sanity check returned invalid JSON: ${text}`, { + cause: error, + }) + } + + return { text, decoded: fromCrossJSON(json as SerovalNode, {}) } +} + +async function assertServerFnResponse({ + response, + label, + expectedMarker, +}: { + response: Response + label: string + expectedMarker: string +}) { + const { text } = await readSerializedServerFnResult(response, label) + + if (!text.includes(expectedMarker)) { + throw new Error( + `${label} sanity check did not include expected marker ${expectedMarker}: ${text}`, + ) + } +} + +async function assertServerFnRedirectResponse(response: Response) { + const text = await response.text() + + if (response.status !== 200) { + throw new Error( + `redirect sanity check failed with status ${response.status}: ${text}`, + ) + } + + let payload: { + href?: string + isSerializedRedirect?: boolean + statusCode?: number + } + + try { + payload = JSON.parse(text) as typeof payload + } catch (error) { + throw new Error(`redirect sanity check returned invalid JSON: ${text}`, { + cause: error, + }) + } + + if ( + payload.href !== '/' || + payload.statusCode !== 307 || + payload.isSerializedRedirect !== true + ) { + throw new Error( + `redirect sanity check expected serialized redirect to /, got ${text}`, + ) + } +} + +async function assertServerFnNotFoundResponse(response: Response) { + const { decoded } = await readSerializedServerFnResult(response, 'not-found') + const payload = decoded as { error?: { isNotFound?: boolean } } + + if (payload.error?.isNotFound !== true) { + throw new Error( + `not-found sanity check did not include isNotFound error: ${JSON.stringify(decoded)}`, + ) + } +} + +async function assertServerFnContextResponse({ + response, + expectedMarker, + expectedStamp, +}: { + response: Response + expectedMarker: string + expectedStamp: string +}) { + const { decoded } = await readSerializedServerFnResult( + response, + 'send-context', + ) + const payload = decoded as { + result?: { marker?: string; token?: string } + context?: { stamp?: string } + } + + if (payload.result?.marker !== expectedMarker) { + throw new Error( + `send-context sanity check expected result marker ${expectedMarker}, got ${JSON.stringify(decoded)}`, + ) + } + + if (payload.context?.stamp !== expectedStamp) { + throw new Error( + `send-context sanity check expected response context ${expectedStamp}, got ${JSON.stringify(decoded)}`, + ) + } +} + +async function assertDocumentServerFnCallResponse(response: Response) { + const text = await response.text() + const expectedMarker = 'out-ssr-call-sanity-ssr' + + if (response.status !== 200) { + throw new Error( + `SSR-call sanity check failed with status ${response.status}: ${text}`, + ) + } + + const contentType = response.headers.get('content-type') + + if (!contentType?.includes('text/html')) { + throw new Error(`SSR-call sanity check was not HTML: ${contentType}`) + } + + if (response.headers.get(xTssSerialized)) { + throw new Error('SSR-call sanity check returned a server-fn RPC response') + } + + if (!text.includes('data-bench="server-fn-ssr-call"')) { + throw new Error('SSR-call sanity check missing document marker') + } + + if (!text.includes(expectedMarker)) { + throw new Error( + `SSR-call sanity check missing expected marker ${expectedMarker}`, + ) + } +} + +function buildGetRequest(urls: FnUrls, queries: Array, index: number) { + return new Request(`${origin}${urls.get}${queries[index % queries.length]}`, { + method: 'GET', + headers: commonHeaders, + }) +} + +function buildPostUrlRequest( + url: string, + bodies: Array, + index: number, +) { + return new Request(`${origin}${url}`, { + method: 'POST', + headers: postHeaders, + body: bodies[index % bodies.length], + }) +} + +function buildPostRequest(urls: FnUrls, bodies: Array, index: number) { + return buildPostUrlRequest(urls.post, bodies, index) +} + +function buildRedirectRequest( + urls: FnUrls, + bodies: Array, + index: number, +) { + return buildPostUrlRequest(urls.redirect, bodies, index) +} + +function buildNotFoundRequest( + urls: FnUrls, + bodies: Array, + index: number, +) { + return buildPostUrlRequest(urls.notFound, bodies, index) +} + +function buildContextRequest( + urls: FnUrls, + contextBodies: Array, + index: number, +) { + return buildPostUrlRequest(urls.context, contextBodies, index) +} + +function buildSsrCallRequest(random: () => number) { + return new Request( + `${origin}/ssr-call/${randomSegment(random)}`, + documentRequestInit, + ) +} + +export async function setupServerFnBench(handler: StartRequestHandler) { + const urls = await discoverUrls(handler) + const payloads = createPayloads() + const bodies = await createBodies(payloads) + const contextFixtures = await createContextFixtures(payloads) + const getQueries = createGetQueries(bodies) + const expectedMarker = `out-${payloads[0]!.nested.list[0]}` + + return { + urls, + bodies, + getQueries, + expectedMarker, + contextBodies: contextFixtures.map((fixture) => fixture.body), + contextExpectedMarkers: contextFixtures.map((fixture) => fixture.marker), + contextExpectedStamps: contextFixtures.map((fixture) => fixture.stamp), + } +} + +export async function assertServerFnScenario( + handler: StartRequestHandler, + context: ServerFnBenchContext, +) { + await assertServerFnResponse({ + response: await handler.fetch( + buildGetRequest(context.urls, context.getQueries, 0), + ), + label: 'GET', + expectedMarker: context.expectedMarker, + }) + await assertServerFnResponse({ + response: await handler.fetch( + buildPostRequest(context.urls, context.bodies, 0), + ), + label: 'POST', + expectedMarker: context.expectedMarker, + }) + await assertServerFnRedirectResponse( + await handler.fetch(buildRedirectRequest(context.urls, context.bodies, 0)), + ) + await assertServerFnNotFoundResponse( + await handler.fetch(buildNotFoundRequest(context.urls, context.bodies, 0)), + ) + await assertServerFnContextResponse({ + response: await handler.fetch( + buildContextRequest(context.urls, context.contextBodies, 0), + ), + expectedMarker: context.contextExpectedMarkers[0]!, + expectedStamp: context.contextExpectedStamps[0]!, + }) + await assertDocumentServerFnCallResponse( + await handler.fetch( + new Request(`${origin}/ssr-call/sanity-ssr`, documentRequestInit), + ), + ) +} + +export function runServerFnGetRequestLoop( + handler: StartRequestHandler, + context: ServerFnBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnRequestLoopOptions, + buildRequest: (_random, index) => + buildGetRequest(context.urls, context.getQueries, index), + }) +} + +export function runServerFnPostRequestLoop( + handler: StartRequestHandler, + context: ServerFnBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnRequestLoopOptions, + buildRequest: (_random, index) => + buildPostRequest(context.urls, context.bodies, index), + }) +} + +export function runServerFnRedirectRequestLoop( + handler: StartRequestHandler, + context: ServerFnBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnRequestLoopOptions, + buildRequest: (_random, index) => + buildRedirectRequest(context.urls, context.bodies, index), + validateResponse: validateRedirectResponse, + }) +} + +export function runServerFnNotFoundRequestLoop( + handler: StartRequestHandler, + context: ServerFnBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnRequestLoopOptions, + buildRequest: (_random, index) => + buildNotFoundRequest(context.urls, context.bodies, index), + validateResponse: (response) => + validateSerializedResponse(response, 'not-found'), + }) +} + +export function runServerFnSendContextRequestLoop( + handler: StartRequestHandler, + context: ServerFnBenchContext, +) { + return runRequestLoop(handler, { + ...serverFnRequestLoopOptions, + buildRequest: (_random, index) => + buildContextRequest(context.urls, context.contextBodies, index), + validateResponse: (response) => + validateSerializedResponse(response, 'send-context'), + }) +} + +export function runServerFnDocumentSsrRequestLoop( + handler: StartRequestHandler, +) { + return runRequestLoop(handler, { + ...serverFnSsrRequestLoopOptions, + buildRequest: buildSsrCallRequest, + validateResponse: validateDocumentResponse, + }) +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/project.json b/benchmarks/ssr/scenarios/server-fns/react/project.json new file mode 100644 index 00000000000..8858309f35e --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-fns-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/speed.bench.ts b/benchmarks/ssr/scenarios/server-fns/react/speed.bench.ts new file mode 100644 index 00000000000..46dca5adc77 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/speed.bench.ts @@ -0,0 +1,61 @@ +import { bench, describe } from 'vitest' +import { + assertServerFnScenario, + runServerFnDocumentSsrRequestLoop, + runServerFnGetRequestLoop, + runServerFnNotFoundRequestLoop, + runServerFnPostRequestLoop, + runServerFnRedirectRequestLoop, + runServerFnSendContextRequestLoop, + serverFnBenchOptions, + setupServerFnBench, +} from '../bench' +import type { StartRequestHandler } from '../bench' + +const { default: handler } = (await import( + /* @vite-ignore */ new URL('./dist/server/server.js', import.meta.url).href +)) as { + default: StartRequestHandler +} + +const context = await setupServerFnBench(handler) + +await assertServerFnScenario(handler, context) + +describe('ssr', () => { + bench( + 'ssr server-fn GET (react)', + () => runServerFnGetRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn POST (react)', + () => runServerFnPostRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn redirect (react)', + () => runServerFnRedirectRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn not-found (react)', + () => runServerFnNotFoundRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn send-context (react)', + () => runServerFnSendContextRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn during document ssr (react)', + () => runServerFnDocumentSsrRequestLoop(handler), + serverFnBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/fns.ts b/benchmarks/ssr/scenarios/server-fns/react/src/fns.ts new file mode 100644 index 00000000000..ce2ba48c5bc --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/fns.ts @@ -0,0 +1,87 @@ +import { createMiddleware, createServerFn } from '@tanstack/react-start' +import { notFound, redirect } from '@tanstack/react-router' + +const mwA = createMiddleware({ type: 'function' }).server(({ next }) => + next({ context: { a: 1 } }), +) +const mwB = createMiddleware({ type: 'function' }).server(({ next }) => + next({ context: { b: 2 } }), +) + +const sendContextMw = createMiddleware({ type: 'function' }) + .client(({ data, next }) => { + return next({ + sendContext: { token: createContextToken(data as unknown as Payload) }, + }) + }) + .server(({ context, next }) => { + const { token } = context as { token?: string } + + if (typeof token !== 'string') { + throw new Error('missing sendContext token') + } + + return next({ + context: { token }, + sendContext: { stamp: `stamp-${token}` }, + }) + }) + +type Payload = { q: string; n: number; nested: { list: Array } } + +function createContextToken(data: Payload) { + return `token-${data.q}-${data.nested.list[0]}` +} + +const validate = (input: unknown): Payload => { + const p = input as Payload + + if ( + typeof p?.q !== 'string' || + typeof p?.n !== 'number' || + !Array.isArray(p?.nested?.list) + ) { + throw new Error('invalid payload') + } + + return p +} + +function echo(data: Payload, context: { a: number; b: number }) { + return { + echoed: data, + sum: data.n + context.a + context.b, + list: data.nested.list.map((s) => `out-${s}`), + } +} + +export const echoGet = createServerFn({ method: 'GET' }) + .middleware([mwA, mwB]) + .validator(validate) + .handler(({ data, context }) => echo(data, context)) + +export const echoPost = createServerFn({ method: 'POST' }) + .middleware([mwA, mwB]) + .validator(validate) + .handler(({ data, context }) => echo(data, context)) + +export const redirector = createServerFn({ method: 'POST' }).handler(() => { + throw redirect({ to: '/', statusCode: 307 }) +}) + +export const notFounder = createServerFn({ method: 'POST' }).handler(() => { + throw notFound() +}) + +export const ctxEcho = createServerFn({ method: 'POST' }) + .middleware([sendContextMw]) + .validator(validate) + .handler(({ data, context }) => { + const { token } = context as { token: string } + + return { + marker: `ctx-${token}-${data.nested.list[0]}`, + q: data.q, + token, + } + }) diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-fns/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..1abf49d56be --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/routeTree.gen.ts @@ -0,0 +1,104 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as SsrCallIdRouteImport } from './routes/ssr-call.$id' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SsrCallIdRoute = SsrCallIdRouteImport.update({ + id: '/ssr-call/$id', + path: '/ssr-call/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' | '/ssr-call/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' | '/ssr-call/$id' + id: '__root__' | '/' | '/api/fn-urls' | '/ssr-call/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute + SsrCallIdRoute: typeof SsrCallIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/ssr-call/$id': { + id: '/ssr-call/$id' + path: '/ssr-call/$id' + fullPath: '/ssr-call/$id' + preLoaderRoute: typeof SsrCallIdRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, + SsrCallIdRoute: SsrCallIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/router.tsx b/benchmarks/ssr/scenarios/server-fns/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-fns/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/server-fns/react/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..ed3d9a9b7bf --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/routes/api.fn-urls.ts @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' +import { ctxEcho, echoGet, echoPost, notFounder, redirector } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: echoGet.url, + post: echoPost.url, + redirect: redirector.url, + notFound: notFounder.url, + context: ctxEcho.url, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-fns/react/src/routes/index.tsx new file mode 100644 index 00000000000..45636ef30e4 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { echoGet, echoPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ server-fns +
+ ) +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/src/routes/ssr-call.$id.tsx b/benchmarks/ssr/scenarios/server-fns/react/src/routes/ssr-call.$id.tsx new file mode 100644 index 00000000000..0f8a39f69d7 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/src/routes/ssr-call.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/react-router' +import { echoGet } from '../fns' + +export const Route = createFileRoute('/ssr-call/$id')({ + loader: async ({ params }) => { + const result = await echoGet({ + data: { + q: `ssr-${params.id}`, + n: 300, + nested: { list: [`ssr-call-${params.id}`] }, + }, + }) + + return { marker: result.list[0] } + }, + component: SsrCallComponent, +}) + +function SsrCallComponent() { + const data = Route.useLoaderData() + + return
{data.marker}
+} diff --git a/benchmarks/ssr/scenarios/server-fns/react/tsconfig.json b/benchmarks/ssr/scenarios/server-fns/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-fns/react/vite.config.ts b/benchmarks/ssr/scenarios/server-fns/react/vite.config.ts new file mode 100644 index 00000000000..4f608f04446 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-fns (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fns/solid/project.json b/benchmarks/ssr/scenarios/server-fns/solid/project.json new file mode 100644 index 00000000000..f1e9581897c --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-fns-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/speed.bench.ts b/benchmarks/ssr/scenarios/server-fns/solid/speed.bench.ts new file mode 100644 index 00000000000..a116ea9102a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/speed.bench.ts @@ -0,0 +1,61 @@ +import { bench, describe } from 'vitest' +import { + assertServerFnScenario, + runServerFnDocumentSsrRequestLoop, + runServerFnGetRequestLoop, + runServerFnNotFoundRequestLoop, + runServerFnPostRequestLoop, + runServerFnRedirectRequestLoop, + runServerFnSendContextRequestLoop, + serverFnBenchOptions, + setupServerFnBench, +} from '../bench' +import type { StartRequestHandler } from '../bench' + +const { default: handler } = (await import( + /* @vite-ignore */ new URL('./dist/server/server.js', import.meta.url).href +)) as { + default: StartRequestHandler +} + +const context = await setupServerFnBench(handler) + +await assertServerFnScenario(handler, context) + +describe('ssr', () => { + bench( + 'ssr server-fn GET (solid)', + () => runServerFnGetRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn POST (solid)', + () => runServerFnPostRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn redirect (solid)', + () => runServerFnRedirectRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn not-found (solid)', + () => runServerFnNotFoundRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn send-context (solid)', + () => runServerFnSendContextRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn during document ssr (solid)', + () => runServerFnDocumentSsrRequestLoop(handler), + serverFnBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/fns.ts b/benchmarks/ssr/scenarios/server-fns/solid/src/fns.ts new file mode 100644 index 00000000000..e9a81ad06e4 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/fns.ts @@ -0,0 +1,87 @@ +import { createMiddleware, createServerFn } from '@tanstack/solid-start' +import { notFound, redirect } from '@tanstack/solid-router' + +const mwA = createMiddleware({ type: 'function' }).server(({ next }) => + next({ context: { a: 1 } }), +) +const mwB = createMiddleware({ type: 'function' }).server(({ next }) => + next({ context: { b: 2 } }), +) + +const sendContextMw = createMiddleware({ type: 'function' }) + .client(({ data, next }) => { + return next({ + sendContext: { token: createContextToken(data as unknown as Payload) }, + }) + }) + .server(({ context, next }) => { + const { token } = context as { token?: string } + + if (typeof token !== 'string') { + throw new Error('missing sendContext token') + } + + return next({ + context: { token }, + sendContext: { stamp: `stamp-${token}` }, + }) + }) + +type Payload = { q: string; n: number; nested: { list: Array } } + +function createContextToken(data: Payload) { + return `token-${data.q}-${data.nested.list[0]}` +} + +const validate = (input: unknown): Payload => { + const p = input as Payload + + if ( + typeof p?.q !== 'string' || + typeof p?.n !== 'number' || + !Array.isArray(p?.nested?.list) + ) { + throw new Error('invalid payload') + } + + return p +} + +function echo(data: Payload, context: { a: number; b: number }) { + return { + echoed: data, + sum: data.n + context.a + context.b, + list: data.nested.list.map((s) => `out-${s}`), + } +} + +export const echoGet = createServerFn({ method: 'GET' }) + .middleware([mwA, mwB]) + .validator(validate) + .handler(({ data, context }) => echo(data, context)) + +export const echoPost = createServerFn({ method: 'POST' }) + .middleware([mwA, mwB]) + .validator(validate) + .handler(({ data, context }) => echo(data, context)) + +export const redirector = createServerFn({ method: 'POST' }).handler(() => { + throw redirect({ to: '/', statusCode: 307 }) +}) + +export const notFounder = createServerFn({ method: 'POST' }).handler(() => { + throw notFound() +}) + +export const ctxEcho = createServerFn({ method: 'POST' }) + .middleware([sendContextMw]) + .validator(validate) + .handler(({ data, context }) => { + const { token } = context as { token: string } + + return { + marker: `ctx-${token}-${data.nested.list[0]}`, + q: data.q, + token, + } + }) diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-fns/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..3db5e487b33 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/routeTree.gen.ts @@ -0,0 +1,104 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as SsrCallIdRouteImport } from './routes/ssr-call.$id' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SsrCallIdRoute = SsrCallIdRouteImport.update({ + id: '/ssr-call/$id', + path: '/ssr-call/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' | '/ssr-call/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' | '/ssr-call/$id' + id: '__root__' | '/' | '/api/fn-urls' | '/ssr-call/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute + SsrCallIdRoute: typeof SsrCallIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/ssr-call/$id': { + id: '/ssr-call/$id' + path: '/ssr-call/$id' + fullPath: '/ssr-call/$id' + preLoaderRoute: typeof SsrCallIdRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, + SsrCallIdRoute: SsrCallIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/router.tsx b/benchmarks/ssr/scenarios/server-fns/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..7e18f6a03b2 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/api.fn-urls.ts @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { ctxEcho, echoGet, echoPost, notFounder, redirector } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: echoGet.url, + post: echoPost.url, + redirect: redirector.url, + notFound: notFounder.url, + context: ctxEcho.url, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/index.tsx new file mode 100644 index 00000000000..83e7b223722 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { echoGet, echoPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ server-fns +
+ ) +} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/src/routes/ssr-call.$id.tsx b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/ssr-call.$id.tsx new file mode 100644 index 00000000000..abc5571a59a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/src/routes/ssr-call.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { echoGet } from '../fns' + +export const Route = createFileRoute('/ssr-call/$id')({ + loader: async ({ params }) => { + const result = await echoGet({ + data: { + q: `ssr-${params.id}`, + n: 300, + nested: { list: [`ssr-call-${params.id}`] }, + }, + }) + + return { marker: result.list[0] } + }, + component: SsrCallComponent, +}) + +function SsrCallComponent() { + const data = Route.useLoaderData() + + return
{data().marker}
+} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/tsconfig.json b/benchmarks/ssr/scenarios/server-fns/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-fns/solid/vite.config.ts b/benchmarks/ssr/scenarios/server-fns/solid/vite.config.ts new file mode 100644 index 00000000000..ef8292a042a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-fns (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fns/vue/project.json b/benchmarks/ssr/scenarios/server-fns/vue/project.json new file mode 100644 index 00000000000..ad6f0538380 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-fns-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/speed.bench.ts b/benchmarks/ssr/scenarios/server-fns/vue/speed.bench.ts new file mode 100644 index 00000000000..02cb03efd1f --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/speed.bench.ts @@ -0,0 +1,61 @@ +import { bench, describe } from 'vitest' +import { + assertServerFnScenario, + runServerFnDocumentSsrRequestLoop, + runServerFnGetRequestLoop, + runServerFnNotFoundRequestLoop, + runServerFnPostRequestLoop, + runServerFnRedirectRequestLoop, + runServerFnSendContextRequestLoop, + serverFnBenchOptions, + setupServerFnBench, +} from '../bench' +import type { StartRequestHandler } from '../bench' + +const { default: handler } = (await import( + /* @vite-ignore */ new URL('./dist/server/server.js', import.meta.url).href +)) as { + default: StartRequestHandler +} + +const context = await setupServerFnBench(handler) + +await assertServerFnScenario(handler, context) + +describe('ssr', () => { + bench( + 'ssr server-fn GET (vue)', + () => runServerFnGetRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn POST (vue)', + () => runServerFnPostRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn redirect (vue)', + () => runServerFnRedirectRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn not-found (vue)', + () => runServerFnNotFoundRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn send-context (vue)', + () => runServerFnSendContextRequestLoop(handler, context), + serverFnBenchOptions, + ) + + bench( + 'ssr server-fn during document ssr (vue)', + () => runServerFnDocumentSsrRequestLoop(handler), + serverFnBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/fns.ts b/benchmarks/ssr/scenarios/server-fns/vue/src/fns.ts new file mode 100644 index 00000000000..fc08b2b9bf6 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/fns.ts @@ -0,0 +1,87 @@ +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { notFound, redirect } from '@tanstack/vue-router' + +const mwA = createMiddleware({ type: 'function' }).server(({ next }) => + next({ context: { a: 1 } }), +) +const mwB = createMiddleware({ type: 'function' }).server(({ next }) => + next({ context: { b: 2 } }), +) + +const sendContextMw = createMiddleware({ type: 'function' }) + .client(({ data, next }) => { + return next({ + sendContext: { token: createContextToken(data as unknown as Payload) }, + }) + }) + .server(({ context, next }) => { + const { token } = context as { token?: string } + + if (typeof token !== 'string') { + throw new Error('missing sendContext token') + } + + return next({ + context: { token }, + sendContext: { stamp: `stamp-${token}` }, + }) + }) + +type Payload = { q: string; n: number; nested: { list: Array } } + +function createContextToken(data: Payload) { + return `token-${data.q}-${data.nested.list[0]}` +} + +const validate = (input: unknown): Payload => { + const p = input as Payload + + if ( + typeof p?.q !== 'string' || + typeof p?.n !== 'number' || + !Array.isArray(p?.nested?.list) + ) { + throw new Error('invalid payload') + } + + return p +} + +function echo(data: Payload, context: { a: number; b: number }) { + return { + echoed: data, + sum: data.n + context.a + context.b, + list: data.nested.list.map((s) => `out-${s}`), + } +} + +export const echoGet = createServerFn({ method: 'GET' }) + .middleware([mwA, mwB]) + .validator(validate) + .handler(({ data, context }) => echo(data, context)) + +export const echoPost = createServerFn({ method: 'POST' }) + .middleware([mwA, mwB]) + .validator(validate) + .handler(({ data, context }) => echo(data, context)) + +export const redirector = createServerFn({ method: 'POST' }).handler(() => { + throw redirect({ to: '/', statusCode: 307 }) +}) + +export const notFounder = createServerFn({ method: 'POST' }).handler(() => { + throw notFound() +}) + +export const ctxEcho = createServerFn({ method: 'POST' }) + .middleware([sendContextMw]) + .validator(validate) + .handler(({ data, context }) => { + const { token } = context as { token: string } + + return { + marker: `ctx-${token}-${data.nested.list[0]}`, + q: data.q, + token, + } + }) diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-fns/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..fa27fb1a5d3 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/routeTree.gen.ts @@ -0,0 +1,104 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as SsrCallIdRouteImport } from './routes/ssr-call.$id' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SsrCallIdRoute = SsrCallIdRouteImport.update({ + id: '/ssr-call/$id', + path: '/ssr-call/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute + '/ssr-call/$id': typeof SsrCallIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' | '/ssr-call/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' | '/ssr-call/$id' + id: '__root__' | '/' | '/api/fn-urls' | '/ssr-call/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute + SsrCallIdRoute: typeof SsrCallIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/ssr-call/$id': { + id: '/ssr-call/$id' + path: '/ssr-call/$id' + fullPath: '/ssr-call/$id' + preLoaderRoute: typeof SsrCallIdRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, + SsrCallIdRoute: SsrCallIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/router.tsx b/benchmarks/ssr/scenarios/server-fns/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/routes/api.fn-urls.ts b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/api.fn-urls.ts new file mode 100644 index 00000000000..0199e7f1d32 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/api.fn-urls.ts @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { ctxEcho, echoGet, echoPost, notFounder, redirector } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: echoGet.url, + post: echoPost.url, + redirect: redirector.url, + notFound: notFounder.url, + context: ctxEcho.url, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/index.tsx new file mode 100644 index 00000000000..e72e667bc5c --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { echoGet, echoPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ server-fns +
+ ) +} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/src/routes/ssr-call.$id.tsx b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/ssr-call.$id.tsx new file mode 100644 index 00000000000..8abe77c2af6 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/src/routes/ssr-call.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { echoGet } from '../fns' + +export const Route = createFileRoute('/ssr-call/$id')({ + loader: async ({ params }) => { + const result = await echoGet({ + data: { + q: `ssr-${params.id}`, + n: 300, + nested: { list: [`ssr-call-${params.id}`] }, + }, + }) + + return { marker: result.list[0] } + }, + component: SsrCallComponent, +}) + +function SsrCallComponent() { + const data = Route.useLoaderData() + + return
{data.value.marker}
+} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/tsconfig.json b/benchmarks/ssr/scenarios/server-fns/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-fns/vue/vite.config.ts b/benchmarks/ssr/scenarios/server-fns/vue/vite.config.ts new file mode 100644 index 00000000000..2a7783ed7ab --- /dev/null +++ b/benchmarks/ssr/scenarios/server-fns/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-fns (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/project.json b/benchmarks/ssr/scenarios/server-routes-middleware/react/project.json new file mode 100644 index 00000000000..e5f11d0a57d --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-routes-middleware-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/speed.bench.ts b/benchmarks/ssr/scenarios/server-routes-middleware/react/speed.bench.ts new file mode 100644 index 00000000000..8e96bc8e041 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertServerRouteMiddlewareResponse, + runServerRouteMiddlewareLoop, + serverRouteMiddlewareBenchOptions, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertServerRouteMiddlewareResponse(handler) + +describe('ssr', () => { + bench( + 'ssr server-route middleware (react)', + () => runServerRouteMiddlewareLoop(handler), + serverRouteMiddlewareBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/src/middleware.ts b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/middleware.ts new file mode 100644 index 00000000000..1e8c4286964 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/middleware.ts @@ -0,0 +1,16 @@ +import { createMiddleware } from '@tanstack/react-start' + +const make = (key: TKey, value: number) => + createMiddleware({ type: 'request' }).server(({ next }) => + next({ context: { [key]: value } as Record }), + ) + +export const routeMws = [ + make('r1', 1), + make('r2', 2), + make('r3', 3), + make('r4', 4), + make('r5', 5), +] as const + +export const methodMws = [make('m1', 6), make('m2', 7)] as const diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..7a63a799a16 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiChainIdRouteImport } from './routes/api.chain.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiChainIdRoute = ApiChainIdRouteImport.update({ + id: '/api/chain/$id', + path: '/api/chain/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/chain/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/chain/$id' + id: '__root__' | '/' | '/api/chain/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiChainIdRoute: typeof ApiChainIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/chain/$id': { + id: '/api/chain/$id' + path: '/api/chain/$id' + fullPath: '/api/chain/$id' + preLoaderRoute: typeof ApiChainIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiChainIdRoute: ApiChainIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/src/router.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/api.chain.$id.ts b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/api.chain.$id.ts new file mode 100644 index 00000000000..5a9f5d50355 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/api.chain.$id.ts @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/react-router' +import { methodMws, routeMws } from '../middleware' + +const allMws = [...routeMws, ...methodMws] as const + +export const Route = createFileRoute('/api/chain/$id')({ + server: { + middleware: allMws, + handlers: { + GET: ({ params, context }) => + Response.json({ + id: params.id, + total: + context.r1 + + context.r2 + + context.r3 + + context.r4 + + context.r5 + + context.m1 + + context.m2, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/index.tsx new file mode 100644 index 00000000000..9fe98b68512 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
server routes middleware
+} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/tsconfig.json b/benchmarks/ssr/scenarios/server-routes-middleware/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/react/vite.config.ts b/benchmarks/ssr/scenarios/server-routes-middleware/react/vite.config.ts new file mode 100644 index 00000000000..9c41f6d542d --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-routes-middleware (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/shared.ts b/benchmarks/ssr/scenarios/server-routes-middleware/shared.ts new file mode 100644 index 00000000000..30bbc5d6f45 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/shared.ts @@ -0,0 +1,57 @@ +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const loopIterations = 100 + +const apiRequestInit = { + method: 'GET', + headers: { + accept: 'application/json', + }, +} satisfies RequestInit + +export const serverRouteMiddlewareBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runServerRouteMiddlewareLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: loopIterations, + buildRequest: (random) => + new Request( + `http://localhost/api/chain/${randomSegment(random)}`, + apiRequestInit, + ), + }) +} + +export async function assertServerRouteMiddlewareResponse( + handler: StartRequestHandler, +) { + const id = 'sanity' + const response = await handler.fetch( + new Request(`http://localhost/api/chain/${id}`, apiRequestInit), + ) + + if (response.status !== 200) { + throw new Error(`Expected status 200, received ${response.status}`) + } + + const contentType = response.headers.get('content-type') + + if (!contentType?.includes('application/json')) { + throw new Error(`Expected JSON response, received ${contentType}`) + } + + const body = (await response.json()) as { total?: number } + + if (body.total !== 28) { + throw new Error(`Expected total 28, received ${body.total}`) + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/project.json b/benchmarks/ssr/scenarios/server-routes-middleware/solid/project.json new file mode 100644 index 00000000000..0bde91f7949 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-routes-middleware-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/speed.bench.ts b/benchmarks/ssr/scenarios/server-routes-middleware/solid/speed.bench.ts new file mode 100644 index 00000000000..7e6fac4bbe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertServerRouteMiddlewareResponse, + runServerRouteMiddlewareLoop, + serverRouteMiddlewareBenchOptions, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertServerRouteMiddlewareResponse(handler) + +describe('ssr', () => { + bench( + 'ssr server-route middleware (solid)', + () => runServerRouteMiddlewareLoop(handler), + serverRouteMiddlewareBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/middleware.ts b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/middleware.ts new file mode 100644 index 00000000000..fcaf3d08f7d --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/middleware.ts @@ -0,0 +1,16 @@ +import { createMiddleware } from '@tanstack/solid-start' + +const make = (key: TKey, value: number) => + createMiddleware({ type: 'request' }).server(({ next }) => + next({ context: { [key]: value } as Record }), + ) + +export const routeMws = [ + make('r1', 1), + make('r2', 2), + make('r3', 3), + make('r4', 4), + make('r5', 5), +] as const + +export const methodMws = [make('m1', 6), make('m2', 7)] as const diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..b8c91b91349 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiChainIdRouteImport } from './routes/api.chain.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiChainIdRoute = ApiChainIdRouteImport.update({ + id: '/api/chain/$id', + path: '/api/chain/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/chain/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/chain/$id' + id: '__root__' | '/' | '/api/chain/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiChainIdRoute: typeof ApiChainIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/chain/$id': { + id: '/api/chain/$id' + path: '/api/chain/$id' + fullPath: '/api/chain/$id' + preLoaderRoute: typeof ApiChainIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiChainIdRoute: ApiChainIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/router.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/api.chain.$id.ts b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/api.chain.$id.ts new file mode 100644 index 00000000000..fabdb9de857 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/api.chain.$id.ts @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { methodMws, routeMws } from '../middleware' + +const allMws = [...routeMws, ...methodMws] as const + +export const Route = createFileRoute('/api/chain/$id')({ + server: { + middleware: allMws, + handlers: { + GET: ({ params, context }) => + Response.json({ + id: params.id, + total: + context.r1 + + context.r2 + + context.r3 + + context.r4 + + context.r5 + + context.m1 + + context.m2, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/index.tsx new file mode 100644 index 00000000000..e2357b558df --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
server routes middleware
+} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/tsconfig.json b/benchmarks/ssr/scenarios/server-routes-middleware/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/solid/vite.config.ts b/benchmarks/ssr/scenarios/server-routes-middleware/solid/vite.config.ts new file mode 100644 index 00000000000..89116c111ef --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-routes-middleware (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/project.json b/benchmarks/ssr/scenarios/server-routes-middleware/vue/project.json new file mode 100644 index 00000000000..2a21b81bf92 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-routes-middleware-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/speed.bench.ts b/benchmarks/ssr/scenarios/server-routes-middleware/vue/speed.bench.ts new file mode 100644 index 00000000000..e67ca0a4204 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertServerRouteMiddlewareResponse, + runServerRouteMiddlewareLoop, + serverRouteMiddlewareBenchOptions, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertServerRouteMiddlewareResponse(handler) + +describe('ssr', () => { + bench( + 'ssr server-route middleware (vue)', + () => runServerRouteMiddlewareLoop(handler), + serverRouteMiddlewareBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/middleware.ts b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/middleware.ts new file mode 100644 index 00000000000..05e7a86be9c --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/middleware.ts @@ -0,0 +1,16 @@ +import { createMiddleware } from '@tanstack/vue-start' + +const make = (key: TKey, value: number) => + createMiddleware({ type: 'request' }).server(({ next }) => + next({ context: { [key]: value } as Record }), + ) + +export const routeMws = [ + make('r1', 1), + make('r2', 2), + make('r3', 3), + make('r4', 4), + make('r5', 5), +] as const + +export const methodMws = [make('m1', 6), make('m2', 7)] as const diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..db4f072a699 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiChainIdRouteImport } from './routes/api.chain.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiChainIdRoute = ApiChainIdRouteImport.update({ + id: '/api/chain/$id', + path: '/api/chain/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/chain/$id': typeof ApiChainIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/chain/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/chain/$id' + id: '__root__' | '/' | '/api/chain/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiChainIdRoute: typeof ApiChainIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/chain/$id': { + id: '/api/chain/$id' + path: '/api/chain/$id' + fullPath: '/api/chain/$id' + preLoaderRoute: typeof ApiChainIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiChainIdRoute: ApiChainIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/router.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/api.chain.$id.ts b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/api.chain.$id.ts new file mode 100644 index 00000000000..d643a339c51 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/api.chain.$id.ts @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { methodMws, routeMws } from '../middleware' + +const allMws = [...routeMws, ...methodMws] as const + +export const Route = createFileRoute('/api/chain/$id')({ + server: { + middleware: allMws, + handlers: { + GET: ({ params, context }) => + Response.json({ + id: params.id, + total: + context.r1 + + context.r2 + + context.r3 + + context.r4 + + context.r5 + + context.m1 + + context.m2, + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/index.tsx new file mode 100644 index 00000000000..60e9d08e6db --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
server routes middleware
+} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/tsconfig.json b/benchmarks/ssr/scenarios/server-routes-middleware/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-routes-middleware/vue/vite.config.ts b/benchmarks/ssr/scenarios/server-routes-middleware/vue/vite.config.ts new file mode 100644 index 00000000000..a1b26182f71 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes-middleware/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-routes-middleware (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes/react/project.json b/benchmarks/ssr/scenarios/server-routes/react/project.json new file mode 100644 index 00000000000..a16cb3e5379 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-routes-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/react/speed.bench.ts b/benchmarks/ssr/scenarios/server-routes/react/speed.bench.ts new file mode 100644 index 00000000000..1bdb7946b29 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertServerRouteResponse, + runServerRouteLoop, + serverRouteBenchOptions, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertServerRouteResponse(handler) + +describe('ssr', () => { + bench( + 'ssr server-route (react)', + () => runServerRouteLoop(handler), + serverRouteBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-routes/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-routes/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..6615cc0611a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiUsersIdRouteImport } from './routes/api.users.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiUsersIdRoute = ApiUsersIdRouteImport.update({ + id: '/api/users/$id', + path: '/api/users/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/users/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/users/$id' + id: '__root__' | '/' | '/api/users/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiUsersIdRoute: typeof ApiUsersIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/users/$id': { + id: '/api/users/$id' + path: '/api/users/$id' + fullPath: '/api/users/$id' + preLoaderRoute: typeof ApiUsersIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiUsersIdRoute: ApiUsersIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/react/src/router.tsx b/benchmarks/ssr/scenarios/server-routes/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-routes/react/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-routes/react/src/routes/api.users.$id.ts b/benchmarks/ssr/scenarios/server-routes/react/src/routes/api.users.$id.ts new file mode 100644 index 00000000000..b43275a5c6e --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/src/routes/api.users.$id.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/users/$id')({ + server: { + handlers: { + GET: ({ params }) => + Response.json({ + id: params.id, + name: `user-${params.id}`, + roles: ['a', 'b', 'c'], + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes/react/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-routes/react/src/routes/index.tsx new file mode 100644 index 00000000000..f5a19839f98 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
server routes
+} diff --git a/benchmarks/ssr/scenarios/server-routes/react/tsconfig.json b/benchmarks/ssr/scenarios/server-routes/react/tsconfig.json new file mode 100644 index 00000000000..91027bfc888 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-routes/react/vite.config.ts b/benchmarks/ssr/scenarios/server-routes/react/vite.config.ts new file mode 100644 index 00000000000..fdc16bf50c9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-routes (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes/shared.ts b/benchmarks/ssr/scenarios/server-routes/shared.ts new file mode 100644 index 00000000000..9b531217ff6 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/shared.ts @@ -0,0 +1,55 @@ +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad +const loopIterations = 100 + +const apiRequestInit = { + method: 'GET', + headers: { + accept: 'application/json', + }, +} satisfies RequestInit + +export const serverRouteBenchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runServerRouteLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + iterations: loopIterations, + buildRequest: (random) => + new Request( + `http://localhost/api/users/${randomSegment(random)}`, + apiRequestInit, + ), + }) +} + +export async function assertServerRouteResponse(handler: StartRequestHandler) { + const id = 'sanity' + const response = await handler.fetch( + new Request(`http://localhost/api/users/${id}`, apiRequestInit), + ) + + if (response.status !== 200) { + throw new Error(`Expected status 200, received ${response.status}`) + } + + const contentType = response.headers.get('content-type') + + if (!contentType?.includes('application/json')) { + throw new Error(`Expected JSON response, received ${contentType}`) + } + + const body = (await response.json()) as { name?: string } + + if (body.name !== `user-${id}`) { + throw new Error(`Expected user-${id}, received ${body.name}`) + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/project.json b/benchmarks/ssr/scenarios/server-routes/solid/project.json new file mode 100644 index 00000000000..9c4bf7e2302 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-routes-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/speed.bench.ts b/benchmarks/ssr/scenarios/server-routes/solid/speed.bench.ts new file mode 100644 index 00000000000..ce3e1f8a213 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertServerRouteResponse, + runServerRouteLoop, + serverRouteBenchOptions, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertServerRouteResponse(handler) + +describe('ssr', () => { + bench( + 'ssr server-route (solid)', + () => runServerRouteLoop(handler), + serverRouteBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-routes/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-routes/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..1d7fee0d2b9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiUsersIdRouteImport } from './routes/api.users.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiUsersIdRoute = ApiUsersIdRouteImport.update({ + id: '/api/users/$id', + path: '/api/users/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/users/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/users/$id' + id: '__root__' | '/' | '/api/users/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiUsersIdRoute: typeof ApiUsersIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/users/$id': { + id: '/api/users/$id' + path: '/api/users/$id' + fullPath: '/api/users/$id' + preLoaderRoute: typeof ApiUsersIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiUsersIdRoute: ApiUsersIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/src/router.tsx b/benchmarks/ssr/scenarios/server-routes/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-routes/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/src/routes/api.users.$id.ts b/benchmarks/ssr/scenarios/server-routes/solid/src/routes/api.users.$id.ts new file mode 100644 index 00000000000..516af1cc157 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/src/routes/api.users.$id.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/api/users/$id')({ + server: { + handlers: { + GET: ({ params }) => + Response.json({ + id: params.id, + name: `user-${params.id}`, + roles: ['a', 'b', 'c'], + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes/solid/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-routes/solid/src/routes/index.tsx new file mode 100644 index 00000000000..87eb6a178fe --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
server routes
+} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/tsconfig.json b/benchmarks/ssr/scenarios/server-routes/solid/tsconfig.json new file mode 100644 index 00000000000..b1806caa67a --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-routes/solid/vite.config.ts b/benchmarks/ssr/scenarios/server-routes/solid/vite.config.ts new file mode 100644 index 00000000000..24e502a63a8 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-routes (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes/vue/project.json b/benchmarks/ssr/scenarios/server-routes/vue/project.json new file mode 100644 index 00000000000..a92a9ad40ef --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-server-routes-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/vue/speed.bench.ts b/benchmarks/ssr/scenarios/server-routes/vue/speed.bench.ts new file mode 100644 index 00000000000..f2b9e27acb5 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertServerRouteResponse, + runServerRouteLoop, + serverRouteBenchOptions, +} from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertServerRouteResponse(handler) + +describe('ssr', () => { + bench( + 'ssr server-route (vue)', + () => runServerRouteLoop(handler), + serverRouteBenchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/server-routes/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/server-routes/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..b12a446b9f6 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 IndexRouteImport } from './routes/index' +import { Route as ApiUsersIdRouteImport } from './routes/api.users.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiUsersIdRoute = ApiUsersIdRouteImport.update({ + id: '/api/users/$id', + path: '/api/users/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/users/$id': typeof ApiUsersIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/users/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/users/$id' + id: '__root__' | '/' | '/api/users/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiUsersIdRoute: typeof ApiUsersIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/users/$id': { + id: '/api/users/$id' + path: '/api/users/$id' + fullPath: '/api/users/$id' + preLoaderRoute: typeof ApiUsersIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiUsersIdRoute: ApiUsersIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/vue/src/router.tsx b/benchmarks/ssr/scenarios/server-routes/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/server-routes/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/server-routes/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..49422aac381 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/src/routes/__root.tsx @@ -0,0 +1,26 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/server-routes/vue/src/routes/api.users.$id.ts b/benchmarks/ssr/scenarios/server-routes/vue/src/routes/api.users.$id.ts new file mode 100644 index 00000000000..7766b52acc7 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/src/routes/api.users.$id.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/api/users/$id')({ + server: { + handlers: { + GET: ({ params }) => + Response.json({ + id: params.id, + name: `user-${params.id}`, + roles: ['a', 'b', 'c'], + }), + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/server-routes/vue/src/routes/index.tsx b/benchmarks/ssr/scenarios/server-routes/vue/src/routes/index.tsx new file mode 100644 index 00000000000..39d1e2f9358 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
server routes
+} diff --git a/benchmarks/ssr/scenarios/server-routes/vue/tsconfig.json b/benchmarks/ssr/scenarios/server-routes/vue/tsconfig.json new file mode 100644 index 00000000000..4fe3ccecb16 --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/server-routes/vue/vite.config.ts b/benchmarks/ssr/scenarios/server-routes/vue/vite.config.ts new file mode 100644 index 00000000000..c56f3c872ad --- /dev/null +++ b/benchmarks/ssr/scenarios/server-routes/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr server-routes (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/streaming/react/project.json b/benchmarks/ssr/scenarios/streaming/react/project.json new file mode 100644 index 00000000000..a905aa34746 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-streaming-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", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/streaming/react/speed.bench.ts b/benchmarks/ssr/scenarios/streaming/react/speed.bench.ts new file mode 100644 index 00000000000..c1dbf38ae57 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertStreamingSanity, + benchOptions, + runStreamingLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertStreamingSanity(handler) + +describe('ssr', () => { + bench( + 'ssr streaming deferred (react)', + () => runStreamingLoop(handler), + benchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/streaming/react/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/streaming/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..eb1af78a41d --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 StreamIdRouteImport } from './routes/stream.$id' + +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/stream/$id' + id: '__root__' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/streaming/react/src/router.tsx b/benchmarks/ssr/scenarios/streaming/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/streaming/react/src/routes/__root.tsx b/benchmarks/ssr/scenarios/streaming/react/src/routes/__root.tsx new file mode 100644 index 00000000000..1973ca20bb1 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/src/routes/__root.tsx @@ -0,0 +1,25 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/streaming/react/src/routes/stream.$id.tsx b/benchmarks/ssr/scenarios/streaming/react/src/routes/stream.$id.tsx new file mode 100644 index 00000000000..342f7ceb0c9 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/src/routes/stream.$id.tsx @@ -0,0 +1,39 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' +import { makeBigPayload, sleep0 } from '../../../shared-data' + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + fast: { label: `fast-${params.id}` }, + slowSmall: sleep0().then(() => ({ label: `slow-small-${params.id}` })), + slowBig: sleep0().then(() => makeBigPayload(params.id)), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( + <> +

{data.fast.label}

+

loading-small

+ + {(d) =>

{d.label}

}
+
+

loading-big

+ + + {(d) => ( +
+

{d.label}

+ {d.chunks.map((chunk) => ( +

{chunk.value}

+ ))} +
+ )} +
+
+ + ) +} diff --git a/benchmarks/ssr/scenarios/streaming/react/tsconfig.json b/benchmarks/ssr/scenarios/streaming/react/tsconfig.json new file mode 100644 index 00000000000..8800818162e --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/streaming/react/vite.config.ts b/benchmarks/ssr/scenarios/streaming/react/vite.config.ts new file mode 100644 index 00000000000..bdbfebdfcef --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr streaming (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/scenarios/streaming/shared-bench.ts b/benchmarks/ssr/scenarios/streaming/shared-bench.ts new file mode 100644 index 00000000000..25685b3c78f --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/shared-bench.ts @@ -0,0 +1,76 @@ +import { expect } from 'vitest' +import { streamChunkCount } from './shared-data' +import { randomSegment, runRequestLoop } from '../../bench-utils' +import type { StartRequestHandler } from '../../bench-utils' + +export type { StartRequestHandler } + +const benchmarkSeed = 0xdecafbad + +export const CHROME_UA = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + 'user-agent': CHROME_UA, + }, +} satisfies RequestInit + +function buildStreamingRequest(random: () => number) { + return new Request( + `http://localhost/stream/${randomSegment(random)}`, + requestInit, + ) +} + +function getMarkerIndexes(body: string, marker: string) { + const indexes: Array = [] + let index = body.indexOf(marker) + + while (index !== -1) { + indexes.push(index) + index = body.indexOf(marker, index + marker.length) + } + + return indexes +} + +export async function assertStreamingSanity(handler: StartRequestHandler) { + const id = 'sanity-stream' + const response = await handler.fetch( + new Request(`http://localhost/stream/${id}`, requestInit), + ) + const body = await response.text() + const loadingSmallIndexes = getMarkerIndexes(body, 'loading-small') + const loadingBigIndexes = getMarkerIndexes(body, 'loading-big') + const tsrIndex = body.indexOf('$_TSR') + + expect(response.status).toBe(200) + expect(loadingSmallIndexes).toHaveLength(1) + expect(loadingBigIndexes).toHaveLength(1) + expect(body).toContain(`slow-small-${id}`) + expect(body).toContain(`slow-big-${id}`) + expect(body).toContain(`${id}:0:`) + expect(body).toContain(`${id}:${streamChunkCount - 1}:`) + + const loadingSmallIndex = loadingSmallIndexes[0]! + const loadingBigIndex = loadingBigIndexes[0]! + + expect(loadingBigIndex).toBeGreaterThan(loadingSmallIndex) + expect(tsrIndex).toBeGreaterThan(loadingBigIndex) +} + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} + +export function runStreamingLoop(handler: StartRequestHandler) { + return runRequestLoop(handler, { + seed: benchmarkSeed, + buildRequest: buildStreamingRequest, + }) +} diff --git a/benchmarks/ssr/scenarios/streaming/shared-data.ts b/benchmarks/ssr/scenarios/streaming/shared-data.ts new file mode 100644 index 00000000000..550a3517949 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/shared-data.ts @@ -0,0 +1,65 @@ +export interface SmallPayload { + label: string +} + +export interface BigPayload { + label: string + chunks: Array<{ + index: number + value: string + }> +} + +const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' +export const streamChunkCount = 96 +export const streamChunkLength = 192 + +export function sleep0() { + return new Promise((resolve) => setTimeout(resolve, 1)) +} + +function hashId(id: string) { + let state = 2166136261 + + for (let index = 0; index < id.length; index++) { + state ^= id.charCodeAt(index) + state = Math.imul(state, 16777619) >>> 0 + } + + return state || 1 +} + +function nextState(state: number) { + return (state * 1664525 + 1013904223) >>> 0 +} + +function makeChunk(seed: number) { + let state = seed + let value = '' + + for (let index = 0; index < streamChunkLength; index++) { + state = nextState(state) + value += alphabet[state % alphabet.length] + } + + return { state, value } +} + +export function makeBigPayload(id: string): BigPayload { + let state = hashId(id) + const chunks: BigPayload['chunks'] = [] + + for (let index = 0; index < streamChunkCount; index++) { + const chunk = makeChunk(state + index) + state = chunk.state + chunks.push({ + index, + value: `${id}:${index}:${chunk.value}`, + }) + } + + return { + label: `slow-big-${id}`, + chunks, + } +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/project.json b/benchmarks/ssr/scenarios/streaming/solid/project.json new file mode 100644 index 00000000000..15e5f211036 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-streaming-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/speed.bench.ts b/benchmarks/ssr/scenarios/streaming/solid/speed.bench.ts new file mode 100644 index 00000000000..08287152204 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertStreamingSanity, + benchOptions, + runStreamingLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertStreamingSanity(handler) + +describe('ssr', () => { + bench( + 'ssr streaming deferred (solid)', + () => runStreamingLoop(handler), + benchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/streaming/solid/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/streaming/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..e015aee737d --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 StreamIdRouteImport } from './routes/stream.$id' + +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/stream/$id' + id: '__root__' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/src/router.tsx b/benchmarks/ssr/scenarios/streaming/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/src/routes/__root.tsx b/benchmarks/ssr/scenarios/streaming/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..15de858e78c --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/src/routes/__root.tsx @@ -0,0 +1,25 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/src/routes/stream.$id.tsx b/benchmarks/ssr/scenarios/streaming/solid/src/routes/stream.$id.tsx new file mode 100644 index 00000000000..1a65fff950c --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/src/routes/stream.$id.tsx @@ -0,0 +1,39 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { Suspense } from 'solid-js' +import { makeBigPayload, sleep0 } from '../../../shared-data' + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + fast: { label: `fast-${params.id}` }, + slowSmall: sleep0().then(() => ({ label: `slow-small-${params.id}` })), + slowBig: sleep0().then(() => makeBigPayload(params.id)), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( + <> +

{data().fast.label}

+

loading-small

+ + {(d) =>

{d.label}

}
+
+

loading-big

+ + + {(d) => ( +
+

{d.label}

+ {d.chunks.map((chunk) => ( +

{chunk.value}

+ ))} +
+ )} +
+
+ + ) +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/tsconfig.json b/benchmarks/ssr/scenarios/streaming/solid/tsconfig.json new file mode 100644 index 00000000000..47f264ba4f5 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/streaming/solid/vite.config.ts b/benchmarks/ssr/scenarios/streaming/solid/vite.config.ts new file mode 100644 index 00000000000..5b736899c07 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr streaming (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/scenarios/streaming/vue/project.json b/benchmarks/ssr/scenarios/streaming/vue/project.json new file mode 100644 index 00000000000..acaa7c460c7 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-streaming-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/scenarios/streaming/vue/speed.bench.ts b/benchmarks/ssr/scenarios/streaming/vue/speed.bench.ts new file mode 100644 index 00000000000..a484f95d6aa --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/speed.bench.ts @@ -0,0 +1,25 @@ +import { bench, describe } from 'vitest' +import { + assertStreamingSanity, + benchOptions, + runStreamingLoop, + type StartRequestHandler, +} from '../shared-bench' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +await assertStreamingSanity(handler) + +describe('ssr', () => { + bench( + 'ssr streaming deferred (vue)', + () => runStreamingLoop(handler), + benchOptions, + ) +}) diff --git a/benchmarks/ssr/scenarios/streaming/vue/src/routeTree.gen.ts b/benchmarks/ssr/scenarios/streaming/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..2ec10975f25 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 StreamIdRouteImport } from './routes/stream.$id' + +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/stream/$id' + id: '__root__' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/ssr/scenarios/streaming/vue/src/router.tsx b/benchmarks/ssr/scenarios/streaming/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/ssr/scenarios/streaming/vue/src/routes/__root.tsx b/benchmarks/ssr/scenarios/streaming/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..4b035198230 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, + validateSearch: (s) => s as { q?: string }, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/ssr/scenarios/streaming/vue/src/routes/stream.$id.tsx b/benchmarks/ssr/scenarios/streaming/vue/src/routes/stream.$id.tsx new file mode 100644 index 00000000000..d627fa56c9e --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/src/routes/stream.$id.tsx @@ -0,0 +1,56 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense, defineComponent } from 'vue' +import { makeBigPayload, sleep0 } from '../../../shared-data' +import type { BigPayload, SmallPayload } from '../../../shared-data' + +const StreamComponent = defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () => ( + <> +

{data.value.fast.label}

+

loading-small

+ + {{ + default: () => ( +

{d.label}

} + /> + ), + fallback: () => null, + }} +
+

loading-big

+ + {{ + default: () => ( + ( +
+

{d.label}

+ {d.chunks.map((chunk) => ( +

{chunk.value}

+ ))} +
+ )} + /> + ), + fallback: () => null, + }} +
+ + ) + }, +}) + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + fast: { label: `fast-${params.id}` }, + slowSmall: sleep0().then(() => ({ label: `slow-small-${params.id}` })), + slowBig: sleep0().then(() => makeBigPayload(params.id)), + }), + component: StreamComponent, +}) diff --git a/benchmarks/ssr/scenarios/streaming/vue/tsconfig.json b/benchmarks/ssr/scenarios/streaming/vue/tsconfig.json new file mode 100644 index 00000000000..c302f637701 --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../shared-data.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/ssr/scenarios/streaming/vue/vite.config.ts b/benchmarks/ssr/scenarios/streaming/vue/vite.config.ts new file mode 100644 index 00000000000..8c65f71268c --- /dev/null +++ b/benchmarks/ssr/scenarios/streaming/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/ssr streaming (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/solid/project.json b/benchmarks/ssr/solid/project.json new file mode 100644 index 00000000000..0f19144d3bc --- /dev/null +++ b/benchmarks/ssr/solid/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/ssr/vitest.config.ts b/benchmarks/ssr/vitest.config.ts index 14776452ed8..a91eae4967a 100644 --- a/benchmarks/ssr/vitest.config.ts +++ b/benchmarks/ssr/vitest.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ './react/vite.config.ts', './solid/vite.config.ts', './vue/vite.config.ts', + './scenarios/*/*/vite.config.ts', ], }, }) diff --git a/benchmarks/ssr/vitest.react.config.ts b/benchmarks/ssr/vitest.react.config.ts new file mode 100644 index 00000000000..a397ad929ef --- /dev/null +++ b/benchmarks/ssr/vitest.react.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./react/vite.config.ts', './scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/ssr/vitest.solid.config.ts b/benchmarks/ssr/vitest.solid.config.ts new file mode 100644 index 00000000000..b8827b86aa0 --- /dev/null +++ b/benchmarks/ssr/vitest.solid.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./solid/vite.config.ts', './scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/ssr/vitest.vue.config.ts b/benchmarks/ssr/vitest.vue.config.ts new file mode 100644 index 00000000000..17960d7569d --- /dev/null +++ b/benchmarks/ssr/vitest.vue.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./vue/vite.config.ts', './scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/benchmarks/ssr/vue/project.json b/benchmarks/ssr/vue/project.json new file mode 100644 index 00000000000..930d6c2e223 --- /dev/null +++ b/benchmarks/ssr/vue/project.json @@ -0,0 +1,31 @@ +{ + "name": "@benchmarks/ssr-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/nx.json b/nx.json index 88eb404453b..08e75636421 100644 --- a/nx.json +++ b/nx.json @@ -65,6 +65,11 @@ "dependsOn": ["^build"], "inputs": ["default", "^production"] }, + "test:types:ssr": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^production"] + }, "build": { "cache": true, "dependsOn": ["^build"], diff --git a/package.json b/package.json index 742fe4a82c1..75bc1531693 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "test:build": "nx affected --target=test:build --exclude=examples/**", "test:types": "nx affected --target=test:types --exclude=examples/**", "test:e2e": "nx run-many --target=test:e2e", - "benchmark:bundle-size": "pnpm nx run @benchmarks/bundle-size:build", + "benchmark:bundle-size": "nx run @benchmarks/bundle-size:build", "benchmark:bundle-size:query": "node scripts/benchmarks/bundle-size/query.mjs", "benchmark:bundle-size:diff": "node scripts/benchmarks/bundle-size/diff.mjs", "benchmark:bundle-size:history": "node scripts/benchmarks/bundle-size/history.mjs", "benchmark:bundle-size:analyze": "node scripts/benchmarks/bundle-size/analyze.mjs", - "benchmark:client-nav": "pnpm nx run @benchmarks/client-nav:test:perf", - "benchmark:ssr": "pnpm nx run @benchmarks/ssr:test:perf", + "benchmark:client-nav": "nx run @benchmarks/client-nav:test:perf", + "benchmark:ssr": "nx run @benchmarks/ssr:test:perf", "build": "nx affected --target=build --exclude=e2e/** --exclude=examples/**", "build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/**", "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7938cd51f46..efb2ac9b5fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,6 +355,9 @@ importers: '@vitejs/plugin-vue-jsx': specifier: ^5.1.5 version: 5.1.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + seroval: + specifier: ^1.5.4 + version: 1.5.4 typescript: specifier: ^6.0.2 version: 6.0.2