diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index 2f6429392b0..fc5fd5375e5 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -18,7 +18,7 @@ env: jobs: client-nav-benchmarks: - name: Run Client Nav Benchmarks + name: Run Client Nav and SSR Benchmarks runs-on: ubuntu-latest steps: - name: Checkout @@ -47,3 +47,24 @@ jobs: with: mode: simulation run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/client-nav:test:perf:vue + + - name: Run CodSpeed SSR benchmark for React + continue-on-error: true + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/ssr:test:perf:react + + - name: Run CodSpeed SSR benchmark for Solid + continue-on-error: true + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/ssr:test:perf:solid + + - name: Run CodSpeed SSR benchmark for Vue + continue-on-error: true + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/ssr:test:perf:vue diff --git a/benchmarks/ssr/README.md b/benchmarks/ssr/README.md new file mode 100644 index 00000000000..9fb429ecfe0 --- /dev/null +++ b/benchmarks/ssr/README.md @@ -0,0 +1,37 @@ +# SSR Benchmarks + +Cross-framework SSR request-loop benchmarks for: + +- `@tanstack/react-start` +- `@tanstack/solid-start` +- `@tanstack/vue-start` + +Each benchmark builds a Start app with file-based routes and runs Vitest benches against the built server handler. + +## Layout + +- `react/` - React Start benchmark + Vitest config +- `solid/` - Solid Start benchmark + Vitest config +- `vue/` - Vue Start benchmark + Vitest config + +## 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 +``` + +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 +``` + +Typecheck benchmark sources: + +```bash +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/ssr:test:types --outputStyle=stream --skipRemoteCache +``` diff --git a/benchmarks/ssr/bench-utils.ts b/benchmarks/ssr/bench-utils.ts new file mode 100644 index 00000000000..14f8a56ddb5 --- /dev/null +++ b/benchmarks/ssr/bench-utils.ts @@ -0,0 +1,67 @@ +export interface StartRequestHandler { + fetch: (request: Request) => Promise | Response +} + +export interface RunSsrRequestLoopOptions { + seed: number + iterations?: number +} + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} + +function randomSearchValue(random: () => number) { + return `q-${randomSegment(random)}` +} + +function randomRequestUrl(random: () => number) { + const a = randomSegment(random) + const b = randomSegment(random) + const c = randomSegment(random) + const d = randomSegment(random) + const q = randomSearchValue(random) + + return `http://localhost/${a}/${b}/${c}/${d}?q=${q}` +} + +export async function runSsrRequestLoop( + handler: StartRequestHandler, + { seed, iterations = 10 }: RunSsrRequestLoopOptions, +) { + const random = createDeterministicRandom(seed) + const pendingBodyReads: Array> = [] + + for (let index = 0; index < iterations; index++) { + const requestUrl = randomRequestUrl(random) + const response = await handler.fetch(new Request(requestUrl, requestInit)) + + if (response.status !== 200) { + await Promise.allSettled(pendingBodyReads) + + throw new Error( + `Request failed with non-200 status ${response.status} (${requestUrl})`, + ) + } + + pendingBodyReads.push(response.text().then(() => undefined)) + } + + await Promise.all(pendingBodyReads) +} diff --git a/benchmarks/ssr/package.json b/benchmarks/ssr/package.json new file mode 100644 index 00000000000..5d8215ae699 --- /dev/null +++ b/benchmarks/ssr/package.json @@ -0,0 +1,94 @@ +{ + "name": "@benchmarks/ssr", + "private": true, + "type": "module", + "scripts": { + "build:react": "vite build --config ./react/vite.config.ts", + "build:solid": "vite build --config ./solid/vite.config.ts", + "build:vue": "vite build --config ./vue/vite.config.ts", + "test:perf": "vitest bench", + "test:perf:react": "vitest bench --config ./react/vite.config.ts ./react/speed.bench.ts", + "test:perf:solid": "vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.ts", + "test:perf:vue": "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:^", + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "@tanstack/vue-router": "workspace:^", + "@tanstack/vue-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "solid-js": "^1.9.10", + "vue": "^3.5.16" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.0.1", + "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-vue-jsx": "^4.1.2", + "typescript": "^5.7.2", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10", + "vitest": "^4.0.17" + }, + "nx": { + "targets": { + "build:react": { + "cache": false, + "dependsOn": [ + "^build" + ] + }, + "build:solid": { + "cache": false, + "dependsOn": [ + "^build" + ] + }, + "build:vue": { + "cache": false, + "dependsOn": [ + "^build" + ] + }, + "test:perf": { + "cache": false, + "dependsOn": [ + "^build", + "build:react", + "build:solid", + "build:vue" + ] + }, + "test:perf:react": { + "cache": false, + "dependsOn": [ + "build:react" + ] + }, + "test:perf:solid": { + "cache": false, + "dependsOn": [ + "build:solid" + ] + }, + "test:perf:vue": { + "cache": false, + "dependsOn": [ + "build:vue" + ] + }, + "test:types": { + "cache": false, + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/benchmarks/ssr/react/speed.bench.ts b/benchmarks/ssr/react/speed.bench.ts new file mode 100644 index 00000000000..89bfd54318a --- /dev/null +++ b/benchmarks/ssr/react/speed.bench.ts @@ -0,0 +1,50 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { runSsrRequestLoop } from '../bench-utils' +import type { StartRequestHandler } from '../bench-utils' + +const appModulePath = './dist/server/server.js' +const benchmarkSeed = 0xdecafbad + +const uninitializedHandler: StartRequestHandler = { + fetch: () => Promise.reject(new Error('Benchmark not initialized')), +} + +let handler = uninitializedHandler + +async function setup() { + const module = (await import(appModulePath)) as { + default: StartRequestHandler + } + + handler = module.default +} + +function teardown() { + handler = uninitializedHandler +} + +describe('ssr', () => { + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + beforeAll(setup) + afterAll(teardown) + + bench( + 'ssr request loop (react)', + () => runSsrRequestLoop(handler, { seed: benchmarkSeed }), + { + warmupIterations: 100, + time: 10_000, + setup, + teardown, + throws: true, + }, + ) +}) diff --git a/benchmarks/ssr/react/src/routeTree.gen.ts b/benchmarks/ssr/react/src/routeTree.gen.ts new file mode 100644 index 00000000000..9f110bba316 --- /dev/null +++ b/benchmarks/ssr/react/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* 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' +import { Route as ABCDRouteImport } from './routes/$a.$b.$c.$d' + +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) +const ABCDRoute = ABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => ABCRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + 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 + } + '/$a/$b/$c/$d': { + id: '/$a/$b/$c/$d' + path: '/$d' + fullPath: '/$a/$b/$c/$d' + preLoaderRoute: typeof ABCDRouteImport + parentRoute: typeof ABCRoute + } + } +} + +interface ABCRouteChildren { + ABCDRoute: typeof ABCDRoute +} + +const ABCRouteChildren: ABCRouteChildren = { + ABCDRoute: ABCDRoute, +} + +const ABCRouteWithChildren = ABCRoute._addFileChildren(ABCRouteChildren) + +interface ABRouteChildren { + ABCRoute: typeof ABCRouteWithChildren +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRouteWithChildren, +} + +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/react/src/router.tsx b/benchmarks/ssr/react/src/router.tsx new file mode 100644 index 00000000000..7c4eb0babe9 --- /dev/null +++ b/benchmarks/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/react/src/routes/$a.$b.$c.$d.tsx b/benchmarks/ssr/react/src/routes/$a.$b.$c.$d.tsx new file mode 100644 index 00000000000..056f1c308ee --- /dev/null +++ b/benchmarks/ssr/react/src/routes/$a.$b.$c.$d.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b/$c/$d')({ + component: LevelDComponent, +}) + +function LevelDComponent() { + return +} diff --git a/benchmarks/ssr/react/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/react/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..b73ede60f48 --- /dev/null +++ b/benchmarks/ssr/react/src/routes/$a.$b.$c.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b/$c')({ + component: LevelCComponent, +}) + +function LevelCComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/react/src/routes/$a.$b.tsx b/benchmarks/ssr/react/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..649b0f0a469 --- /dev/null +++ b/benchmarks/ssr/react/src/routes/$a.$b.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b')({ + component: LevelBComponent, +}) + +function LevelBComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/react/src/routes/$a.tsx b/benchmarks/ssr/react/src/routes/$a.tsx new file mode 100644 index 00000000000..cc2fa1483b9 --- /dev/null +++ b/benchmarks/ssr/react/src/routes/$a.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a')({ + component: LevelAComponent, +}) + +function LevelAComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/react/src/routes/__root.tsx b/benchmarks/ssr/react/src/routes/__root.tsx new file mode 100644 index 00000000000..1973ca20bb1 --- /dev/null +++ b/benchmarks/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/react/src/workload.tsx b/benchmarks/ssr/react/src/workload.tsx new file mode 100644 index 00000000000..2cc5c9f1aee --- /dev/null +++ b/benchmarks/ssr/react/src/workload.tsx @@ -0,0 +1,81 @@ +import { Link, useParams, useSearch } from '@tanstack/react-router' + +const probes = Array.from({ length: 10 }, (_, index) => index) + +function runSelectorWork(input: string, salt: number) { + let value = salt + + 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 +} + +function ParamsProbe({ salt }: { salt: number }) { + const params = useParams({ + strict: false, + select: (nextParams) => + runSelectorWork( + `${nextParams.a ?? ''}/${nextParams.b ?? ''}/${nextParams.c ?? ''}/${nextParams.d ?? ''}`, + salt, + ), + }) + + void params + + return null +} + +function SearchProbe({ salt }: { salt: number }) { + const search = useSearch({ + strict: false, + select: (nextSearch) => runSelectorWork(String(nextSearch.q ?? ''), salt), + }) + + void search + + return null +} + +function LinkProbe({ salt }: { salt: number }) { + const value = String((salt % 97) + 1) + + return ( + + Link + + ) +} + +export function RouteWorkload() { + return ( + <> + {probes.map((probe) => ( + + ))} + {probes.map((probe) => ( + + ))} + {probes.map((probe) => ( + + ))} + + ) +} diff --git a/benchmarks/ssr/react/tsconfig.json b/benchmarks/ssr/react/tsconfig.json new file mode 100644 index 00000000000..4de88f2eb45 --- /dev/null +++ b/benchmarks/ssr/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/react/vite.config.ts b/benchmarks/ssr/react/vite.config.ts new file mode 100644 index 00000000000..04f15c39592 --- /dev/null +++ b/benchmarks/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 (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/ssr/solid/speed.bench.ts b/benchmarks/ssr/solid/speed.bench.ts new file mode 100644 index 00000000000..05eb8e2ca17 --- /dev/null +++ b/benchmarks/ssr/solid/speed.bench.ts @@ -0,0 +1,50 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { runSsrRequestLoop } from '../bench-utils' +import type { StartRequestHandler } from '../bench-utils' + +const appModulePath = './dist/server/server.js' +const benchmarkSeed = 0xcafebabe + +const uninitializedHandler: StartRequestHandler = { + fetch: () => Promise.reject(new Error('Benchmark not initialized')), +} + +let handler = uninitializedHandler + +async function setup() { + const module = (await import(appModulePath)) as { + default: StartRequestHandler + } + + handler = module.default +} + +function teardown() { + handler = uninitializedHandler +} + +describe('ssr', () => { + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + beforeAll(setup) + afterAll(teardown) + + bench( + 'ssr request loop (solid)', + () => runSsrRequestLoop(handler, { seed: benchmarkSeed }), + { + warmupIterations: 100, + time: 10_000, + setup, + teardown, + throws: true, + }, + ) +}) diff --git a/benchmarks/ssr/solid/src/routeTree.gen.ts b/benchmarks/ssr/solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..27cbe594763 --- /dev/null +++ b/benchmarks/ssr/solid/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* 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' +import { Route as ABCDRouteImport } from './routes/$a.$b.$c.$d' + +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) +const ABCDRoute = ABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => ABCRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + 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 + } + '/$a/$b/$c/$d': { + id: '/$a/$b/$c/$d' + path: '/$d' + fullPath: '/$a/$b/$c/$d' + preLoaderRoute: typeof ABCDRouteImport + parentRoute: typeof ABCRoute + } + } +} + +interface ABCRouteChildren { + ABCDRoute: typeof ABCDRoute +} + +const ABCRouteChildren: ABCRouteChildren = { + ABCDRoute: ABCDRoute, +} + +const ABCRouteWithChildren = ABCRoute._addFileChildren(ABCRouteChildren) + +interface ABRouteChildren { + ABCRoute: typeof ABCRouteWithChildren +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRouteWithChildren, +} + +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/solid/src/router.tsx b/benchmarks/ssr/solid/src/router.tsx new file mode 100644 index 00000000000..038ec0ab5e9 --- /dev/null +++ b/benchmarks/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/solid/src/routes/$a.$b.$c.$d.tsx b/benchmarks/ssr/solid/src/routes/$a.$b.$c.$d.tsx new file mode 100644 index 00000000000..9bab038f374 --- /dev/null +++ b/benchmarks/ssr/solid/src/routes/$a.$b.$c.$d.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b/$c/$d')({ + component: LevelDComponent, +}) + +function LevelDComponent() { + return +} diff --git a/benchmarks/ssr/solid/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/solid/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..e10b9da6dfa --- /dev/null +++ b/benchmarks/ssr/solid/src/routes/$a.$b.$c.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b/$c')({ + component: LevelCComponent, +}) + +function LevelCComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/solid/src/routes/$a.$b.tsx b/benchmarks/ssr/solid/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..cd2d0e9754e --- /dev/null +++ b/benchmarks/ssr/solid/src/routes/$a.$b.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b')({ + component: LevelBComponent, +}) + +function LevelBComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/solid/src/routes/$a.tsx b/benchmarks/ssr/solid/src/routes/$a.tsx new file mode 100644 index 00000000000..a2f02008829 --- /dev/null +++ b/benchmarks/ssr/solid/src/routes/$a.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a')({ + component: LevelAComponent, +}) + +function LevelAComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/solid/src/routes/__root.tsx b/benchmarks/ssr/solid/src/routes/__root.tsx new file mode 100644 index 00000000000..15de858e78c --- /dev/null +++ b/benchmarks/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/solid/src/workload.tsx b/benchmarks/ssr/solid/src/workload.tsx new file mode 100644 index 00000000000..76fcae555a3 --- /dev/null +++ b/benchmarks/ssr/solid/src/workload.tsx @@ -0,0 +1,81 @@ +import { For, createEffect } from 'solid-js' +import { Link, useParams, useSearch } from '@tanstack/solid-router' + +const probes = Array.from({ length: 10 }, (_, index) => index) + +function runSelectorWork(input: string, salt: number) { + let value = salt + + 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 +} + +function ParamsProbe(props: { salt: number }) { + const params = useParams({ + strict: false, + select: (nextParams) => + runSelectorWork( + `${nextParams.a ?? ''}/${nextParams.b ?? ''}/${nextParams.c ?? ''}/${nextParams.d ?? ''}`, + props.salt, + ), + }) + + createEffect(() => { + void params() + }) + + return null +} + +function SearchProbe(props: { salt: number }) { + const search = useSearch({ + strict: false, + select: (nextSearch) => + runSelectorWork(String(nextSearch.q ?? ''), props.salt), + }) + + createEffect(() => { + void search() + }) + + return null +} + +function LinkProbe(props: { salt: number }) { + const value = String((props.salt % 97) + 1) + + return ( + + Link + + ) +} + +export function RouteWorkload() { + return ( + <> + {(probe) => } + {(probe) => } + {(probe) => } + + ) +} diff --git a/benchmarks/ssr/solid/tsconfig.json b/benchmarks/ssr/solid/tsconfig.json new file mode 100644 index 00000000000..6c87e995c77 --- /dev/null +++ b/benchmarks/ssr/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/solid/vite.config.ts b/benchmarks/ssr/solid/vite.config.ts new file mode 100644 index 00000000000..dc9cbf90e31 --- /dev/null +++ b/benchmarks/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 (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/ssr/tsconfig.json b/benchmarks/ssr/tsconfig.json new file mode 100644 index 00000000000..e3ed3ad08ae --- /dev/null +++ b/benchmarks/ssr/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": ["bench-utils.ts"] +} diff --git a/benchmarks/ssr/vitest.config.ts b/benchmarks/ssr/vitest.config.ts new file mode 100644 index 00000000000..14776452ed8 --- /dev/null +++ b/benchmarks/ssr/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: [ + './react/vite.config.ts', + './solid/vite.config.ts', + './vue/vite.config.ts', + ], + }, +}) diff --git a/benchmarks/ssr/vue/speed.bench.ts b/benchmarks/ssr/vue/speed.bench.ts new file mode 100644 index 00000000000..f996f900e8e --- /dev/null +++ b/benchmarks/ssr/vue/speed.bench.ts @@ -0,0 +1,50 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { runSsrRequestLoop } from '../bench-utils' +import type { StartRequestHandler } from '../bench-utils' + +const appModulePath = './dist/server/server.js' +const benchmarkSeed = 0xdeadbeef + +const uninitializedHandler: StartRequestHandler = { + fetch: () => Promise.reject(new Error('Benchmark not initialized')), +} + +let handler = uninitializedHandler + +async function setup() { + const module = (await import(appModulePath)) as { + default: StartRequestHandler + } + + handler = module.default +} + +function teardown() { + handler = uninitializedHandler +} + +describe('ssr', () => { + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + beforeAll(setup) + afterAll(teardown) + + bench( + 'ssr request loop (vue)', + () => runSsrRequestLoop(handler, { seed: benchmarkSeed }), + { + warmupIterations: 100, + time: 10_000, + setup, + teardown, + throws: true, + }, + ) +}) diff --git a/benchmarks/ssr/vue/src/routeTree.gen.ts b/benchmarks/ssr/vue/src/routeTree.gen.ts new file mode 100644 index 00000000000..a6dfb5853e7 --- /dev/null +++ b/benchmarks/ssr/vue/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* 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' +import { Route as ABCDRouteImport } from './routes/$a.$b.$c.$d' + +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) +const ABCDRoute = ABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => ABCRoute, +} as any) + +export interface FileRoutesByFullPath { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRoutesByTo { + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/$a': typeof ARouteWithChildren + '/$a/$b': typeof ABRouteWithChildren + '/$a/$b/$c': typeof ABCRouteWithChildren + '/$a/$b/$c/$d': typeof ABCDRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + fileRoutesByTo: FileRoutesByTo + to: '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + id: '__root__' | '/$a' | '/$a/$b' | '/$a/$b/$c' | '/$a/$b/$c/$d' + 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 + } + '/$a/$b/$c/$d': { + id: '/$a/$b/$c/$d' + path: '/$d' + fullPath: '/$a/$b/$c/$d' + preLoaderRoute: typeof ABCDRouteImport + parentRoute: typeof ABCRoute + } + } +} + +interface ABCRouteChildren { + ABCDRoute: typeof ABCDRoute +} + +const ABCRouteChildren: ABCRouteChildren = { + ABCDRoute: ABCDRoute, +} + +const ABCRouteWithChildren = ABCRoute._addFileChildren(ABCRouteChildren) + +interface ABRouteChildren { + ABCRoute: typeof ABCRouteWithChildren +} + +const ABRouteChildren: ABRouteChildren = { + ABCRoute: ABCRouteWithChildren, +} + +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/vue/src/router.tsx b/benchmarks/ssr/vue/src/router.tsx new file mode 100644 index 00000000000..4290e7cdd31 --- /dev/null +++ b/benchmarks/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/vue/src/routes/$a.$b.$c.$d.tsx b/benchmarks/ssr/vue/src/routes/$a.$b.$c.$d.tsx new file mode 100644 index 00000000000..36c28968409 --- /dev/null +++ b/benchmarks/ssr/vue/src/routes/$a.$b.$c.$d.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b/$c/$d')({ + component: LevelDComponent, +}) + +function LevelDComponent() { + return +} diff --git a/benchmarks/ssr/vue/src/routes/$a.$b.$c.tsx b/benchmarks/ssr/vue/src/routes/$a.$b.$c.tsx new file mode 100644 index 00000000000..32498769703 --- /dev/null +++ b/benchmarks/ssr/vue/src/routes/$a.$b.$c.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b/$c')({ + component: LevelCComponent, +}) + +function LevelCComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/vue/src/routes/$a.$b.tsx b/benchmarks/ssr/vue/src/routes/$a.$b.tsx new file mode 100644 index 00000000000..fe50f887360 --- /dev/null +++ b/benchmarks/ssr/vue/src/routes/$a.$b.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a/$b')({ + component: LevelBComponent, +}) + +function LevelBComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/vue/src/routes/$a.tsx b/benchmarks/ssr/vue/src/routes/$a.tsx new file mode 100644 index 00000000000..51310ce0639 --- /dev/null +++ b/benchmarks/ssr/vue/src/routes/$a.tsx @@ -0,0 +1,15 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { RouteWorkload } from '../workload' + +export const Route = createFileRoute('/$a')({ + component: LevelAComponent, +}) + +function LevelAComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/ssr/vue/src/routes/__root.tsx b/benchmarks/ssr/vue/src/routes/__root.tsx new file mode 100644 index 00000000000..4b035198230 --- /dev/null +++ b/benchmarks/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/vue/src/workload.tsx b/benchmarks/ssr/vue/src/workload.tsx new file mode 100644 index 00000000000..3266aff694f --- /dev/null +++ b/benchmarks/ssr/vue/src/workload.tsx @@ -0,0 +1,111 @@ +import * as Vue from 'vue' +import { Link, useParams, useSearch } from '@tanstack/vue-router' + +const probes = Array.from({ length: 10 }, (_, index) => index) + +function runSelectorWork(input: string, salt: number) { + let value = salt + + 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 +} + +const ParamsProbe = Vue.defineComponent({ + props: { + salt: { + type: Number, + required: true, + }, + }, + setup(props) { + const params = useParams({ + strict: false, + select: (nextParams) => + runSelectorWork( + `${nextParams.a ?? ''}/${nextParams.b ?? ''}/${nextParams.c ?? ''}/${nextParams.d ?? ''}`, + props.salt, + ), + }) + + return () => { + void params.value + return null + } + }, +}) + +const SearchProbe = Vue.defineComponent({ + props: { + salt: { + type: Number, + required: true, + }, + }, + setup(props) { + const search = useSearch({ + strict: false, + select: (nextSearch) => + runSelectorWork(String(nextSearch.q ?? ''), props.salt), + }) + + return () => { + void search.value + return null + } + }, +}) + +const LinkProbe = Vue.defineComponent({ + props: { + salt: { + type: Number, + required: true, + }, + }, + setup(props) { + const value = String((props.salt % 97) + 1) + + return () => ( + + Link + + ) + }, +}) + +export const RouteWorkload = Vue.defineComponent({ + setup() { + return () => ( + <> + {probes.map((probe) => ( + + ))} + {probes.map((probe) => ( + + ))} + {probes.map((probe) => ( + + ))} + + ) + }, +}) diff --git a/benchmarks/ssr/vue/tsconfig.json b/benchmarks/ssr/vue/tsconfig.json new file mode 100644 index 00000000000..18951642398 --- /dev/null +++ b/benchmarks/ssr/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/vue/vite.config.ts b/benchmarks/ssr/vue/vite.config.ts new file mode 100644 index 00000000000..312f43a7ae2 --- /dev/null +++ b/benchmarks/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 (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/package.json b/package.json index 95f796f58e2..9dcb695afaf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:e2e": "nx run-many --target=test:e2e", "benchmark:bundle-size": "pnpm nx run @benchmarks/bundle-size:build", "benchmark:client-nav": "pnpm nx run @benchmarks/client-nav:test:perf", + "benchmark:ssr": "pnpm 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 3c518f831fc..864d69d7ca4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -277,6 +277,61 @@ importers: specifier: ^4.0.17 version: 4.0.17(@types/node@25.0.9)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + benchmarks/ssr: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../packages/react-router + '@tanstack/react-start': + specifier: workspace:* + version: link:../../packages/react-start + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:* + version: link:../../packages/solid-start + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../packages/vue-router + '@tanstack/vue-start': + specifier: workspace:* + version: link:../../packages/vue-start + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + solid-js: + specifier: 1.9.10 + version: 1.9.10 + vue: + specifier: ^3.5.16 + version: 3.5.25(typescript@5.9.3) + devDependencies: + '@codspeed/vitest-plugin': + specifier: ^5.0.1 + version: 5.2.0(tinybench@2.9.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.1.2 + version: 4.2.0(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3)) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + vitest: + specifier: ^4.0.17 + version: 4.0.17(@types/node@25.0.9)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + e2e/e2e-utils: devDependencies: get-port-please: