diff --git a/benchmarks/client-nav/README.md b/benchmarks/client-nav/README.md index 44b58107e3..6179c7f0b8 100644 --- a/benchmarks/client-nav/README.md +++ b/benchmarks/client-nav/README.md @@ -1,16 +1,80 @@ # Client Navigation Benchmarks -Cross-framework client-side navigation benchmarks for: +Cross-framework client-side CPU benchmarks for: - `@tanstack/react-router` - `@tanstack/solid-router` - `@tanstack/vue-router` +The benchmarks run in jsdom against production builds of real apps, and are +tracked in CI by CodSpeed (simulation mode). + +> **Scope:** these benchmarks cover the standalone client router only. The +> client side of TanStack Start (hydration of a server-rendered document, +> streamed payload consumption, server-function calls from the client, ...) is +> not covered here; the server side of Start is covered by `benchmarks/ssr`. + ## Layout -- `react/` - React benchmark + Vitest config -- `solid/` - Solid benchmark + Vitest config -- `vue/` - Vue benchmark + Vitest config +- `react/`, `solid/`, `vue/` - baseline benchmark (mixed navigation loop) + 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/harness.ts` - shared scenario runner (mount, link-click steps, `onRendered` synchronization) +- `scenarios//shared.ts` - framework-agnostic scenario definition (workload data, step sequence, assertions, bench options) +- `scenarios///` - isolated scenario apps + +Scenario app layout: + +```text +scenarios/// + vite.config.ts + speed.bench.ts + speed.flame.ts + setup.ts + project.json + tsconfig.json + src/ + main.tsx + routeTree.gen.ts + routes/ +``` + +Scenario apps use file-based routing (`@tanstack/router-plugin`) with a +generated `routeTree.gen.ts`, like a regular user app. Each scenario uses one +app per framework instead of sharing routes in the baseline app. This keeps +route-tree size and router options 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 client-side responsibility so benchmark changes can +be attributed to a specific feature area. + +| Scenario | Client-side responsibility | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `react/`, `solid/`, `vue/` baseline apps | Mixed navigation loop: path params, search params, route context, and `useParams`/`useSearch`/`useLoaderData` selector subscriptions. | +| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | +| `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | +| `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | +| `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | +| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `useMatchRoute` probes (the `MatchRoute` component is avoided: vue-router's implementation leaks one subscription per render). | +| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), cached revisits (re-run once per lap by the `invalidate` step), `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | +| `mount` | Cold start: `createRouter` (route-tree processing) + first render + initial `router.load()` + unmount, with a fresh router per mount. The only scenario measuring router creation. | +| `nested-params` | Deep nesting (8 dynamic levels): per-level `params.parse`/`stringify`, `beforeLoad` context accumulation across matches, and per-level `useParams`/`useRouteContext` subscriptions. Param values include characters requiring percent-encoding (as do `route-tree-scale`'s), so segment encode/decode paths run on every navigation. | +| `preload` | Intent preloading from hover events, programmatic `router.preloadRoute`, deterministic preload cache behavior (`defaultPreloadStaleTime: 0`), and commit-time cache maintenance. | +| `rewrites` | Composed client-side location rewrites: router `basepath` plus a locale input/output rewrite pair, running on every href build and location parse (the client analog of the SSR `rewrites` scenario). | +| `route-tree-scale` | Route matching and link-target resolution on a wide (~40 route) tree mixing static, dynamic, prefixed-param, splat, pathless-layout, and route-group paths, with `autoCodeSplitting` enabled so navigations also resolve lazy route chunks. | +| `search-params` | `validateSearch` execution, search middlewares (`retainSearchParams`/`stripSearchParams`), functional search updaters, structural sharing, and `useSearch` selector subscriptions. | + +## Conventions + +- Apps are built with `NODE_ENV=production` (`minify: false`) into `dist/app.js`; benches import the built bundle, so production package builds and production JSX output are measured, not dev transforms. +- Scenarios behave like a real user app: navigation happens through `` clicks dispatched on real anchor elements (unless a scenario specifically measures the imperative API), the router uses the default browser history, and `scrollRestoration` is enabled. +- Each benchmark iteration advances a fixed, circular sequence of steps; every step awaits the router's `onRendered` event, so render work is included and steps cannot overlap. No two consecutive steps may target the same location, and the sequence ends back on the initial route. +- Setup runs one warm-up lap through the sequence and asserts each step's observable output (e.g. `document.title`), so a scenario that silently stops doing its work fails instead of reporting a fast time. +- Determinism: no wall-clock timers (async work is resolved promises or counted `setTimeout(0)` hops), `staleTime`/`gcTime` only ever `0` or effectively-infinite, no `Math.random`/`Date.now` in the measured loop. Scroll restoration uses `getScrollRestorationKey: (location) => location.pathname` so its cache stays bounded (the default random per-entry key grows one cache entry per push navigation). The `mount` scenario opts out of `scrollRestoration` entirely and destroys its history on unmount — both register page-lifetime globals that would leak across its mount/unmount loop. +- Push-only step laps grow jsdom's session-history entry list over a run (jsdom never truncates entries behind the current index). Per-push cost stays O(1) so timings are unaffected; only the `history` scenario needs — and has — a lap that is depth-stationary. +- Calibration: pick per-iteration step counts so a bench's `vitest bench` run stays roughly between 8 and 30 seconds (long enough to average out, short enough for CI). ## Run @@ -20,7 +84,7 @@ Run all benchmarks through Nx so dependency builds are part of the graph: CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf --outputStyle=stream --skipRemoteCache ``` -Run framework-specific benchmarks: +Run framework-specific benchmarks (baseline + all scenarios): ```bash CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:react --outputStyle=stream --skipRemoteCache @@ -28,15 +92,25 @@ CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:solid --output CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache ``` +Run a single scenario app manually (after building it through Nx): + +```bash +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav--:build:client --outputStyle=stream --skipRemoteCache +cd benchmarks/client-nav && NODE_ENV=production vitest bench --config ./scenarios///vite.config.ts +``` + Run framework-specific flame benchmarks (10 second loop, profiled with `@platformatic/flame`, forced to `NODE_ENV=production`): ```bash +# baseline CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:react --outputStyle=stream --skipRemoteCache CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:solid --outputStyle=stream --skipRemoteCache CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:vue --outputStyle=stream --skipRemoteCache +# scenarios +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav--:test:flame --outputStyle=stream --skipRemoteCache ``` -Typecheck benchmark sources: +Typecheck benchmark sources (baseline + scenarios): ```bash CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:types --outputStyle=stream --skipRemoteCache diff --git a/benchmarks/client-nav/jsdom.ts b/benchmarks/client-nav/jsdom.ts index 05535e693d..855c2a2316 100644 --- a/benchmarks/client-nav/jsdom.ts +++ b/benchmarks/client-nav/jsdom.ts @@ -30,6 +30,11 @@ setGlobal('MutationObserver', window.MutationObserver) setGlobal('sessionStorage', window.sessionStorage) setGlobal('localStorage', window.localStorage) setGlobal('getComputedStyle', window.getComputedStyle.bind(window)) +setGlobal('addEventListener', window.addEventListener.bind(window)) +setGlobal('removeEventListener', window.removeEventListener.bind(window)) +setGlobal('dispatchEvent', window.dispatchEvent.bind(window)) +setGlobal('CustomEvent', window.CustomEvent) +setGlobal('Event', window.Event) setGlobal( 'requestAnimationFrame', @@ -45,5 +50,6 @@ setGlobal( ) window.scrollTo = () => {} +setGlobal('scrollTo', () => {}) export { window } diff --git a/benchmarks/client-nav/package.json b/benchmarks/client-nav/package.json index cb86e70654..701e068b49 100644 --- a/benchmarks/client-nav/package.json +++ b/benchmarks/client-nav/package.json @@ -10,9 +10,9 @@ "test:flame:solid": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./solid/speed.flame.ts", "test:flame:vue": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./vue/speed.flame.ts", "test:perf": "NODE_ENV=production vitest bench --config ./vitest.config.ts", - "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:perf:react": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "test:perf:solid": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "test:perf:vue": "NODE_ENV=production vitest bench --config ./vitest.vue.config.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", @@ -31,6 +31,7 @@ "devDependencies": { "@platformatic/flame": "^1.6.0", "@codspeed/vitest-plugin": "^5.5.0", + "@tanstack/router-plugin": "workspace:^", "@testing-library/react": "^16.2.0", "@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-vue": "^6.0.5", @@ -51,6 +52,23 @@ "@tanstack/react-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-react", + "@benchmarks/client-nav-control-flow-react", + "@benchmarks/client-nav-head-react", + "@benchmarks/client-nav-history-react", + "@benchmarks/client-nav-links-react", + "@benchmarks/client-nav-loaders-react", + "@benchmarks/client-nav-mount-react", + "@benchmarks/client-nav-nested-params-react", + "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-rewrites-react", + "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-search-params-react" + ], + "target": "build:client" } ] }, @@ -62,6 +80,23 @@ "@tanstack/solid-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-solid", + "@benchmarks/client-nav-control-flow-solid", + "@benchmarks/client-nav-head-solid", + "@benchmarks/client-nav-history-solid", + "@benchmarks/client-nav-links-solid", + "@benchmarks/client-nav-loaders-solid", + "@benchmarks/client-nav-mount-solid", + "@benchmarks/client-nav-nested-params-solid", + "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-rewrites-solid", + "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-search-params-solid" + ], + "target": "build:client" } ] }, @@ -73,6 +108,23 @@ "@tanstack/vue-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-vue", + "@benchmarks/client-nav-control-flow-vue", + "@benchmarks/client-nav-head-vue", + "@benchmarks/client-nav-history-vue", + "@benchmarks/client-nav-links-vue", + "@benchmarks/client-nav-loaders-vue", + "@benchmarks/client-nav-mount-vue", + "@benchmarks/client-nav-nested-params-vue", + "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-rewrites-vue", + "@benchmarks/client-nav-route-tree-scale-vue", + "@benchmarks/client-nav-search-params-vue" + ], + "target": "build:client" } ] }, @@ -123,7 +175,48 @@ "test:types": { "cache": false, "dependsOn": [ - "^build" + "^build", + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-react", + "@benchmarks/client-nav-async-pipeline-solid", + "@benchmarks/client-nav-async-pipeline-vue", + "@benchmarks/client-nav-control-flow-react", + "@benchmarks/client-nav-control-flow-solid", + "@benchmarks/client-nav-control-flow-vue", + "@benchmarks/client-nav-head-react", + "@benchmarks/client-nav-head-solid", + "@benchmarks/client-nav-head-vue", + "@benchmarks/client-nav-history-react", + "@benchmarks/client-nav-history-solid", + "@benchmarks/client-nav-history-vue", + "@benchmarks/client-nav-links-react", + "@benchmarks/client-nav-links-solid", + "@benchmarks/client-nav-links-vue", + "@benchmarks/client-nav-loaders-react", + "@benchmarks/client-nav-loaders-solid", + "@benchmarks/client-nav-loaders-vue", + "@benchmarks/client-nav-mount-react", + "@benchmarks/client-nav-mount-solid", + "@benchmarks/client-nav-mount-vue", + "@benchmarks/client-nav-nested-params-react", + "@benchmarks/client-nav-nested-params-solid", + "@benchmarks/client-nav-nested-params-vue", + "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-rewrites-react", + "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-rewrites-solid", + "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-rewrites-vue", + "@benchmarks/client-nav-route-tree-scale-vue", + "@benchmarks/client-nav-search-params-react", + "@benchmarks/client-nav-search-params-solid", + "@benchmarks/client-nav-search-params-vue" + ], + "target": "test:types:client" + } ] } } diff --git a/benchmarks/client-nav/react/vite.config.ts b/benchmarks/client-nav/react/vite.config.ts index ffa1d41be8..adc31ddd64 100644 --- a/benchmarks/client-nav/react/vite.config.ts +++ b/benchmarks/client-nav/react/vite.config.ts @@ -1,8 +1,14 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -25,6 +31,6 @@ export default defineConfig({ name: '@benchmarks/client-nav (react)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], }, }) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/project.json b/benchmarks/client-nav/scenarios/async-pipeline/react/project.json new file mode 100644 index 0000000000..44ab55ccfe --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts new file mode 100644 index 0000000000..68c5f97c2f --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-async-pipeline navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx new file mode 100644 index 0000000000..4b141859e6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx @@ -0,0 +1,37 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..2b3336859a --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* 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 NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx new file mode 100644 index 0000000000..1cc8566678 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const ids = ['1', '2'] + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..d644376b7b --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) + +function CtxPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx new file mode 100644 index 0000000000..5b6d70e744 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..bc002c303f --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) + +function NestedPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx new file mode 100644 index 0000000000..443f592373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) + +function NestedLayout() { + const data = Route.useLoaderData() + + return ( +
+
{data.value}
+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..4839b0ca6d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) + +function SlowPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts new file mode 100644 index 0000000000..7161c9a13d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav async-pipeline (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/shared.ts b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts new file mode 100644 index 0000000000..02123309b2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts @@ -0,0 +1,113 @@ +/** + * Shared definition of the `async-pipeline` scenario: the router's async + * load pipeline — fully async loaders (transition-held navigations), async + * `beforeLoad` context building, and parallel nested async loaders. All async + * work resolves through counted 0ms timer hops so the workload is + * deterministic. The three framework apps consume these builders so the + * workload is identical modulo the rendering layer. + * + * Note: component-level deferred consumption (`Await`/Suspense) is + * intentionally NOT covered. React throttles every Suspense reveal (with or + * without a fallback, including `pendingComponent`) by ~300ms of wall-clock + * time, which cannot be made deterministic; the async work is exercised + * through the router's own pipeline instead. + */ +import type { ScenarioStep } from '../harness' + +/** + * Resolves after exactly `hops` chained macrotask turns. Uses `setImmediate` + * rather than `setTimeout(0)`: both are counted, deterministic event-loop + * turns, but timers cost ~3-4 syscalls per hop (timerfd + epoll) and CodSpeed + * excludes syscall time from the measure — inconsistently past a threshold — + * which destabilized the hop-heavy benches. + */ +export function hopDelay(hops: number): Promise { + let promise = Promise.resolve() + for (let i = 0; i < hops; i++) { + promise = promise.then( + () => new Promise((resolve) => setImmediate(() => resolve())), + ) + } + return promise +} + +/** Number of 0ms hops every async loader/beforeLoad takes before resolving. */ +export const deferredHops = 2 + +function checksum(seed: number) { + let value = seed | 0 + for (let index = 0; index < 20; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + return value +} + +export function slowStateValue(id: string) { + return `state-${id}-${checksum(Number(id) * 71 + 3)}` +} + +export function ctxSeedValue(id: string) { + return checksum(Number(id) * 13 + 1) +} + +export function ctxStateValue(id: string) { + return `ctx-${id}-${checksum(ctxSeedValue(id) + 5)}` +} + +export function nestedLayoutValue() { + return `layout-${checksum(97)}` +} + +export function nestedStateValue(id: string) { + return `nested-${id}-${checksum(Number(id) * 41 + 11)}` +} + +function markerText(testId: string) { + return ( + document.querySelector(`[data-testid="${testId}"]`)?.textContent ?? + undefined + ) +} + +function step(testId: string, isSettled: () => boolean): ScenarioStep { + return { type: 'click', testId, isSettled } +} + +export const steps: ReadonlyArray = [ + step('go-slow-1', () => markerText('slow-state') === slowStateValue('1')), + step('go-ctx-1', () => markerText('ctx-state') === ctxStateValue('1')), + step( + 'go-nested-1', + () => + markerText('nested-layout') === nestedLayoutValue() && + markerText('nested-state') === nestedStateValue('1'), + ), + step('go-slow-2', () => markerText('slow-state') === slowStateValue('2')), + step('go-ctx-2', () => markerText('ctx-state') === ctxStateValue('2')), + step( + 'go-nested-2', + () => markerText('nested-state') === nestedStateValue('2'), + ), + step('go-home', () => markerText('home-state') === 'home'), +] + +export function assertStepResult(stepIndex: number) { + const current = steps[stepIndex]! + if (typeof current === 'string' || !('isSettled' in current)) { + return + } + if (!current.isSettled!()) { + throw new Error( + `Unexpected document state after async-pipeline step ${stepIndex}`, + ) + } +} + +// Two laps through the 7-step sequence per benchmark iteration. +export const ticksPerIteration = 14 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json b/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json new file mode 100644 index 0000000000..2fce426b1e --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts new file mode 100644 index 0000000000..d1f620f447 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-async-pipeline navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx new file mode 100644 index 0000000000..92dcc74d0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx @@ -0,0 +1,34 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..7c15baf4e9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* 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 NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..7d3647fee8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx @@ -0,0 +1,46 @@ +import { For } from 'solid-js' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const ids = ['1', '2'] + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..e757b57b9d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) + +function CtxPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx new file mode 100644 index 0000000000..12907d440d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..5518ccf498 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) + +function NestedPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx new file mode 100644 index 0000000000..fdc939dc95 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) + +function NestedLayout() { + const data = Route.useLoaderData() + + return ( +
+
{data().value}
+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..4c229407e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) + +function SlowPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts new file mode 100644 index 0000000000..5107c1f056 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav async-pipeline (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json b/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json new file mode 100644 index 0000000000..911bb61e38 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts new file mode 100644 index 0000000000..eeb568323c --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-async-pipeline navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx new file mode 100644 index 0000000000..d8e659fb09 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx @@ -0,0 +1,41 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..ce26dc62d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* 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 NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..c477ab7e68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const ids = ['1', '2'] + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..d995f1ff6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx @@ -0,0 +1,32 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +const CtxPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx new file mode 100644 index 0000000000..128e132190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..bbeaaa7dd7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +const NestedPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx new file mode 100644 index 0000000000..763657dfc6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx @@ -0,0 +1,26 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +const NestedLayout = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () => ( +
+
{data.value.value}
+ +
+ ) + }, +}) + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..c9afccba97 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +const SlowPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts new file mode 100644 index 0000000000..e16a8ee828 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav async-pipeline (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/project.json b/benchmarks/client-nav/scenarios/control-flow/react/project.json new file mode 100644 index 0000000000..e3e580911f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/setup.ts b/benchmarks/client-nav/scenarios/control-flow/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts new file mode 100644 index 0000000000..55a2224d3a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-control-flow navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx new file mode 100644 index 0000000000..9a0c3d3b50 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx @@ -0,0 +1,37 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + // The scenario intentionally throws loader errors every lap; keep React 19's + // default per-caught-error console reporting out of the measured loop. + const reactRoot = createRoot(container, { onCaughtError: () => {} }) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..86edcc8472 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* 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 TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx new file mode 100644 index 0000000000..63d5feb901 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function LocationMarker() { + const pathname = useLocation({ select: (location) => location.pathname }) + return {pathname} +} + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx new file mode 100644 index 0000000000..2dc4a26486 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { errorMessage } from '../../../shared' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) + +function BrokenError(props: ErrorComponentProps) { + return
{props.error.message}
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx new file mode 100644 index 0000000000..aacba39384 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx new file mode 100644 index 0000000000..db31abe9f3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx new file mode 100644 index 0000000000..24e856ea15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..0c807bb2bc --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) + +function MissingPage() { + const data = Route.useLoaderData() + return
{data.id}
+} + +function MissingNotFound() { + return
missing not found
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx new file mode 100644 index 0000000000..280f65835e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) + +function TargetPage() { + return
target
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts new file mode 100644 index 0000000000..1aa2c1fa35 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav control-flow (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/shared.ts b/benchmarks/client-nav/scenarios/control-flow/shared.ts new file mode 100644 index 0000000000..c4f368bee1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/shared.ts @@ -0,0 +1,91 @@ +/** + * Shared definition of the `control-flow` scenario: loader-thrown redirects + * (including a 2-hop chain), notFound() rendering, loader errors through the + * route error boundary, and recovery navigation resetting the boundary. + */ +import type { ScenarioStep } from '../harness' + +export const errorMessage = 'bench-error' + +export const steps: ReadonlyArray = [ + { + type: 'click', + testId: 'go-hop1', + // The click lands on /hop1 whose loader chains redirects to /target; wait + // until the post-redirect location is committed before finishing the step. + isSettled: () => + document.querySelector('[data-testid="loc"]')?.textContent === '/target', + }, + 'go-missing-ok', + 'go-missing-gone', + 'go-broken', + 'go-target', + 'go-missing-gone', + 'home', +] + +interface StepExpectation { + loc: string + present: ReadonlyArray + absent?: ReadonlyArray + errorText?: string +} + +const expectations: ReadonlyArray = [ + { loc: '/target', present: ['target-state'] }, + { loc: '/missing/exists', present: ['missing-state'] }, + { + loc: '/missing/gone', + present: ['not-found-state'], + absent: ['missing-state'], + }, + { loc: '/broken', present: ['error-state'], errorText: errorMessage }, + { loc: '/target', present: ['target-state'], absent: ['error-state'] }, + { loc: '/missing/gone', present: ['not-found-state'] }, + { loc: '/', present: ['home-state'] }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expectation = expectations[stepIndex % expectations.length]! + + const loc = container.querySelector('[data-testid="loc"]') + if (loc?.textContent !== expectation.loc) { + throw new Error( + `Expected location "${expectation.loc}" after step ${stepIndex}, received "${loc?.textContent ?? ''}"`, + ) + } + + for (const testId of expectation.present) { + const element = container.querySelector(`[data-testid="${testId}"]`) + if (!element) { + throw new Error( + `Expected [data-testid="${testId}"] after step ${stepIndex}`, + ) + } + if ( + expectation.errorText && + !element.textContent?.includes(expectation.errorText) + ) { + throw new Error( + `Expected [data-testid="${testId}"] to include "${expectation.errorText}", received "${element.textContent}"`, + ) + } + } + + for (const testId of expectation.absent ?? []) { + if (container.querySelector(`[data-testid="${testId}"]`)) { + throw new Error( + `Expected [data-testid="${testId}"] to be absent after step ${stepIndex}`, + ) + } + } +} + +// Two laps through the 7-step sequence per benchmark iteration. +export const ticksPerIteration = 14 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/project.json b/benchmarks/client-nav/scenarios/control-flow/solid/project.json new file mode 100644 index 0000000000..2b5154d385 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts b/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts new file mode 100644 index 0000000000..abbb85a575 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-control-flow navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..44d88e9f8d --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* 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 TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..1bce9e8a3e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function LocationMarker() { + const pathname = useLocation({ select: (location) => location.pathname }) + return {pathname()} +} + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx new file mode 100644 index 0000000000..9931ef9c58 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { errorMessage } from '../../../shared' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) + +function BrokenError(props: ErrorComponentProps) { + return
{props.error.message}
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx new file mode 100644 index 0000000000..1aedb481ec --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx new file mode 100644 index 0000000000..91b601c516 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx new file mode 100644 index 0000000000..cc46764de5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..9c0493fb48 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) + +function MissingPage() { + const data = Route.useLoaderData() + return
{data().id}
+} + +function MissingNotFound() { + return
missing not found
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx new file mode 100644 index 0000000000..87de4e59a7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) + +function TargetPage() { + return
target
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts new file mode 100644 index 0000000000..1c6244d3d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav control-flow (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/project.json b/benchmarks/client-nav/scenarios/control-flow/vue/project.json new file mode 100644 index 0000000000..7370687988 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts b/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts new file mode 100644 index 0000000000..551b88e68f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-control-flow navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..60fc846f89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* 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 TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..9695c4b32d --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx @@ -0,0 +1,57 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/vue-router' + +const LocationMarker = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + return () => {pathname.value} + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx new file mode 100644 index 0000000000..c67fa2817e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { errorMessage } from '../../../shared' + +const BrokenError = Vue.defineComponent({ + props: { + error: { type: Error, required: true }, + }, + setup(props) { + return () =>
{props.error.message}
+ }, +}) + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx new file mode 100644 index 0000000000..ed00cefe3f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx new file mode 100644 index 0000000000..f6d4b1c1e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f3ffc58610 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..022ea656a6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx @@ -0,0 +1,28 @@ +import * as Vue from 'vue' +import { createFileRoute, notFound } from '@tanstack/vue-router' + +const MissingPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + return () =>
{data.value.id}
+ }, +}) + +const MissingNotFound = Vue.defineComponent({ + setup() { + return () =>
missing not found
+ }, +}) + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx new file mode 100644 index 0000000000..4e22d0993a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const TargetPage = Vue.defineComponent({ + setup() { + return () =>
target
+ }, +}) + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts new file mode 100644 index 0000000000..4694cf69c8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav control-flow (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts new file mode 100644 index 0000000000..7b993d0ed8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -0,0 +1,371 @@ +import { getRequiredLink, waitForRequiredLink } from '../setup-helpers' + +export interface ScenarioRouter { + load: () => Promise + subscribe: (eventType: 'onRendered', callback: () => void) => () => void + navigate: (options: any) => Promise + preloadRoute: (options: any) => Promise + invalidate: () => Promise + history: { + back: () => void + forward: () => void + go: (index: number) => void + } +} + +export interface MountedScenarioApp { + router: ScenarioRouter + unmount: () => void +} + +/** + * A single benchmark step. Plain strings are link-click steps (the common + * case). Steps that change the router location (`click`, `navigate`, `back`, + * `forward`, `go`) are awaited through the router's `onRendered` event; steps + * that do background work (`hover`, `preload`, `invalidate`) are awaited + * through a fixed number of 0ms timer hops so the loop stays deterministic. + */ +export type ScenarioStep = + | string + | { + type: 'click' + testId: string + /** + * Optional post-render settle predicate for steps that trigger work + * finishing after `onRendered` (e.g. deferred data resolving through a + * Suspense boundary). The step keeps taking 0ms timer hops until the + * predicate returns true (bounded, throws when exceeded). + */ + isSettled?: () => boolean + } + | { + /** + * Dispatches a single `mouseover` on the link, which triggers + * `preload: 'intent'` exactly once in every adapter: React synthesizes + * `mouseEnter` from `mouseover`, and Solid/Vue attach a native + * `mouseover` preload handler. (Solid/Vue also listen to `mouseenter` — + * dispatching both events would run the preload pipeline twice for them + * but only once for React.) Then settles via counted 0ms hops. + */ + type: 'hover' + testId: string + settleHops?: number + } + | { type: 'navigate'; getOptions: () => object } + | { type: 'preload'; getOptions: () => object; settleHops?: number } + | { type: 'invalidate'; settleHops?: number } + | { type: 'back' } + | { type: 'forward' } + | { type: 'go'; delta: number } + +/** + * One macrotask turn via `setImmediate` rather than `setTimeout(0)`: both + * yield deterministically to the event loop (timers, MessageChannel, React's + * Node scheduler), but a timer costs ~3-4 syscalls (timerfd + epoll) per hop + * while an immediate costs ~1. CodSpeed excludes syscall time from the + * measure — inconsistently past a threshold, which flagged the hop-heavy + * benches as "skipped" — so the loop must stay syscall-lean. + */ +const timerHop = () => + new Promise((resolve) => setImmediate(() => resolve())) + +async function settle(hops: number) { + for (let i = 0; i < hops; i++) { + await timerHop() + } +} + +const MAX_SETTLE_HOPS = 100 + +async function settleUntil(isSettled: () => boolean, label: string) { + for (let i = 0; i < MAX_SETTLE_HOPS; i++) { + if (isSettled()) { + return + } + await timerHop() + } + throw new Error( + `Step "${label}" did not settle within ${MAX_SETTLE_HOPS} timer hops`, + ) +} + +/** + * Reroute zero-delay `window.setTimeout` calls onto `setImmediate` for the + * duration of the benchmark. jsdom delivers history traversals through two + * nested `window.setTimeout(0)` tasks; Node clamps those to 1ms of wall + * time, so with nothing else pending the event loop blocks in `epoll_wait` + * until each timer expires — and that blocked wall-time is recorded as + * highly variable syscall time (5-26ms across runs), enough for CodSpeed to + * skip-warn the history benches. Zero-delay timeouts carry no ordering + * semantics a check-phase immediate doesn't satisfy, and the suite's + * conventions already forbid real-delay timers in measured code; non-zero + * delays are passed through untouched. + */ +function patchZeroDelayTimeouts() { + const originalSetTimeout = window.setTimeout.bind(window) + const originalClearTimeout = window.clearTimeout.bind(window) + const immediates = new Map() + // Negative ids cannot collide with real jsdom timer ids. + let nextImmediateId = -2 + + window.setTimeout = (( + handler: (...handlerArgs: Array) => void, + delay?: number, + ...args: Array + ) => { + if (!delay) { + const id = nextImmediateId-- + immediates.set( + id, + setImmediate(() => { + immediates.delete(id) + handler(...args) + }), + ) + return id + } + return originalSetTimeout(handler, delay, ...args) + }) as typeof window.setTimeout + + window.clearTimeout = ((id: number) => { + const immediate = immediates.get(id) + if (immediate) { + clearImmediate(immediate) + immediates.delete(id) + return + } + originalClearTimeout(id) + }) as typeof window.clearTimeout + + return () => { + window.setTimeout = originalSetTimeout as typeof window.setTimeout + window.clearTimeout = originalClearTimeout as typeof window.clearTimeout + } +} + +export interface ScenarioSetupOptions { + frameworkLabel: string + mount: (container: HTMLElement) => MountedScenarioApp + /** + * Circular sequence of steps advanced by `tick()`. Link steps must target + * links rendered from the start (e.g. in the root layout), no two + * consecutive location-changing steps may target the same location (a + * same-location click never triggers `onRendered`), and the sequence must + * leave the app back on the initial route so the warm-up lap ends in the + * starting state. + */ + steps: ReadonlyArray + /** Sanity check run once per step during the warm-up lap in `before()`. */ + assertAfterStep?: (stepIndex: number, container: HTMLElement) => void + /** + * URL the window is reset to before mounting (default '/'). Scenarios whose + * router uses a `basepath` or an input rewrite must start on an external + * URL that maps to their initial route. + */ + initialUrl?: string +} + +function warnAboutDevMode(frameworkLabel: string) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `client-nav scenario benchmark is running without NODE_ENV=production; ${frameworkLabel} dev overhead will dominate results.`, + ) + } +} + +/** + * Shared scenario runner: mounts the prebuilt app, then advances a circular + * sequence of steps, synchronizing each step so its work is fully part of the + * measured tick and steps cannot overlap. + */ +export function createScenarioSetup(options: ScenarioSetupOptions) { + warnAboutDevMode(options.frameworkLabel) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let restoreTimeouts = () => {} + let stepIndex = 0 + let next: () => Promise = () => + Promise.reject(new Error('Benchmark not initialized')) + + async function before() { + stepIndex = 0 + restoreTimeouts = patchZeroDelayTimeouts() + window.history.replaceState(null, '', options.initialUrl ?? '/') + container = document.createElement('div') + document.body.append(container) + + const { router, unmount: dispose } = options.mount(container) + unmount = dispose + + let resolveRendered: () => void = () => {} + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + const cachedLinks = new Map() + const rendered = () => + new Promise((resolveNext) => { + resolveRendered = resolveNext + }) + + const runStep = async (step: ScenarioStep) => { + if (typeof step === 'string') { + step = { type: 'click', testId: step } + } + + switch (step.type) { + case 'click': { + const renderedPromise = rendered() + getRequiredLink(container!, step.testId, cachedLinks).dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + button: 0, + }), + ) + await renderedPromise + if (step.isSettled) { + await settleUntil(step.isSettled, `click ${step.testId}`) + } + return + } + case 'hover': { + const link = getRequiredLink(container!, step.testId, cachedLinks) + link.dispatchEvent( + new MouseEvent('mouseover', { bubbles: true, cancelable: true }), + ) + await settle(step.settleHops ?? 4) + return + } + case 'navigate': { + const renderedPromise = rendered() + router.navigate(step.getOptions()) + await renderedPromise + return + } + case 'preload': { + await router.preloadRoute(step.getOptions()) + await settle(step.settleHops ?? 2) + return + } + case 'invalidate': { + await router.invalidate() + await settle(step.settleHops ?? 2) + return + } + case 'back': { + const renderedPromise = rendered() + router.history.back() + await renderedPromise + return + } + case 'forward': { + const renderedPromise = rendered() + router.history.forward() + await renderedPromise + return + } + case 'go': { + const renderedPromise = rendered() + router.history.go(step.delta) + await renderedPromise + return + } + } + } + + await router.load() + + const linkTestIds = new Set() + for (const step of options.steps) { + if (typeof step === 'string') { + linkTestIds.add(step) + } else if (step.type === 'click' || step.type === 'hover') { + linkTestIds.add(step.testId) + } + } + for (const testId of linkTestIds) { + await waitForRequiredLink(container, testId, cachedLinks) + } + + // One warm-up lap that also sanity-checks each step's observable output, + // ending back on the initial route so measurement starts from a known state. + for (const [index, step] of options.steps.entries()) { + await runStep(step) + options.assertAfterStep?.(index, container) + } + + next = () => { + const step = options.steps[stepIndex % options.steps.length]! + stepIndex += 1 + return runStep(step) + } + } + + function after() { + unmount?.() + container?.remove() + unsub() + restoreTimeouts() + restoreTimeouts = () => {} + unmount = undefined + container = undefined + } + + function tick() { + return next() + } + + return { + before, + tick, + after, + } +} + +export interface MountLoopSetupOptions { + frameworkLabel: string + mount: (container: HTMLElement) => MountedScenarioApp + /** Test id that must appear in the container before a mount counts as done. */ + readyTestId: string + assertReady?: (container: HTMLElement) => void +} + +/** + * Mount-loop runner for cold-start scenarios: every tick creates a fresh + * container, mounts the app (router creation included), waits for the initial + * render to commit, then unmounts and cleans up. + */ +export function createMountLoopSetup(options: MountLoopSetupOptions) { + warnAboutDevMode(options.frameworkLabel) + + async function tick() { + window.history.replaceState(null, '', '/') + const container = document.createElement('div') + document.body.append(container) + + const { router, unmount } = options.mount(container) + + try { + await router.load() + await settleUntil( + () => + container.querySelector(`[data-testid="${options.readyTestId}"]`) !== + null, + `mount ${options.readyTestId}`, + ) + options.assertReady?.(container) + } finally { + unmount() + container.remove() + } + } + + return { + before: async () => {}, + tick, + after: () => {}, + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/project.json b/benchmarks/client-nav/scenarios/head/react/project.json new file mode 100644 index 0000000000..1becf30fb9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/setup.ts b/benchmarks/client-nav/scenarios/head/react/setup.ts new file mode 100644 index 0000000000..1a28b0ca94 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/react/speed.bench.ts b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts new file mode 100644 index 0000000000..c71b426c56 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-head navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/react/speed.flame.ts b/benchmarks/client-nav/scenarios/head/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/main.tsx b/benchmarks/client-nav/scenarios/head/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..5c80a3104e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* 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 SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx new file mode 100644 index 0000000000..3d61dad0e9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx @@ -0,0 +1,59 @@ +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/react-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..6f5b78d60e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) + +function ArticlePage() { + const params = Route.useParams() + + return ( +
+

{articleTitle(params.id)}

+

{articleDescription(params.id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..6ed1935ada --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) + +function DocsSectionPage() { + const params = Route.useParams() + + return ( +
+

{docsTitle(params.section)}

+

{docsDescription(params.section)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx new file mode 100644 index 0000000000..a2da216ddb --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { docsLayoutHead } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) + +function DocsLayout() { + return ( +
+

Docs

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx new file mode 100644 index 0000000000..6ac318452f --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeHead, homeTitle } from '../../../shared' + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeTitle}

+

Landing page

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx new file mode 100644 index 0000000000..cd73902bff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { settingsHead, settingsTitle } from '../../../shared' + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) + +function SettingsPage() { + return ( +
+

{settingsTitle}

+

Account settings

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/tsconfig.json b/benchmarks/client-nav/scenarios/head/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/react/vite.config.ts b/benchmarks/client-nav/scenarios/head/react/vite.config.ts new file mode 100644 index 0000000000..086bd60bb4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav head (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/head/shared.ts b/benchmarks/client-nav/scenarios/head/shared.ts new file mode 100644 index 0000000000..db185d602b --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/shared.ts @@ -0,0 +1,152 @@ +/** + * Shared definition of the `head` scenario: route head() payloads, the click + * sequence, and per-step sanity assertions. The three framework apps consume + * these builders so the workload is identical modulo the rendering layer. + */ + +const siteName = 'Head Bench' + +export const docsSections = ['api', 'guide', 'cli'] as const +export const articleIds = ['1', '2', '3'] as const + +export const homeTitle = `Home | ${siteName}` +export const settingsTitle = `Settings | ${siteName}` + +export function docsTitle(section: string) { + return `Docs ${section} | ${siteName}` +} + +export function docsDescription(section: string) { + return `Documentation for the ${section} section of the head benchmark app.` +} + +export function articleTitle(id: string) { + return `Article ${id} | ${siteName}` +} + +export function articleDescription(id: string) { + return `Long-form article number ${id}, with a description sized like a realistic summary of the page content.` +} + +export function rootHead() { + return { + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: siteName }, + { name: 'description', content: 'Client head-management benchmark' }, + { name: 'theme-color', content: '#111827' }, + { property: 'og:type', content: 'website' }, + { property: 'og:site_name', content: siteName }, + ], + links: [ + { rel: 'icon', href: '/favicon.ico' }, + { rel: 'preconnect', href: 'https://assets.example.com' }, + ], + } +} + +export function homeHead() { + return { + meta: [ + { title: homeTitle }, + { name: 'description', content: 'Landing page of the head benchmark' }, + { property: 'og:title', content: homeTitle }, + ], + links: [{ rel: 'canonical', href: 'https://example.com/' }], + } +} + +export function docsLayoutHead() { + return { + meta: [ + // Overridden by the child section route on every docs navigation, so the + // dedupe path is exercised for both `name` and `property` entries. + { name: 'description', content: 'Documentation hub' }, + { property: 'og:title', content: `Docs | ${siteName}` }, + { name: 'docsearch:index', content: 'head-bench-docs' }, + ], + links: [{ rel: 'search', href: '/docs/opensearch.xml' }], + } +} + +export function docsSectionHead(section: string) { + return { + meta: [ + { title: docsTitle(section) }, + { name: 'description', content: docsDescription(section) }, + { name: 'keywords', content: `docs,${section},router,benchmark` }, + { property: 'og:title', content: docsTitle(section) }, + { property: 'og:description', content: docsDescription(section) }, + { property: 'og:url', content: `https://example.com/docs/${section}` }, + { name: 'twitter:card', content: 'summary' }, + { name: 'twitter:title', content: docsTitle(section) }, + ], + links: [ + { rel: 'canonical', href: `https://example.com/docs/${section}` }, + { rel: 'prev', href: `https://example.com/docs/${section}/intro` }, + ], + } +} + +export function articleHead(id: string) { + return { + meta: [ + { title: articleTitle(id) }, + { name: 'description', content: articleDescription(id) }, + { name: 'author', content: 'Bench Author' }, + { property: 'og:title', content: articleTitle(id) }, + { property: 'og:description', content: articleDescription(id) }, + { property: 'og:url', content: `https://example.com/articles/${id}` }, + { property: 'og:image', content: `https://example.com/covers/${id}.png` }, + { property: 'article:published_time', content: '2026-01-01T00:00:00Z' }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: articleTitle(id) }, + ], + links: [{ rel: 'canonical', href: `https://example.com/articles/${id}` }], + } +} + +export function settingsHead() { + return { + meta: [ + { title: settingsTitle }, + // Same names as root entries, so the child-wins dedupe path runs. + { name: 'description', content: 'Account settings' }, + { name: 'theme-color', content: '#f9fafb' }, + { name: 'robots', content: 'noindex' }, + ], + links: [], + } +} + +export const steps = [ + { testId: 'go-docs-api', title: docsTitle('api') }, + { testId: 'go-article-1', title: articleTitle('1') }, + { testId: 'go-docs-guide', title: docsTitle('guide') }, + { testId: 'go-settings', title: settingsTitle }, + { testId: 'go-article-2', title: articleTitle('2') }, + { testId: 'go-docs-cli', title: docsTitle('cli') }, + { testId: 'go-article-3', title: articleTitle('3') }, + { testId: 'go-home', title: homeTitle }, +] as const + +export const stepTestIds = steps.map((step) => step.testId) + +export function assertStepResult(stepIndex: number) { + const expected = steps[stepIndex]!.title + if (document.title !== expected) { + throw new Error( + `Expected document.title to be "${expected}" after step ${stepIndex}, received "${document.title}"`, + ) + } +} + +// Two laps through the 8-step sequence per benchmark iteration. +export const ticksPerIteration = 16 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/head/solid/project.json b/benchmarks/client-nav/scenarios/head/solid/project.json new file mode 100644 index 0000000000..2575a19b89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/solid/setup.ts b/benchmarks/client-nav/scenarios/head/solid/setup.ts new file mode 100644 index 0000000000..4ee924acff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts new file mode 100644 index 0000000000..ea0a5ca818 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-head navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/main.tsx b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..ead6a4436a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* 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 SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..52ca7da373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { For } from 'solid-js' +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/solid-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..dab11ada18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) + +function ArticlePage() { + const params = Route.useParams() + + return ( +
+

{articleTitle(params().id)}

+

{articleDescription(params().id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..7ab1123e7a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) + +function DocsSectionPage() { + const params = Route.useParams() + + return ( +
+

{docsTitle(params().section)}

+

{docsDescription(params().section)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx new file mode 100644 index 0000000000..7f84b214d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { docsLayoutHead } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) + +function DocsLayout() { + return ( +
+

Docs

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx new file mode 100644 index 0000000000..229eecc8c1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeHead, homeTitle } from '../../../shared' + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeTitle}

+

Landing page

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx new file mode 100644 index 0000000000..88424caef3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { settingsHead, settingsTitle } from '../../../shared' + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) + +function SettingsPage() { + return ( +
+

{settingsTitle}

+

Account settings

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/tsconfig.json b/benchmarks/client-nav/scenarios/head/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/solid/vite.config.ts b/benchmarks/client-nav/scenarios/head/solid/vite.config.ts new file mode 100644 index 0000000000..7b5a1e22b4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav head (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/project.json b/benchmarks/client-nav/scenarios/head/vue/project.json new file mode 100644 index 0000000000..5ad7909d74 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/vue/setup.ts b/benchmarks/client-nav/scenarios/head/vue/setup.ts new file mode 100644 index 0000000000..3dd6bc95a5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts new file mode 100644 index 0000000000..3630032e44 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-head navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/vue/src/main.tsx b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b115854c26 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* 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 SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..ad87699fef --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import * as Vue from 'vue' +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/vue-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + ) + }, +}) + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..43a6fade58 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +const ArticlePage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{articleTitle(params.value.id)}

+

{articleDescription(params.value.id)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..ebb59d61a2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +const DocsSectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{docsTitle(params.value.section)}

+

{docsDescription(params.value.section)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx new file mode 100644 index 0000000000..feabbc4d4a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { docsLayoutHead } from '../../../shared' + +const DocsLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Docs

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx new file mode 100644 index 0000000000..421478833f --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeHead, homeTitle } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeTitle}

+

Landing page

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx new file mode 100644 index 0000000000..1a519e16c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { settingsHead, settingsTitle } from '../../../shared' + +const SettingsPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{settingsTitle}

+

Account settings

+
+ ) + }, +}) + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/tsconfig.json b/benchmarks/client-nav/scenarios/head/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/vue/vite.config.ts b/benchmarks/client-nav/scenarios/head/vue/vite.config.ts new file mode 100644 index 0000000000..0ad2067925 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav head (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/react/project.json b/benchmarks/client-nav/scenarios/history/react/project.json new file mode 100644 index 0000000000..a2fe0001be --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/react/setup.ts b/benchmarks/client-nav/scenarios/history/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/react/speed.bench.ts b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts new file mode 100644 index 0000000000..df2c951c03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-history navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/react/speed.flame.ts b/benchmarks/client-nav/scenarios/history/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/main.tsx b/benchmarks/client-nav/scenarios/history/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..61cc83c4c8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx new file mode 100644 index 0000000000..240d8719d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/react-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx new file mode 100644 index 0000000000..55094556a6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) + +function GalleryPage() { + return ( +
+

Gallery

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx new file mode 100644 index 0000000000..932a72f2ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..b9c5e2f8d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { pageLabel } from '../../../shared' + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) + +function PageComponent() { + const params = Route.useParams() + + return ( +
+

{pageLabel(params.n)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..793b531c89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { photoLabel } from '../../../shared' + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) + +function PhotoComponent() { + const params = Route.useParams() + + return ( +
+

{photoLabel(params.photoId)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/tsconfig.json b/benchmarks/client-nav/scenarios/history/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/react/vite.config.ts b/benchmarks/client-nav/scenarios/history/react/vite.config.ts new file mode 100644 index 0000000000..61a1b2feef --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav history (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/shared.ts b/benchmarks/client-nav/scenarios/history/shared.ts new file mode 100644 index 0000000000..9ddfa6c104 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/shared.ts @@ -0,0 +1,89 @@ +/** + * Shared definition of the `history` scenario: push/replace/back/forward + * traversal, location masking, blocker registration overhead (the blocker + * never blocks), and useLocation/useCanGoBack subscriptions. + * + * The step lap is designed to keep the history depth stationary: it ends back + * on the initial entry, so the first push of the next lap truncates the stale + * forward tail and the session history never grows past 4 entries. + */ +import type { ScenarioStep } from '../harness' + +export const pageIds = ['1', '2', '3'] as const +export const maskedPhotoId = '42' + +export function pageLabel(n: string) { + return `Page ${n}` +} + +export function photoLabel(id: string) { + return `Photo ${id}` +} + +/** Registered on every navigation, never blocks. */ +export const shouldBlockFn = () => false + +interface HistoryStepExpectation { + step: ScenarioStep + /** Expected router location pathname (the REAL location, even when masked). */ + pathname: string + /** Expected window.location pathname when it differs (location masking). */ + windowPathname?: string + /** Marker test id that must be present in the container after the step. */ + marker?: string +} + +export const stepExpectations: ReadonlyArray = [ + { step: 'p-1', pathname: '/pages/1', marker: 'page-state' }, + { step: 'p-2', pathname: '/pages/2', marker: 'page-state' }, + { + step: 'photo-masked', + pathname: `/photos/${maskedPhotoId}`, + windowPathname: '/gallery', + marker: 'photo-state', + }, + { step: { type: 'back' }, pathname: '/pages/2', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/pages/1', marker: 'page-state' }, + { step: { type: 'forward' }, pathname: '/pages/2', marker: 'page-state' }, + { step: 'p-3-replace', pathname: '/pages/3', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/pages/1', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/', marker: 'home-state' }, +] + +export const steps = stepExpectations.map((expectation) => expectation.step) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = stepExpectations[stepIndex]! + + const loc = container.querySelector('[data-testid="loc"]')?.textContent + if (loc !== expected.pathname) { + throw new Error( + `Expected location marker to be "${expected.pathname}" after step ${stepIndex}, received "${loc}"`, + ) + } + + const windowPathname = expected.windowPathname ?? expected.pathname + if (window.location.pathname !== windowPathname) { + throw new Error( + `Expected window.location.pathname to be "${windowPathname}" after step ${stepIndex}, received "${window.location.pathname}"`, + ) + } + + if ( + expected.marker && + !container.querySelector(`[data-testid="${expected.marker}"]`) + ) { + throw new Error( + `Expected marker "${expected.marker}" to be rendered after step ${stepIndex}`, + ) + } +} + +// Two laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/history/solid/project.json b/benchmarks/client-nav/scenarios/history/solid/project.json new file mode 100644 index 0000000000..6f4bba1ca5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/solid/setup.ts b/benchmarks/client-nav/scenarios/history/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts new file mode 100644 index 0000000000..8e814bc133 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-history navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/main.tsx b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a6ab2a4fc8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..edf7368929 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/solid-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx new file mode 100644 index 0000000000..b72d1c6195 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) + +function GalleryPage() { + return ( +
+

Gallery

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx new file mode 100644 index 0000000000..44a0b8045a --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..1def6674c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { pageLabel } from '../../../shared' + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) + +function PageComponent() { + const params = Route.useParams() + + return ( +
+

{pageLabel(params().n)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..ab2fe06e3f --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { photoLabel } from '../../../shared' + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) + +function PhotoComponent() { + const params = Route.useParams() + + return ( +
+

{photoLabel(params().photoId)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/tsconfig.json b/benchmarks/client-nav/scenarios/history/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/solid/vite.config.ts b/benchmarks/client-nav/scenarios/history/solid/vite.config.ts new file mode 100644 index 0000000000..de0eda543c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav history (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/project.json b/benchmarks/client-nav/scenarios/history/vue/project.json new file mode 100644 index 0000000000..17fca887ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/vue/setup.ts b/benchmarks/client-nav/scenarios/history/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts new file mode 100644 index 0000000000..be2956bd85 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-history navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/vue/src/main.tsx b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..cee1b68870 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..7f7dff159c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/vue-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx new file mode 100644 index 0000000000..3a397b1843 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const GalleryPage = Vue.defineComponent({ + setup() { + return () => ( +
+

Gallery

+
+ ) + }, +}) + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx new file mode 100644 index 0000000000..fbc5b957a7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Home

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..560e83ed29 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { pageLabel } from '../../../shared' + +const PageComponent = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{pageLabel(params.value.n)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..186566fe72 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { photoLabel } from '../../../shared' + +const PhotoComponent = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{photoLabel(params.value.photoId)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/tsconfig.json b/benchmarks/client-nav/scenarios/history/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/vue/vite.config.ts b/benchmarks/client-nav/scenarios/history/vue/vite.config.ts new file mode 100644 index 0000000000..58986d6526 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav history (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/react/project.json b/benchmarks/client-nav/scenarios/links/react/project.json new file mode 100644 index 0000000000..52c9df8a68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/react/setup.ts b/benchmarks/client-nav/scenarios/links/react/setup.ts new file mode 100644 index 0000000000..5409df903c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/react/speed.bench.ts b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts new file mode 100644 index 0000000000..68bb869182 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-links navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/react/speed.flame.ts b/benchmarks/client-nav/scenarios/links/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/main.tsx b/benchmarks/client-nav/scenarios/links/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..97ec042c1c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx new file mode 100644 index 0000000000..7a48510e8a --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx @@ -0,0 +1,104 @@ +import { + Link, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/react-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function StatusPanel() { + const matchRoute = useMatchRoute() + + return ( + + ) +} + +function LinkGrid() { + return ( +
+ {itemIds.map((id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }) => (isActive ? `Item ${id} on` : `Item ${id} off`)} + +
+ ))} +
+ ) +} + +function RootComponent() { + return ( + <> + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx new file mode 100644 index 0000000000..0e4afc9297 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { aboutMarker } from '../../../shared' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( +
+

{aboutMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx new file mode 100644 index 0000000000..2b8c83ebab --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..23121edb5c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { itemMarker } from '../../../shared' + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemMarker(params.id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/tsconfig.json b/benchmarks/client-nav/scenarios/links/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/react/vite.config.ts b/benchmarks/client-nav/scenarios/links/react/vite.config.ts new file mode 100644 index 0000000000..9e9cd48895 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav links (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/shared.ts b/benchmarks/client-nav/scenarios/links/shared.ts new file mode 100644 index 0000000000..6e75ea045c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/shared.ts @@ -0,0 +1,133 @@ +/** + * Shared definition of the `links` scenario: a large persistent grid of + * ``s in the root layout so every navigation recomputes link props and + * active state across many activeOptions variants, plus `useMatchRoute` / + * `MatchRoute` consumers. The three framework apps consume these constants so + * the workload is identical modulo the rendering layer. + */ +import type { ScenarioStep } from '../harness' + +// 40 item ids x 5 link variants = 200 links mounted in the root layout. +export const LINK_ITEM_COUNT = 40 +export const LINK_VARIANT_COUNT = 5 + +export const itemIds = Array.from({ length: LINK_ITEM_COUNT }, (_, index) => + String(index + 1), +) + +// Static search object attached to the includeSearch:false variant. +export const variantSearch = { tab: 'specs', page: 2 } + +// Item ids that get a `go-item-` test id on their plain variant link. +export const stepItemIds = ['1', '7', '13', '25', '40'] + +// Ids probed by the useMatchRoute checks in the root StatusPanel. +export const matchProbeIds = [ + '1', + '5', + '7', + '13', + '20', + '25', + '30', + '35', + '40', + '2', +] + +export function itemMarker(id: string) { + return `item-${id}` +} + +export const homeMarker = 'home' +export const aboutMarker = 'about' + +interface StepDef { + testId: string + marker: string + /** Expected number of `.active-link` elements after the step. */ + activeCount: number + /** When one link is active, its href must include this substring. */ + activeHrefPart?: string +} + +export const stepDefs: ReadonlyArray = [ + { + testId: 'go-item-1', + marker: itemMarker('1'), + activeCount: 1, + activeHrefPart: '/items/1', + }, + { + testId: 'go-item-7', + marker: itemMarker('7'), + activeCount: 1, + activeHrefPart: '/items/7', + }, + { testId: 'go-about', marker: aboutMarker, activeCount: 0 }, + { + testId: 'go-item-13', + marker: itemMarker('13'), + activeCount: 1, + activeHrefPart: '/items/13', + }, + { + testId: 'go-item-25', + marker: itemMarker('25'), + activeCount: 1, + activeHrefPart: '/items/25', + }, + { + testId: 'go-item-40', + marker: itemMarker('40'), + activeCount: 1, + activeHrefPart: '/items/40', + }, + { + testId: 'go-item-7', + marker: itemMarker('7'), + activeCount: 1, + activeHrefPart: '/items/7', + }, + { testId: 'go-home', marker: homeMarker, activeCount: 0 }, +] + +export const scenarioSteps: ReadonlyArray = stepDefs.map( + (step) => step.testId, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const step = stepDefs[stepIndex]! + + const marker = container.querySelector('[data-testid="page-state"]') + if (marker?.textContent !== step.marker) { + throw new Error( + `Expected page marker "${step.marker}" after step ${stepIndex}, received "${marker?.textContent}"`, + ) + } + + const activeLinks = container.querySelectorAll('a.active-link') + if (activeLinks.length !== step.activeCount) { + throw new Error( + `Expected ${step.activeCount} active link(s) after step ${stepIndex}, received ${activeLinks.length}`, + ) + } + if (step.activeHrefPart) { + const href = activeLinks[0]!.getAttribute('href') ?? '' + if (!href.includes(step.activeHrefPart)) { + throw new Error( + `Expected the active link href to include "${step.activeHrefPart}" after step ${stepIndex}, received "${href}"`, + ) + } + } +} + +// One lap through the 8-step sequence per benchmark iteration; each step +// recomputes all 200 links plus the match probes. +export const ticksPerIteration = 8 + +export const benchOptions = { + warmupIterations: 50, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/links/solid/project.json b/benchmarks/client-nav/scenarios/links/solid/project.json new file mode 100644 index 0000000000..ef1dab4c50 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/solid/setup.ts b/benchmarks/client-nav/scenarios/links/solid/setup.ts new file mode 100644 index 0000000000..34270fe8a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts new file mode 100644 index 0000000000..beba86fb6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-links navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/main.tsx b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..b61431e40d --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a889a26854 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx @@ -0,0 +1,119 @@ +import { For } from 'solid-js' +import { + Link, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/solid-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function StatusPanel() { + const matchRoute = useMatchRoute() + + return ( + + ) +} + +function LinkGrid() { + return ( +
+ + {(id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }) => + isActive ? `Item ${id} on` : `Item ${id} off` + } + +
+ )} +
+
+ ) +} + +function RootComponent() { + return ( + <> + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx new file mode 100644 index 0000000000..a2a71678b7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { aboutMarker } from '../../../shared' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( +
+

{aboutMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx new file mode 100644 index 0000000000..72fbfe387a --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..64d84f81f4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { itemMarker } from '../../../shared' + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemMarker(params().id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/tsconfig.json b/benchmarks/client-nav/scenarios/links/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/solid/vite.config.ts b/benchmarks/client-nav/scenarios/links/solid/vite.config.ts new file mode 100644 index 0000000000..9a5b3d138b --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav links (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/project.json b/benchmarks/client-nav/scenarios/links/vue/project.json new file mode 100644 index 0000000000..fe1e68fad3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/vue/setup.ts b/benchmarks/client-nav/scenarios/links/vue/setup.ts new file mode 100644 index 0000000000..f70a154909 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts new file mode 100644 index 0000000000..54b99d1273 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-links navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/vue/src/main.tsx b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..cee64be43c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..04f99677fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx @@ -0,0 +1,120 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/vue-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +const StatusPanel = Vue.defineComponent({ + setup() { + const matchRoute = useMatchRoute() + const probes = matchProbeIds.map((id) => ({ + id, + match: matchRoute({ to: '/items/$id', params: { id } }), + })) + // Static probes use the setup-scoped matchRoute instead of : + // vue-router's MatchRoute creates an undisposed watcher per render (one + // leaked subscription per navigation), so all three apps use + // useMatchRoute for cross-framework parity. + const atHome = matchRoute({ to: '/' }) + const atAbout = matchRoute({ to: '/about' }) + const underRoot = matchRoute({ to: '/', fuzzy: true }) + const underAbout = matchRoute({ to: '/about', fuzzy: true }) + const aboutSearch = matchRoute({ to: '/about', includeSearch: true }) + + return () => ( + + ) + }, +}) + +const LinkGrid = Vue.defineComponent({ + setup() { + return () => ( +
+ {itemIds.map((id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }: { isActive: boolean }) => + isActive ? `Item ${id} on` : `Item ${id} off` + } + +
+ ))} +
+ ) + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx new file mode 100644 index 0000000000..5cc1473bff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { aboutMarker } from '../../../shared' + +const AboutPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{aboutMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f04a9f5796 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeMarker } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..0dc5efa3d3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { itemMarker } from '../../../shared' + +const ItemPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{itemMarker(params.value.id)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/tsconfig.json b/benchmarks/client-nav/scenarios/links/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/vue/vite.config.ts b/benchmarks/client-nav/scenarios/links/vue/vite.config.ts new file mode 100644 index 0000000000..d1cf9f561d --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav links (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/react/project.json b/benchmarks/client-nav/scenarios/loaders/react/project.json new file mode 100644 index 0000000000..d3dbe585d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/setup.ts b/benchmarks/client-nav/scenarios/loaders/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts new file mode 100644 index 0000000000..7c44f1a0f7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-loaders navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..50f0a8863b --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx new file mode 100644 index 0000000000..6309763cf3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx @@ -0,0 +1,70 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..9b954185cc --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + void computeChecksum(value) + return null +} + +function CachedPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Cached

+
{`c-${params.id}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx new file mode 100644 index 0000000000..92afe04a21 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx @@ -0,0 +1,65 @@ +import { createFileRoute } from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function DepsSubscriber() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + void computeChecksum(value) + return null +} + +function DepsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Deps

+
{`d-${search.page}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..fb7cbf2f00 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + void computeChecksum(value) + return null +} + +function FreshPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Fresh

+
{`f-${params.id}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx new file mode 100644 index 0000000000..9203c00841 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Loaders Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts new file mode 100644 index 0000000000..6fa28bcd35 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav loaders (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/shared.ts b/benchmarks/client-nav/scenarios/loaders/shared.ts new file mode 100644 index 0000000000..f744246160 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/shared.ts @@ -0,0 +1,134 @@ +/** + * Shared definition of the `loaders` scenario: deterministic loader data + * derivation, the click sequence, and per-step sanity assertions. The three + * framework apps consume these builders so the workload is identical modulo + * the rendering layer. + * + * Isolates: client loader dispatch — always-stale re-runs (staleTime 0), + * pure cache hits (staleTime 1e9), loaderDeps-keyed caching, + * router.invalidate(), and useLoaderData selector subscriptions. + */ + +import type { ScenarioStep } from '../harness' + +export function computeChecksum(seed: number) { + let value = Math.trunc(seed) | 0 + + for (let index = 0; index < 40; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + + return value +} + +export interface LoaderItem { + id: string + name: string + value: number + flag: boolean +} + +export function makeItems(seedText: string): Array { + let seed = 0 + for (let index = 0; index < seedText.length; index++) { + seed = (seed * 31 + seedText.charCodeAt(index)) >>> 0 + } + + const items: Array = [] + let value = seed + + for (let index = 0; index < 50; index++) { + value = (value * 1664525 + 1013904223) >>> 0 + items.push({ + id: `${seedText}-${index}`, + name: `Item ${index} of ${seedText}`, + value: value % 100000, + flag: value % 3 === 0, + }) + } + + return items +} + +export function itemsChecksum(items: Array) { + let sum = 0 + for (const item of items) { + sum = (sum + item.value + (item.flag ? 1 : 0)) >>> 0 + } + return sum +} + +export function normalizePage(value: unknown) { + const page = Number(value) + return Number.isFinite(page) && page > 0 ? Math.trunc(page) : 1 +} + +export function freshMarkerText(id: string) { + return `f-${id}-${itemsChecksum(makeItems(`fresh-${id}`))}` +} + +export function cachedMarkerText(id: string) { + return `c-${id}-${itemsChecksum(makeItems(`cached-${id}`))}` +} + +export function depsMarkerText(page: number) { + return `d-${page}-${itemsChecksum(makeItems(`deps-${page}`))}` +} + +export const steps: ReadonlyArray = [ + 'go-fresh-1', + 'go-fresh-2', + 'go-cached-1', + 'go-cached-2', + 'go-deps-1', + 'go-deps-2', + // Invalidates the currently matched routes (the deps route), re-running its + // loader with unchanged deps. Note: this also marks the cached fresh/cached + // matches invalid, so each of those loaders re-runs once per lap on its next + // visit — deterministic, identical work every lap. + { type: 'invalidate' }, + 'go-home', +] + +interface ExpectedState { + testId: string + text?: string +} + +const expectedStates: ReadonlyArray = [ + { testId: 'fresh-state', text: freshMarkerText('1') }, + { testId: 'fresh-state', text: freshMarkerText('2') }, + { testId: 'cached-state', text: cachedMarkerText('1') }, + { testId: 'cached-state', text: cachedMarkerText('2') }, + { testId: 'deps-state', text: depsMarkerText(1) }, + { testId: 'deps-state', text: depsMarkerText(2) }, + // After invalidate the deps loader re-ran with the same deps. + { testId: 'deps-state', text: depsMarkerText(2) }, + { testId: 'home-state' }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = expectedStates[stepIndex]! + const element = container.querySelector(`[data-testid="${expected.testId}"]`) + + if (!element) { + throw new Error( + `Expected marker "${expected.testId}" to exist after step ${stepIndex}`, + ) + } + + if (expected.text !== undefined && element.textContent !== expected.text) { + throw new Error( + `Expected marker "${expected.testId}" to read "${expected.text}" after step ${stepIndex}, received "${element.textContent}"`, + ) + } +} + +// Two laps through the 8-step sequence per benchmark iteration. +export const ticksPerIteration = 16 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/project.json b/benchmarks/client-nav/scenarios/loaders/solid/project.json new file mode 100644 index 0000000000..071e6bc385 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/setup.ts b/benchmarks/client-nav/scenarios/loaders/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts new file mode 100644 index 0000000000..4c97823d11 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-loaders navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..bdec400237 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..30350f7907 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..aab6be74dc --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx @@ -0,0 +1,62 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return computeChecksum(value())} /> +} + +function CachedPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Cached

+
{`c-${params().id}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx new file mode 100644 index 0000000000..2f23f2d3da --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx @@ -0,0 +1,72 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function DepsSubscriber() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + return computeChecksum(value())} /> +} + +function DepsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Deps

+
{`d-${search().page}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..acd6a9aa27 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx @@ -0,0 +1,62 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return computeChecksum(value())} /> +} + +function FreshPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Fresh

+
{`f-${params().id}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx new file mode 100644 index 0000000000..7287d47d0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Loaders Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts new file mode 100644 index 0000000000..ea913ac578 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav loaders (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/project.json b/benchmarks/client-nav/scenarios/loaders/vue/project.json new file mode 100644 index 0000000000..69bd95837d --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/setup.ts b/benchmarks/client-nav/scenarios/loaders/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts new file mode 100644 index 0000000000..eeb9cefaeb --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-loaders navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..25a82df477 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* 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 DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..b9102a92c4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..a426e35765 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx @@ -0,0 +1,68 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FirstItemSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const CachedPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Cached

+
+ {`c-${params.value.id}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx new file mode 100644 index 0000000000..f987d15d34 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx @@ -0,0 +1,78 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const DepsSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const DepsPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Deps

+
+ {`d-${search.value.page}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..64eec85cfa --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx @@ -0,0 +1,68 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FirstItemSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FreshPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Fresh

+
+ {`f-${params.value.id}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx new file mode 100644 index 0000000000..c9724c90a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Loaders Bench

+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts new file mode 100644 index 0000000000..350fadf387 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav loaders (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/react/project.json b/benchmarks/client-nav/scenarios/mount/react/project.json new file mode 100644 index 0000000000..61839943b3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/react/setup.ts b/benchmarks/client-nav/scenarios/mount/react/setup.ts new file mode 100644 index 0000000000..ed0426c696 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts new file mode 100644 index 0000000000..8f7967c42c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-mount loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/main.tsx b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx new file mode 100644 index 0000000000..78fe2082aa --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx @@ -0,0 +1,47 @@ +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/react-router' + +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { + return createRouter({ + routeTree, + history, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const history = createBrowserHistory() + const router = createTestRouter(history) + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + history.destroy() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..a796abe0ec --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* 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 ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx new file mode 100644 index 0000000000..0deab171d8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx @@ -0,0 +1,41 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx new file mode 100644 index 0000000000..3fedde283b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..f995d95d94 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' +import { articleData } from '../../../shared' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) + +function ArticlePage() { + const article = Route.useLoaderData() + + return
{`${article.title} (${article.words} words)`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx new file mode 100644 index 0000000000..437187eb4f --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) + +function BlogIndexPage() { + return

All articles

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx new file mode 100644 index 0000000000..ab4fbeb482 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { blogContext } from '../../../shared' + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) + +function BlogLayout() { + return ( +
+

Blog

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx new file mode 100644 index 0000000000..dbc99d8f66 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeItems, readyTestId } from '../../../shared' + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) + +function HomePage() { + const items = Route.useLoaderData() + + return ( +
+

{`Home (${items.length} items)`}

+
    + {items.map((item) => ( +
  • {`${item.label}: ${item.score}`}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx new file mode 100644 index 0000000000..faba50fc72 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) + +function SearchPage() { + const search = Route.useSearch() + + return

{`Results for "${search.q}" page ${search.page}`}

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx new file mode 100644 index 0000000000..1f46d9d885 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + return

Settings

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..7b729a3dca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' +import { productData } from '../../../shared' + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) + +function ProductPage() { + const product = Route.useLoaderData() + + return
{`${product.name}: ${product.price}`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx new file mode 100644 index 0000000000..dbca94357a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) + +function ShopIndexPage() { + return

All products

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx new file mode 100644 index 0000000000..5243d7bb0e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { shopContext } from '../../../shared' + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) + +function ShopLayout() { + return ( +
+

Shop

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/tsconfig.json b/benchmarks/client-nav/scenarios/mount/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/react/vite.config.ts b/benchmarks/client-nav/scenarios/mount/react/vite.config.ts new file mode 100644 index 0000000000..68ee46738a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav mount (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/shared.ts b/benchmarks/client-nav/scenarios/mount/shared.ts new file mode 100644 index 0000000000..a8f3d2c7bd --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/shared.ts @@ -0,0 +1,75 @@ +/** + * Shared definition of the `mount` scenario: cold start of a moderate, + * realistic app — router creation (processRouteTree), first render, initial + * `router.load()`, and unmount. This is the only scenario where router + * construction is inside the measured loop; the wider tree exists as + * realistic route-processing weight even though only the index route loads. + */ + +export const readyTestId = 'mount-ready' + +export const homeItemCount = 5 + +export function homeItems() { + return Array.from({ length: homeItemCount }, (_, index) => ({ + id: `item-${index}`, + label: `Featured item ${index}`, + score: index * 17 + 3, + })) +} + +export const homeReadyText = `Home (${homeItemCount} items)` + +export function shopContext() { + return { shopSeed: 7 } +} + +export function blogContext() { + return { blogSeed: 13 } +} + +export function productData(productId: string) { + return { + id: productId, + name: `Product ${productId}`, + price: productId.length * 100 + 42, + } +} + +export function articleData(slug: string) { + return { + slug, + title: `Article ${slug}`, + words: slug.length * 250, + } +} + +export function normalizePage(value: unknown) { + const page = Number(value) + return Number.isFinite(page) && page > 0 ? Math.trunc(page) : 1 +} + +export function normalizeQuery(value: unknown) { + return typeof value === 'string' ? value : '' +} + +export function assertReady(container: HTMLElement) { + const marker = container.querySelector(`[data-testid="${readyTestId}"]`) + if (marker?.textContent !== homeReadyText) { + throw new Error( + `Expected mount marker to be "${homeReadyText}", received "${marker?.textContent}"`, + ) + } +} + +// Mounts per benchmark iteration. +// 6 mounts per iteration: a single mount simulates to only ~8ms of CPU, and +// very short measures amplify the relative impact of allocator/GC +// quantization; 6 keeps the measure comfortably above ~50ms simulated. +export const ticksPerIteration = 6 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/project.json b/benchmarks/client-nav/scenarios/mount/solid/project.json new file mode 100644 index 0000000000..4349570654 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/setup.ts b/benchmarks/client-nav/scenarios/mount/solid/setup.ts new file mode 100644 index 0000000000..810ea38c91 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts new file mode 100644 index 0000000000..1bce4c6750 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-mount loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx new file mode 100644 index 0000000000..ae03b145ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx @@ -0,0 +1,46 @@ +import { render } from 'solid-js/web' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/solid-router' + +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { + return createRouter({ + routeTree, + history, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const history = createBrowserHistory() + const router = createTestRouter(history) + + const dispose = render(() => , container) + + return { + router, + unmount() { + dispose() + history.destroy() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..dc0acc183d --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* 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 ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..f5c3be2641 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx @@ -0,0 +1,41 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx new file mode 100644 index 0000000000..f74d85aa06 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..82caee757a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { articleData } from '../../../shared' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) + +function ArticlePage() { + const article = Route.useLoaderData() + + return
{`${article().title} (${article().words} words)`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx new file mode 100644 index 0000000000..8e75108aca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) + +function BlogIndexPage() { + return

All articles

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx new file mode 100644 index 0000000000..1e2cfbd019 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { blogContext } from '../../../shared' + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) + +function BlogLayout() { + return ( +
+

Blog

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx new file mode 100644 index 0000000000..30665ba224 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx @@ -0,0 +1,23 @@ +import { For } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { homeItems, readyTestId } from '../../../shared' + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) + +function HomePage() { + const items = Route.useLoaderData() + + return ( +
+

{`Home (${items().length} items)`}

+
    + + {(item) =>
  • {`${item.label}: ${item.score}`}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx new file mode 100644 index 0000000000..3f8b2d9dd7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) + +function SearchPage() { + const search = Route.useSearch() + + return

{`Results for "${search().q}" page ${search().page}`}

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx new file mode 100644 index 0000000000..c6b95424d9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + return

Settings

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..71b0411d6b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { productData } from '../../../shared' + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) + +function ProductPage() { + const product = Route.useLoaderData() + + return
{`${product().name}: ${product().price}`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx new file mode 100644 index 0000000000..e33c8779f9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) + +function ShopIndexPage() { + return

All products

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx new file mode 100644 index 0000000000..ff2ca78653 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { shopContext } from '../../../shared' + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) + +function ShopLayout() { + return ( +
+

Shop

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json b/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts b/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts new file mode 100644 index 0000000000..910d332f9c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav mount (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/project.json b/benchmarks/client-nav/scenarios/mount/vue/project.json new file mode 100644 index 0000000000..f2dbd10440 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/setup.ts b/benchmarks/client-nav/scenarios/mount/vue/setup.ts new file mode 100644 index 0000000000..3976e71c51 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts new file mode 100644 index 0000000000..d2e4b72c84 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-mount loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx new file mode 100644 index 0000000000..6d238e2832 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx @@ -0,0 +1,51 @@ +import * as Vue from 'vue' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/vue-router' + +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { + return createRouter({ + routeTree, + history, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const history = createBrowserHistory() + const router = createTestRouter(history) + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + history.destroy() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..98643d63c9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* 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 ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..5c2b7b52d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx @@ -0,0 +1,44 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx new file mode 100644 index 0000000000..254ecca29c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const AboutPage = Vue.defineComponent({ + setup() { + return () =>

About

+ }, +}) + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..3b2e855a6e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx @@ -0,0 +1,18 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { articleData } from '../../../shared' + +const ArticlePage = Vue.defineComponent({ + setup() { + const article = Route.useLoaderData() + + return () => ( +
{`${article.value.title} (${article.value.words} words)`}
+ ) + }, +}) + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx new file mode 100644 index 0000000000..d3c2bd0305 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const BlogIndexPage = Vue.defineComponent({ + setup() { + return () =>

All articles

+ }, +}) + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx new file mode 100644 index 0000000000..fe9532cc6f --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { blogContext } from '../../../shared' + +const BlogLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Blog

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b0be4d78bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx @@ -0,0 +1,27 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeItems, readyTestId } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + const items = Route.useLoaderData() + + return () => ( +
+

{`Home (${items.value.length} items)`}

+
    + {items.value.map((item) => ( +
  • {`${item.label}: ${item.score}`}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx new file mode 100644 index 0000000000..09fdb29c1e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +const SearchPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + + return () => ( +

{`Results for "${search.value.q}" page ${search.value.page}`}

+ ) + }, +}) + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx new file mode 100644 index 0000000000..3fc0485606 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const SettingsPage = Vue.defineComponent({ + setup() { + return () =>

Settings

+ }, +}) + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..897559599a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx @@ -0,0 +1,18 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { productData } from '../../../shared' + +const ProductPage = Vue.defineComponent({ + setup() { + const product = Route.useLoaderData() + + return () => ( +
{`${product.value.name}: ${product.value.price}`}
+ ) + }, +}) + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx new file mode 100644 index 0000000000..1040520f03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const ShopIndexPage = Vue.defineComponent({ + setup() { + return () =>

All products

+ }, +}) + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx new file mode 100644 index 0000000000..184807cfa0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { shopContext } from '../../../shared' + +const ShopLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Shop

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json b/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts b/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts new file mode 100644 index 0000000000..107e1037b6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav mount (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/react/project.json b/benchmarks/client-nav/scenarios/nested-params/react/project.json new file mode 100644 index 0000000000..800f8af9a8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/setup.ts b/benchmarks/client-nav/scenarios/nested-params/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts new file mode 100644 index 0000000000..321b1ad230 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-nested-params navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..95d4b46623 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* 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 LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx new file mode 100644 index 0000000000..ef2c7b1b18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { leafParamSets, midParams } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx new file mode 100644 index 0000000000..24e856ea15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..9789e28040 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,63 @@ +import { createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.h) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxH }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelH() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[ + params.a, + params.b, + params.c, + params.d, + params.e, + params.f, + params.g, + params.h, + ].join('.')} +
+ + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..650b32ce96 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.g) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxG }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelG() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..ca18e3ccd2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.f) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxF }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelF() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..782c1808c7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.e) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxE }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelE() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..a874bbcb63 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,55 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.d) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxD }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelD() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[params.a, params.b, params.c, params.d].join('.')} +
+ + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..94aba79453 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.c) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxC }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelC() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..1960b68276 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.b) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxB }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelB() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx new file mode 100644 index 0000000000..85d1de6f68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.a) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxA }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelA() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts new file mode 100644 index 0000000000..672b1850ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav nested-params (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/shared.ts b/benchmarks/client-nav/scenarios/nested-params/shared.ts new file mode 100644 index 0000000000..06c968bb18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/shared.ts @@ -0,0 +1,143 @@ +/** + * Shared definition of the `nested-params` scenario: deep dynamic nesting with + * per-level params.parse/stringify, beforeLoad context accumulation, and + * useParams/useRouteContext subscribers at every level. The three framework + * apps consume these builders so the workload is identical modulo the + * rendering layer. + */ + +export interface LeafParams { + a: string + b: string + c: string + d: string + e: string + f: string + g: string + h: string +} + +// Several values need percent-encoding in the URL (spaces, `&`, `%`, `+`, +// unicode) so every navigation exercises the param encode/decode paths, not +// just plain-ASCII interpolation. +const baseParams: LeafParams = { + a: 'alpha', + b: 'bravo & co', + c: 'charlie 100%', + d: 'delta', + e: 'écho-café', + f: 'foxtrot+one', + g: 'golf', + h: 'hôtel', +} + +/** + * set 0: base; set 1: only the deepest param differs (only the leaf re-runs); + * set 2: a mid param differs (levels d..h re-run); set 3: the top param + * differs (every level re-runs). + */ +export const leafParamSets: ReadonlyArray = [ + baseParams, + { ...baseParams, h: 'hilton' }, + { ...baseParams, d: 'dover' }, + { ...baseParams, a: 'axel' }, +] + +export const midParams = { + a: baseParams.a, + b: baseParams.b, + c: baseParams.c, + d: baseParams.d, +} + +export function normalizeParam(value: unknown) { + const raw = String(value) + return raw.length > 24 ? raw.slice(0, 24) : raw +} + +export function smallHash(value: string | number) { + const input = `${value}` + let hash = 5381 + for (let index = 0; index < input.length; index++) { + hash = ((hash * 33) ^ input.charCodeAt(index)) >>> 0 + } + return hash +} + +export function leafMarker(params: LeafParams) { + return [ + params.a, + params.b, + params.c, + params.d, + params.e, + params.f, + params.g, + params.h, + ] + .map(normalizeParam) + .join('.') +} + +export function midMarker(params: typeof midParams) { + return [params.a, params.b, params.c, params.d].map(normalizeParam).join('.') +} + +export const steps = [ + 'leaf-1', + 'leaf-2', + 'leaf-3', + 'leaf-4', + 'mid-4', + 'home', +] as const + +function requireText(container: HTMLElement, testId: string, expected: string) { + const element = container.querySelector(`[data-testid="${testId}"]`) + if (!element || element.textContent !== expected) { + throw new Error( + `Expected [data-testid="${testId}"] to render "${expected}", received "${element?.textContent ?? ''}"`, + ) + } +} + +function requireAbsent(container: HTMLElement, testId: string) { + if (container.querySelector(`[data-testid="${testId}"]`)) { + throw new Error(`Expected [data-testid="${testId}"] to be absent`) + } +} + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const step = steps[stepIndex % steps.length]! + switch (step) { + case 'leaf-1': + requireText(container, 'leaf-state', leafMarker(leafParamSets[0]!)) + break + case 'leaf-2': + requireText(container, 'leaf-state', leafMarker(leafParamSets[1]!)) + break + case 'leaf-3': + requireText(container, 'leaf-state', leafMarker(leafParamSets[2]!)) + break + case 'leaf-4': + requireText(container, 'leaf-state', leafMarker(leafParamSets[3]!)) + break + case 'mid-4': + requireAbsent(container, 'leaf-state') + requireText(container, 'mid-state', midMarker(midParams)) + break + case 'home': + requireAbsent(container, 'leaf-state') + requireAbsent(container, 'mid-state') + break + } +} + +// Three laps through the 6-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/project.json b/benchmarks/client-nav/scenarios/nested-params/solid/project.json new file mode 100644 index 0000000000..eb0b91bd6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts b/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts new file mode 100644 index 0000000000..d35980f612 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-nested-params navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..9065c9294f --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* 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 LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6fe7ab3e3d --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +import { leafParamSets, midParams } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx new file mode 100644 index 0000000000..cc46764de5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..7dc55db0e4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.h) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxH }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelH() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[ + params().a, + params().b, + params().c, + params().d, + params().e, + params().f, + params().g, + params().h, + ].join('.')} +
+ + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..d9c6c9429b --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.g) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxG }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelG() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..010f89bc4d --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.f) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxF }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelF() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..08c107f0c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.e) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxE }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelE() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..1b64edd58b --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,64 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.d) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxD }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelD() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[params().a, params().b, params().c, params().d].join('.')} +
+ + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..e509ea82c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.c) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxC }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelC() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..ada853b845 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.b) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxB }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelB() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx new file mode 100644 index 0000000000..5ecf004fae --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx @@ -0,0 +1,60 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.a) }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxA }) + createRenderEffect(() => { + void value() + }) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + createRenderEffect(() => { + void value() + }) + return null +} + +function LevelA() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts new file mode 100644 index 0000000000..f5c9e13c0f --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav nested-params (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/project.json b/benchmarks/client-nav/scenarios/nested-params/vue/project.json new file mode 100644 index 0000000000..83c234382c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts b/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts new file mode 100644 index 0000000000..fac8703c34 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-nested-params navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a7d1049b49 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* 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 LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..28dd4e847a --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' +import { leafParamSets, midParams } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f3ffc58610 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..756c31e1a9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,86 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.h), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxH, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelH = Vue.defineComponent({ + setup() { + const params = Route.useParams() + return () => ( + <> + + + + +
+ {[ + params.value.a, + params.value.b, + params.value.c, + params.value.d, + params.value.e, + params.value.f, + params.value.g, + params.value.h, + ].join('.')} +
+ + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..d6b9ccde83 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.g), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxG, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelG = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..76ed24904c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.f), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxF, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelF = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..e24d3dc373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.e), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxE, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelE = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..7785638ee7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,83 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.d), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxD, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelD = Vue.defineComponent({ + setup() { + const params = Route.useParams() + return () => ( + <> + + + + +
+ {[ + params.value.a, + params.value.b, + params.value.c, + params.value.d, + ].join('.')} +
+ + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..0d0adc65ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.c), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxC, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelC = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..f013d865af --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.b), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxB, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelB = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx new file mode 100644 index 0000000000..d5764c0985 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.a), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxA, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelA = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts new file mode 100644 index 0000000000..59a4af48ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav nested-params (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/react/project.json b/benchmarks/client-nav/scenarios/preload/react/project.json new file mode 100644 index 0000000000..f095228746 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/react/setup.ts b/benchmarks/client-nav/scenarios/preload/react/setup.ts new file mode 100644 index 0000000000..5409df903c --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts new file mode 100644 index 0000000000..c869ebdcfb --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-preload interaction loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/main.tsx b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx new file mode 100644 index 0000000000..6f8942c118 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..0c76e85974 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx new file mode 100644 index 0000000000..b659fe4d26 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx @@ -0,0 +1,32 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { sections } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx new file mode 100644 index 0000000000..1f87530bac --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsMarker } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) + +function DocsPage() { + return ( +
+

{docsMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx new file mode 100644 index 0000000000..2b8c83ebab --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..42962b8be7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/react-router' +import { sectionItems } from '../../../shared' + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{`${params.section}:${data.checksum}`}

+
    + {data.items.map((item) => ( +
  • {`${item.name}=${item.value}`}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/tsconfig.json b/benchmarks/client-nav/scenarios/preload/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/react/vite.config.ts b/benchmarks/client-nav/scenarios/preload/react/vite.config.ts new file mode 100644 index 0000000000..ff51686959 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav preload (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/shared.ts b/benchmarks/client-nav/scenarios/preload/shared.ts new file mode 100644 index 0000000000..d061f99451 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/shared.ts @@ -0,0 +1,119 @@ +/** + * Shared definition of the `preload` scenario: intent preloading via link + * hover, programmatic `router.preloadRoute`, and commit-time preload-cache + * maintenance. The router is configured with `defaultPreload: 'intent'`, + * `defaultPreloadDelay: 0` (no wall-clock debounce timer) and + * `defaultPreloadStaleTime: 0` — the default 30s wall-clock threshold would + * flip cache-hit behavior mid-run; with 0 every hover re-runs the preload + * work, keeping the loop stationary. + * + * Hover-intent preloading was verified manually per adapter (standalone jsdom + * run inspecting the router's cached matches after dispatching `mouseover` on + * a link). The harness hover step dispatches a single `mouseover`, which + * triggers the preload pipeline exactly once in every adapter: React + * synthesizes mouseEnter from it, Solid/Vue attach a native `mouseover` + * handler. (Solid/Vue also listen to `mouseenter`; dispatching both events + * would preload twice for them but once for React.) + */ +import type { ScenarioStep } from '../harness' + +export const sections = ['a', 'b', 'c', 'd', 'e'] as const + +export const SECTION_ITEM_COUNT = 30 + +/** Deterministic LCG stream seeded from a string — no Math.random. */ +function seededValues(seedText: string, count: number) { + let value = 0 + for (let i = 0; i < seedText.length; i++) { + value = (value * 31 + seedText.charCodeAt(i)) >>> 0 + } + + const values: Array = [] + for (let i = 0; i < count; i++) { + value = (value * 1664525 + 1013904223) >>> 0 + values.push(value % 100_000) + } + return values +} + +export interface SectionItem { + name: string + value: number +} + +export function sectionItems(section: string): Array { + return seededValues(`section-${section}`, SECTION_ITEM_COUNT).map( + (value, index) => ({ + name: `item-${section}-${index}`, + value, + }), + ) +} + +export function sectionChecksum(section: string) { + return sectionItems(section).reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) +} + +export function sectionMarker(section: string) { + return `${section}:${sectionChecksum(section)}` +} + +export const docsMarker = 'docs' +export const homeMarker = 'home' + +interface StepDef { + step: ScenarioStep + /** Expected page marker after the step; undefined for non-navigation steps. */ + marker?: string +} + +export const stepDefs: ReadonlyArray = [ + { step: { type: 'hover', testId: 's-a' } }, + { step: { type: 'hover', testId: 's-b' } }, + { step: 's-a', marker: sectionMarker('a') }, + { step: { type: 'hover', testId: 's-c' } }, + { step: 's-c', marker: sectionMarker('c') }, + { + step: { + type: 'preload', + getOptions: () => ({ + to: '/sections/$section', + params: { section: 'd' }, + }), + }, + }, + { step: 's-d', marker: sectionMarker('d') }, + { step: { type: 'hover', testId: 's-e' } }, + { step: 'go-docs', marker: docsMarker }, + { step: 'go-home', marker: homeMarker }, +] + +export const scenarioSteps: ReadonlyArray = stepDefs.map( + (def) => def.step, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const def = stepDefs[stepIndex]! + if (!def.marker) { + return + } + + const marker = container.querySelector('[data-testid="page-state"]') + if (marker?.textContent !== def.marker) { + throw new Error( + `Expected page marker "${def.marker}" after step ${stepIndex}, received "${marker?.textContent}"`, + ) + } +} + +// Two laps through the 10-step sequence per benchmark iteration. +export const ticksPerIteration = 20 + +export const benchOptions = { + warmupIterations: 50, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/project.json b/benchmarks/client-nav/scenarios/preload/solid/project.json new file mode 100644 index 0000000000..a186879335 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/setup.ts b/benchmarks/client-nav/scenarios/preload/solid/setup.ts new file mode 100644 index 0000000000..34270fe8a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts new file mode 100644 index 0000000000..ff2771985b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-preload interaction loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx new file mode 100644 index 0000000000..72e414c0a0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx @@ -0,0 +1,35 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..645d925238 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a805ed0feb --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx @@ -0,0 +1,34 @@ +import { For } from 'solid-js' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +import { sections } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx new file mode 100644 index 0000000000..b077cfd18f --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { docsMarker } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) + +function DocsPage() { + return ( +
+

{docsMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx new file mode 100644 index 0000000000..72fbfe387a --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..2a7fb2ad1b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx @@ -0,0 +1,33 @@ +import { For } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { sectionItems } from '../../../shared' + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{`${params().section}:${data().checksum}`}

+
    + + {(item) =>
  • {`${item.name}=${item.value}`}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json b/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts b/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts new file mode 100644 index 0000000000..b542834469 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav preload (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/project.json b/benchmarks/client-nav/scenarios/preload/vue/project.json new file mode 100644 index 0000000000..404476723d --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/setup.ts b/benchmarks/client-nav/scenarios/preload/vue/setup.ts new file mode 100644 index 0000000000..f70a154909 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts new file mode 100644 index 0000000000..2e790e9240 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-preload interaction loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx new file mode 100644 index 0000000000..1e325392a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx @@ -0,0 +1,42 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..05a751060b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..bf91ba3770 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx @@ -0,0 +1,35 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' +import { sections } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx new file mode 100644 index 0000000000..07d0a989ad --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { docsMarker } from '../../../shared' + +const DocsPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{docsMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f04a9f5796 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeMarker } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..aeb617a86f --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx @@ -0,0 +1,37 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { sectionItems } from '../../../shared' + +const SectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return () => ( +
+

+ {`${params.value.section}:${data.value.checksum}`} +

+
    + {data.value.items.map((item) => ( +
  • {`${item.name}=${item.value}`}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json b/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts b/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts new file mode 100644 index 0000000000..25e64b5bef --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav preload (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/react/project.json b/benchmarks/client-nav/scenarios/rewrites/react/project.json new file mode 100644 index 0000000000..d98c6e1412 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/setup.ts b/benchmarks/client-nav/scenarios/rewrites/react/setup.ts new file mode 100644 index 0000000000..0c5df273ed --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts new file mode 100644 index 0000000000..9b77a79838 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx new file mode 100644 index 0000000000..47746bb0fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..b6ab8c8be4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +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 { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/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 { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/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 = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx new file mode 100644 index 0000000000..ad14ff51f1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx new file mode 100644 index 0000000000..821cfe4a7c --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..ec1163e5c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { itemLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemLabel(params.a, params.b)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx new file mode 100644 index 0000000000..f4937494f0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx @@ -0,0 +1,17 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { sectionLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + + return ( +
+

{sectionLabel(params.a)}

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts new file mode 100644 index 0000000000..8c07cff4fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav rewrites (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/shared.ts b/benchmarks/client-nav/scenarios/rewrites/shared.ts new file mode 100644 index 0000000000..c45b15d503 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/shared.ts @@ -0,0 +1,101 @@ +/** + * Shared definition of the `rewrites` scenario: composed client-side location + * rewrites — router `basepath` plus a locale input/output rewrite (the client + * analog of the SSR `rewrites` scenario). Externally URLs look like + * `/app/{locale}/p/...`; internally the router sees `/p/...` with the locale + * carried in the `_locale` search param. Every navigation runs the composed + * output rewrite when building hrefs/committing and the input rewrite when + * parsing the committed location. + */ +import type { ScenarioStep } from '../harness' + +export const basepath = '/app' +export const defaultLocale = 'en' + +const localePrefixRe = /^\/(fr|de|es)(\/.*)$/ + +// Framework-agnostic rewrite pair consumed by all three apps' createRouter. +export const localeRewrite = { + input: ({ url }: { url: 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 }: { url: URL }) => { + const locale = url.searchParams.get('_locale') + + if (locale) { + url.searchParams.delete('_locale') + url.pathname = `/${locale}${url.pathname}` + } + + return url + }, +} + +export function sectionLabel(a: string) { + return `Section ${a}` +} + +export function itemLabel(a: string, b: string) { + return `Item ${a}/${b}` +} + +interface StepDef { + testId: string + /** Internal router pathname expected after the step. */ + routerPath: string + /** External window.location pathname expected after the step. */ + windowPath: string +} + +// Circular lap: alternates sections, depths, and locales so every step runs +// the composed rewrites in both directions, then returns to the start. +export const stepDefs: ReadonlyArray = [ + { testId: 'go-x', routerPath: '/p/x', windowPath: '/app/p/x' }, + { testId: 'go-x-1-fr', routerPath: '/p/x/1', windowPath: '/app/fr/p/x/1' }, + { testId: 'go-y-de', routerPath: '/p/y', windowPath: '/app/de/p/y' }, + { testId: 'go-y-2', routerPath: '/p/y/2', windowPath: '/app/p/y/2' }, + { testId: 'go-x-es', routerPath: '/p/x', windowPath: '/app/es/p/x' }, + { testId: 'go-home', routerPath: '/', windowPath: '/app/' }, +] as const + +export const steps: ReadonlyArray = stepDefs.map( + (step) => step.testId, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = stepDefs[stepIndex]! + const marker = container.querySelector('[data-testid="loc"]') + const routerPath = marker?.textContent + + if (routerPath !== expected.routerPath) { + throw new Error( + `Expected router pathname "${expected.routerPath}" after step ${stepIndex}, received "${routerPath}"`, + ) + } + + if (window.location.pathname !== expected.windowPath) { + throw new Error( + `Expected window pathname "${expected.windowPath}" after step ${stepIndex}, received "${window.location.pathname}"`, + ) + } +} + +// The app starts on the external root inside the basepath. +export const initialUrl = '/app' + +// Three laps through the 6-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/project.json b/benchmarks/client-nav/scenarios/rewrites/solid/project.json new file mode 100644 index 0000000000..4b2f763032 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts b/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts new file mode 100644 index 0000000000..5796df712d --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts new file mode 100644 index 0000000000..30d3436071 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx new file mode 100644 index 0000000000..a80de580fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx @@ -0,0 +1,35 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..2d35f899c9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +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 { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/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 { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/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 = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..3a78f2c3cf --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx new file mode 100644 index 0000000000..5ee381ff05 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..febcad35e3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { itemLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemLabel(params().a, params().b)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx new file mode 100644 index 0000000000..adfb7bc3b6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx @@ -0,0 +1,17 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { sectionLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + + return ( +
+

{sectionLabel(params().a)}

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts new file mode 100644 index 0000000000..41613c2ad4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav rewrites (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/project.json b/benchmarks/client-nav/scenarios/rewrites/vue/project.json new file mode 100644 index 0000000000..9b7cb5d198 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts b/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts new file mode 100644 index 0000000000..e26b4d05ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts new file mode 100644 index 0000000000..c10b8723bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx new file mode 100644 index 0000000000..83e430038d --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx @@ -0,0 +1,42 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..6eca4e8ce5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* 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 PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +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 { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/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 { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/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 = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..ae89645005 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx @@ -0,0 +1,64 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx new file mode 100644 index 0000000000..d5405b52d2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Home

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..062c0903b9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { itemLabel } from '../../../shared' + +const ItemPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{itemLabel(params.value.a, params.value.b)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx new file mode 100644 index 0000000000..552700d402 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx @@ -0,0 +1,20 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { sectionLabel } from '../../../shared' + +const SectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{sectionLabel(params.value.a)}

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts new file mode 100644 index 0000000000..39e22b6c3e --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav rewrites (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json new file mode 100644 index 0000000000..3291738f79 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts new file mode 100644 index 0000000000..50cc26d5d6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-route-tree-scale navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..b58b75180f --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* 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 SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..ac2a376549 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) + +function Page() { + return
pricing
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..b71962254e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) + +function Page() { + return
promo
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx new file mode 100644 index 0000000000..7ed47da924 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..893419cef5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) + +function Page() { + return
alpha
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..d20c6007ee --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) + +function Page() { + return
beta
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx new file mode 100644 index 0000000000..961feb50f1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx new file mode 100644 index 0000000000..028aa43d5a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`files:${params._splat}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx new file mode 100644 index 0000000000..8a7ffca9e7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Page, +}) + +function Page() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..dfe8e31905 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`release:${params.version}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..8be053a040 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-a:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..54c1b28618 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) + +function Page() { + return
sec-a:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..3c793a94f5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) + +function Page() { + return
sec-a:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..0037f6463d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) + +function Page() { + return
sec-a:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx new file mode 100644 index 0000000000..846f96b67b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..0dd7f94feb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-b:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..fde4647472 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) + +function Page() { + return
sec-b:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..51b69cf664 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) + +function Page() { + return
sec-b:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..977b7438b1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) + +function Page() { + return
sec-b:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx new file mode 100644 index 0000000000..77d7e6475d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..eb3be796c0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-c:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..7910acf332 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) + +function Page() { + return
sec-c:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..f6ae102f2d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) + +function Page() { + return
sec-c:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..bf2be30788 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) + +function Page() { + return
sec-c:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx new file mode 100644 index 0000000000..77ac5644c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..d34aee8972 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-d:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..1f095cb2ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) + +function Page() { + return
sec-d:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..8fc698a4d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) + +function Page() { + return
sec-d:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..76c7a178ea --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) + +function Page() { + return
sec-d:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx new file mode 100644 index 0000000000..5f3d281b5e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..6e31d0c5d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-e:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..5a2835917b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) + +function Page() { + return
sec-e:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..0f23b8d484 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) + +function Page() { + return
sec-e:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..b566a1ffaa --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) + +function Page() { + return
sec-e:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx new file mode 100644 index 0000000000..e573a71d98 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..a71c17f96e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-f:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..06831063ad --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) + +function Page() { + return
sec-f:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..4cd54b6a7f --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) + +function Page() { + return
sec-f:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..92bdca5e44 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) + +function Page() { + return
sec-f:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx new file mode 100644 index 0000000000..2fa27873e8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts new file mode 100644 index 0000000000..686907a506 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts @@ -0,0 +1,43 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts new file mode 100644 index 0000000000..8af5996804 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts @@ -0,0 +1,64 @@ +/** + * Shared definition of the `route-tree-scale` scenario: route matching, + * location building, and link-target resolution over a wide route tree + * (~40 routes) mixing static segments, dynamic params, prefixed params, + * splats, pathless layouts, and route groups. Navigations jump between + * distant branches so matching cannot reuse the previous branch. + */ + +export const sections = [ + 'sec-a', + 'sec-b', + 'sec-c', + 'sec-d', + 'sec-e', + 'sec-f', +] as const + +export const steps = [ + 'go-sec-a-id', + 'go-sec-f-settings', + 'go-files', + 'go-release', + 'go-alpha', + 'go-promo', + 'go-sec-c-id', + 'go-sec-d-about', + 'go-home', +] as const + +// The dynamic ids and the splat carry characters that need percent-encoding +// (spaces, `&`, `%`, `+`, unicode) so matching also exercises the segment +// encode/decode paths. +const expectedMarkers = [ + 'sec-a:item 11%', + 'sec-f:settings', + 'files:x/ü y/z', + 'release:9', + 'alpha', + 'promo', + 'sec-c:q&a+42', + 'sec-d:about', + 'home', +] as const + +export function assertStepResult(stepIndex: number) { + const expected = expectedMarkers[stepIndex]! + const actual = document.querySelector( + '[data-testid="scale-state"]', + )?.textContent + if (actual !== expected) { + throw new Error( + `Expected scale-state marker "${expected}" after step ${stepIndex}, received "${actual}"`, + ) + } +} + +// Three laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 27 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json new file mode 100644 index 0000000000..c568e41196 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts new file mode 100644 index 0000000000..3d7a22a027 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-route-tree-scale navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx new file mode 100644 index 0000000000..817045bd02 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..20b4af6146 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* 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 SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..0965507de6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) + +function Page() { + return
pricing
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..220bd06a68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) + +function Page() { + return
promo
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a4e4113d9a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..234b8488a9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) + +function Page() { + return
alpha
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..39bc716efa --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) + +function Page() { + return
beta
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx new file mode 100644 index 0000000000..f1e499d3b3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx new file mode 100644 index 0000000000..47b1a5eea9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`files:${params()._splat}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx new file mode 100644 index 0000000000..25b1c03c1a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Page, +}) + +function Page() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..0dbcee35ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`release:${params().version}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..18731a2844 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-a:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..1699360a1c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) + +function Page() { + return
sec-a:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..e71e525d07 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) + +function Page() { + return
sec-a:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..49d470190e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) + +function Page() { + return
sec-a:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx new file mode 100644 index 0000000000..2861169fb9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..86fe71d7c0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-b:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..586b829cd0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) + +function Page() { + return
sec-b:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..7098416658 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) + +function Page() { + return
sec-b:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..c2e0f47650 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) + +function Page() { + return
sec-b:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx new file mode 100644 index 0000000000..f2905da9ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..3e07e8033a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-c:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..86ed5043a3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) + +function Page() { + return
sec-c:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..aa9ed2dc6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) + +function Page() { + return
sec-c:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..d0dd3e952c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) + +function Page() { + return
sec-c:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx new file mode 100644 index 0000000000..fa53825238 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..3722012d7c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-d:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..860d2b9e5b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) + +function Page() { + return
sec-d:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..ec7b8eaf2c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) + +function Page() { + return
sec-d:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..d83e3885fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) + +function Page() { + return
sec-d:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx new file mode 100644 index 0000000000..e71391e2a3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..e58d9d77ac --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-e:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..3ef4d2c763 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) + +function Page() { + return
sec-e:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..88bf7ca271 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) + +function Page() { + return
sec-e:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..8a9945926d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) + +function Page() { + return
sec-e:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx new file mode 100644 index 0000000000..8b5ef24519 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..2b81ef96ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-f:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..1c4e3a5474 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) + +function Page() { + return
sec-f:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..65803ee848 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) + +function Page() { + return
sec-f:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..5761138262 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) + +function Page() { + return
sec-f:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx new file mode 100644 index 0000000000..c7f25eecdd --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts new file mode 100644 index 0000000000..dd0bd234e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts @@ -0,0 +1,51 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json new file mode 100644 index 0000000000..f613070038 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts new file mode 100644 index 0000000000..a767b8d220 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-route-tree-scale navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a03c051a79 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* 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 SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..f606c10b4b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
pricing
+ }, +}) + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..61c3eb3ec9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
promo
+ }, +}) + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..d2e1483106 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..ff6ad5350e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
alpha
+ }, +}) + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..2f7cab6c0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
beta
+ }, +}) + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx new file mode 100644 index 0000000000..e8221f0f65 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx new file mode 100644 index 0000000000..221fae01be --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`files:${params.value._splat}`}
+ ) + }, +}) + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b3e94f37c4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..86b1a2c8f0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`release:${params.value.version}`}
+ ) + }, +}) + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..864074940d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-a:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..715ab9548c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:about
+ }, +}) + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..baef08547c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:index
+ }, +}) + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..9e6a56b913 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:settings
+ }, +}) + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx new file mode 100644 index 0000000000..a9fb557aea --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..9a03a8fe01 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-b:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..2ef1100995 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:about
+ }, +}) + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..652e5f895c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:index
+ }, +}) + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..e8fa36377e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:settings
+ }, +}) + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx new file mode 100644 index 0000000000..ba31612633 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..7f0407d84e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-c:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..d26776d439 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:about
+ }, +}) + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..1835eff9b7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:index
+ }, +}) + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..1bf85fd6d9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:settings
+ }, +}) + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx new file mode 100644 index 0000000000..009f814b93 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..ff5aab05e3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-d:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..25a8078626 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:about
+ }, +}) + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..cd9b89664a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:index
+ }, +}) + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..8e4309de32 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:settings
+ }, +}) + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx new file mode 100644 index 0000000000..6b12b514da --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..4525051967 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-e:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..71a32bd36b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:about
+ }, +}) + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..9b467ca53e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:index
+ }, +}) + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..a01066488c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:settings
+ }, +}) + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx new file mode 100644 index 0000000000..38f4da91fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..c740f9d223 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-f:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..229dab22a2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:about
+ }, +}) + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..14e3f46a13 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:index
+ }, +}) + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..da2c7483b9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:settings
+ }, +}) + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx new file mode 100644 index 0000000000..3b389c8144 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts new file mode 100644 index 0000000000..a547406198 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts @@ -0,0 +1,45 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/react/project.json b/benchmarks/client-nav/scenarios/search-params/react/project.json new file mode 100644 index 0000000000..f2d743f5f9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/setup.ts b/benchmarks/client-nav/scenarios/search-params/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts new file mode 100644 index 0000000000..c534611bd0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-search-params navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx new file mode 100644 index 0000000000..a68ef7bbb2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx @@ -0,0 +1,36 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..7380b704e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx new file mode 100644 index 0000000000..86399d0175 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx @@ -0,0 +1,116 @@ +import { + Link, + Outlet, + createRootRoute, + useSearch, +} from '@tanstack/react-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +function RootSearchSubscriber() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + void computeChecksum(value) + return null +} + +function RootComponent() { + return ( + <> + {rootSubscribers.map((index) => ( + + ))} + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx new file mode 100644 index 0000000000..73f59c13b0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx @@ -0,0 +1,42 @@ +import { createFileRoute, retainSearchParams } from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function ViewSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + void computeChecksum(value) + return null +} + +function CatalogPage() { + const search = Route.useSearch() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} +

Catalog

+
{catalogMarkerText(search)}
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx new file mode 100644 index 0000000000..5879ad518d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Search Params Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx new file mode 100644 index 0000000000..d22e99c4bf --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx @@ -0,0 +1,91 @@ +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PageSubscriber() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + void computeChecksum(value) + return null +} + +function FiltersSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + void computeChecksum(value) + return null +} + +function QuerySubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + void computeChecksum(value) + return null +} + +function ProductsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Products

+
{productsMarkerText(search)}
+

{`checksum ${loaderData.checksum}`}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts new file mode 100644 index 0000000000..b220ae7d0d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav search-params (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/shared.ts b/benchmarks/client-nav/scenarios/search-params/shared.ts new file mode 100644 index 0000000000..343e319ded --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/shared.ts @@ -0,0 +1,200 @@ +/** + * Shared definition of the `search-params` scenario: search schema + * normalizers, the click sequence, and per-step sanity assertions. The three + * framework apps consume these builders so the workload is identical modulo + * the rendering layer. + * + * Isolates: validateSearch execution, search middlewares + * (retainSearchParams/stripSearchParams), parse/stringify of medium-complex + * search objects, functional search updaters, structural sharing, and + * useSearch selector subscriptions. + */ + +import type { ScenarioStep } from '../harness' + +export function computeChecksum(seed: number) { + let value = Math.trunc(seed) | 0 + + for (let index = 0; index < 40; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + + return value +} + +export interface ProductsSearch { + page: number + perPage: number + sort: 'asc' | 'desc' + q: string + filters: { + categories: Array + price: { min: number; max: number } + tags: Array + } +} + +export interface CatalogSearch { + view: 'grid' | 'list' + page: number + perPage: number + sort: 'asc' | 'desc' +} + +function normalizeNumber(value: unknown, fallback: number) { + const num = Number(value) + return Number.isFinite(num) && num >= 0 ? Math.trunc(num) : fallback +} + +function normalizeString(value: unknown, fallback: string) { + return typeof value === 'string' ? value : fallback +} + +function normalizeSort(value: unknown): 'asc' | 'desc' { + return value === 'desc' ? 'desc' : 'asc' +} + +function normalizeStringArray(value: unknown): Array { + if (!Array.isArray(value)) { + return [] + } + return value.filter((entry): entry is string => typeof entry === 'string') +} + +export function normalizeProductsSearch( + search: Record, +): ProductsSearch { + const filters = + typeof search.filters === 'object' && search.filters !== null + ? (search.filters as Record) + : {} + const price = + typeof filters.price === 'object' && filters.price !== null + ? (filters.price as Record) + : {} + + return { + page: Math.max(1, normalizeNumber(search.page, 1)), + perPage: Math.max(1, normalizeNumber(search.perPage, 20)), + sort: normalizeSort(search.sort), + q: normalizeString(search.q, ''), + filters: { + categories: normalizeStringArray(filters.categories), + price: { + min: normalizeNumber(price.min, 0), + max: normalizeNumber(price.max, 1000), + }, + tags: normalizeStringArray(filters.tags), + }, + } +} + +export function normalizeCatalogSearch( + search: Record, +): CatalogSearch { + return { + view: search.view === 'list' ? 'list' : 'grid', + page: Math.max(1, normalizeNumber(search.page, 1)), + perPage: Math.max(1, normalizeNumber(search.perPage, 20)), + sort: normalizeSort(search.sort), + } +} + +export const initialProductsSearch = { + page: 2, + perPage: 50, + sort: 'desc', + q: 'gadget', + filters: { + categories: ['audio', 'video'], + price: { min: 10, max: 200 }, + tags: ['new', 'sale'], + }, +} satisfies ProductsSearch + +export const changedCategories = ['x-ray', 'yield', 'zeta'] + +export function productsMarkerText(search: ProductsSearch) { + return `${search.page}|${search.sort}|${search.filters.categories.join(',')}` +} + +export function catalogMarkerText(search: CatalogSearch) { + return `${search.view}|${search.page}|${search.perPage}|${search.sort}` +} + +export function productsLoaderChecksum(deps: { + page: number + sort: string + filters: ProductsSearch['filters'] +}) { + return computeChecksum( + deps.page * 31 + + deps.sort.length * 7 + + deps.filters.categories.join('').length * 13 + + deps.filters.tags.length * 3 + + (deps.filters.price.max - deps.filters.price.min), + ) +} + +export const steps: ReadonlyArray = [ + 'go-products', + 'products-next-page', + 'products-categories', + 'products-sort', + 'products-reset', + 'go-catalog', + 'catalog-next-page', + 'products-partial', + 'go-home', +] + +interface ExpectedState { + testId: string + text?: string +} + +const expectedStates: ReadonlyArray = [ + // Full search literal from home. + { testId: 'products-state', text: '2|desc|audio,video' }, + // Functional updater bumping the page. + { testId: 'products-state', text: '3|desc|audio,video' }, + // Functional updater replacing filters.categories. + { testId: 'products-state', text: '3|desc|x-ray,yield,zeta' }, + // Functional updater flipping sort desc -> asc. + { testId: 'products-state', text: '3|asc|x-ray,yield,zeta' }, + // Functional updater resetting page to 1 (stripSearchParams removes it). + { testId: 'products-state', text: '1|asc|x-ray,yield,zeta' }, + // search: true carries the current products search into catalog. + { testId: 'catalog-state', text: 'grid|1|50|asc' }, + // Functional updater bumping the catalog page. + { testId: 'catalog-state', text: 'grid|2|50|asc' }, + // Partial search literal; retainSearchParams fills perPage/sort from catalog. + { testId: 'products-state', text: '1|asc|' }, + { testId: 'home-state' }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = expectedStates[stepIndex]! + const element = container.querySelector(`[data-testid="${expected.testId}"]`) + + if (!element) { + throw new Error( + `Expected marker "${expected.testId}" to exist after step ${stepIndex}`, + ) + } + + if (expected.text !== undefined && element.textContent !== expected.text) { + throw new Error( + `Expected marker "${expected.testId}" to read "${expected.text}" after step ${stepIndex}, received "${element.textContent}"`, + ) + } +} + +// Two laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/project.json b/benchmarks/client-nav/scenarios/search-params/solid/project.json new file mode 100644 index 0000000000..af48217e03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/setup.ts b/benchmarks/client-nav/scenarios/search-params/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts new file mode 100644 index 0000000000..df6a24dc15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-search-params navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx new file mode 100644 index 0000000000..e95846e12d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx @@ -0,0 +1,33 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..96d211d64c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..30c042f9c5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx @@ -0,0 +1,120 @@ +import { createRenderEffect } from 'solid-js' +import { + Link, + Outlet, + createRootRoute, + useSearch, +} from '@tanstack/solid-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function RootSearchSubscriber() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + return computeChecksum(value())} /> +} + +function RootComponent() { + return ( + <> + {rootSubscribers.map(() => ( + + ))} + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx new file mode 100644 index 0000000000..60cf8e7896 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx @@ -0,0 +1,50 @@ +import { createRenderEffect } from 'solid-js' +import { createFileRoute, retainSearchParams } from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function ViewSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + return computeChecksum(value())} /> +} + +function CatalogPage() { + const search = Route.useSearch() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} +

Catalog

+
{catalogMarkerText(search())}
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx new file mode 100644 index 0000000000..0bbb913d83 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Search Params Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx new file mode 100644 index 0000000000..8b2a252155 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx @@ -0,0 +1,97 @@ +import { createRenderEffect } from 'solid-js' +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect(() => { + void props.value() + }) + + return null +} + +function PageSubscriber() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + return computeChecksum(value())} /> +} + +function FiltersSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + return computeChecksum(value())} /> +} + +function QuerySubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + return computeChecksum(value())} /> +} + +function ProductsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Products

+
{productsMarkerText(search())}
+

{`checksum ${loaderData().checksum}`}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json new file mode 100644 index 0000000000..98d76ad6b8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts new file mode 100644 index 0000000000..c2f4b32806 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav search-params (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/project.json b/benchmarks/client-nav/scenarios/search-params/vue/project.json new file mode 100644 index 0000000000..47b9712a2c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/setup.ts b/benchmarks/client-nav/scenarios/search-params/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts new file mode 100644 index 0000000000..0b394bbe27 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * 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(test.before) + afterAll(test.after) + + bench( + 'client-search-params navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx new file mode 100644 index 0000000000..1454b8d547 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx @@ -0,0 +1,40 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b95b667803 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* 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 ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..b4cb07f2ae --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx @@ -0,0 +1,114 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute, useSearch } from '@tanstack/vue-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +const RootSearchSubscriber = Vue.defineComponent({ + setup() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + {rootSubscribers.map((index) => ( + + ))} + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx new file mode 100644 index 0000000000..6a09efdd4a --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx @@ -0,0 +1,49 @@ +import * as Vue from 'vue' +import { createFileRoute, retainSearchParams } from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const ViewSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const CatalogPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} +

Catalog

+
{catalogMarkerText(search.value)}
+
+ ) + }, +}) + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx new file mode 100644 index 0000000000..081aa7b4fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Search Params Bench

+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx new file mode 100644 index 0000000000..8be1683e14 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx @@ -0,0 +1,108 @@ +import * as Vue from 'vue' +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const PageSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FiltersSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const QuerySubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const ProductsPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Products

+
+ {productsMarkerText(search.value)} +
+

{`checksum ${loaderData.value.checksum}`}

+
+ ) + }, +}) + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts new file mode 100644 index 0000000000..9a3b82d066 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav search-params (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/solid/vite.config.ts b/benchmarks/client-nav/solid/vite.config.ts index 24db8fe745..bd11fe8de9 100644 --- a/benchmarks/client-nav/solid/vite.config.ts +++ b/benchmarks/client-nav/solid/vite.config.ts @@ -1,8 +1,14 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import solid from 'vite-plugin-solid' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -28,7 +34,7 @@ export default defineConfig({ name: '@benchmarks/client-nav (solid)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], server: { deps: { inline: [/@solidjs/, /@tanstack\/solid-store/], diff --git a/benchmarks/client-nav/vitest.config.ts b/benchmarks/client-nav/vitest.config.ts index 14776452ed..808e7fc724 100644 --- a/benchmarks/client-nav/vitest.config.ts +++ b/benchmarks/client-nav/vitest.config.ts @@ -7,6 +7,9 @@ export default defineConfig({ './react/vite.config.ts', './solid/vite.config.ts', './vue/vite.config.ts', + './scenarios/*/react/vite.config.ts', + './scenarios/*/solid/vite.config.ts', + './scenarios/*/vue/vite.config.ts', ], }, }) diff --git a/benchmarks/client-nav/vitest.react.config.ts b/benchmarks/client-nav/vitest.react.config.ts new file mode 100644 index 0000000000..a397ad929e --- /dev/null +++ b/benchmarks/client-nav/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/client-nav/vitest.solid.config.ts b/benchmarks/client-nav/vitest.solid.config.ts new file mode 100644 index 0000000000..b8827b86aa --- /dev/null +++ b/benchmarks/client-nav/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/client-nav/vitest.vue.config.ts b/benchmarks/client-nav/vitest.vue.config.ts new file mode 100644 index 0000000000..17960d7569 --- /dev/null +++ b/benchmarks/client-nav/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/client-nav/vue/vite.config.ts b/benchmarks/client-nav/vue/vite.config.ts index d917c82e2d..4c44c09fdd 100644 --- a/benchmarks/client-nav/vue/vite.config.ts +++ b/benchmarks/client-nav/vue/vite.config.ts @@ -1,9 +1,15 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -27,6 +33,6 @@ export default defineConfig({ name: '@benchmarks/client-nav (vue)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62525d6c48..96847b4044 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -285,6 +285,9 @@ importers: '@platformatic/flame': specifier: ^1.6.0 version: 1.6.0 + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../packages/router-plugin '@testing-library/react': specifier: ^16.2.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)