Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions benchmarks/client-nav/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Client Navigation Benchmarks

Cross-framework client-side navigation benchmarks for:

- `@tanstack/react-router`
- `@tanstack/solid-router`
- `@tanstack/vue-router`

## Layout

- `react/` - React benchmark + Vitest config
- `solid/` - Solid benchmark + Vitest config
- `vue/` - Vue benchmark + Vitest config

## Run

Run all benchmarks through Nx so dependency builds are part of the graph:

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf --outputStyle=stream --skipRemoteCache
```

Run framework-specific benchmarks:

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:react --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:solid --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache
```

Typecheck benchmark sources:

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:types --outputStyle=stream --skipRemoteCache
```
71 changes: 71 additions & 0 deletions benchmarks/client-nav/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "@benchmarks/client-nav",
"private": true,
"type": "module",
"scripts": {
"test:perf": "vitest bench",
"test:perf:react": "vitest bench --config ./react/vitest.config.ts ./react/speed.bench.tsx",
"test:perf:solid": "vitest bench --config ./solid/vitest.config.ts ./solid/speed.bench.tsx",
"test:perf:vue": "vitest bench --config ./vue/vitest.config.ts ./vue/speed.bench.tsx",
"test:types": "pnpm run test:types:react && pnpm run test:types:solid && pnpm run test:types:vue",
"test:types:react": "tsc -p ./react/tsconfig.json --noEmit",
"test:types:solid": "tsc -p ./solid/tsconfig.json --noEmit",
"test:types:vue": "tsc -p ./vue/tsconfig.json --noEmit"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/router-core": "workspace:^",
"@tanstack/solid-router": "workspace:^",
"@tanstack/vue-router": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"solid-js": "^1.9.10",
"vue": "^3.5.16"
},
"devDependencies": {
"@solidjs/testing-library": "^0.8.10",
"@testing-library/react": "^16.2.0",
"@testing-library/vue": "^8.1.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"typescript": "^5.7.2",
"vite": "^7.3.1",
"vite-plugin-solid": "^2.11.10",
Comment thread
Sheraff marked this conversation as resolved.
"vitest": "^4.0.17"
},
"nx": {
"targets": {
"test:perf": {
"cache": false,
"dependsOn": [
"^build"
]
},
"test:perf:react": {
"cache": false,
"dependsOn": [
"^build"
]
},
"test:perf:solid": {
"cache": false,
"dependsOn": [
"^build"
]
},
"test:perf:vue": {
"cache": false,
"dependsOn": [
"^build"
]
},
"test:types": {
"cache": false,
"dependsOn": [
"^build"
]
}
}
}
}
120 changes: 120 additions & 0 deletions benchmarks/client-nav/react/speed.bench.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { cleanup, render } from '@testing-library/react'
import { act } from 'react'
import { bench, describe } from 'vitest'
import { rootRouteId } from '@tanstack/router-core'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
useParams,
useSearch,
} from '@tanstack/react-router'
import type { NavigateOptions } from '@tanstack/router-core'

function createTestRouter() {
function runPerfSelectorComputation(seed: number) {
let value = Math.trunc(seed) | 0

for (let index = 0; index < 100; index++) {
value = (value * 1664525 + 1013904223 + index) >>> 0
}

return value
}

const selectors = Array.from({ length: 20 }, (_, index) => index)

function Params() {
const params = useParams({
from: rootRouteId,
select: (params) => runPerfSelectorComputation(Number(params.id ?? 0)),
})
void params
return null
}

function Search() {
const search = useSearch({
from: rootRouteId,
select: (search) => runPerfSelectorComputation(Number(search.id ?? 0)),
})
void search
return null
}

function Root() {
return (
<>
{selectors.map((selector) => (
<Params key={selector} />
))}
{selectors.map((selector) => (
<Search key={selector} />
))}
<Outlet />
</>
)
}

const root = createRootRoute({
component: Root,
})

const route = createRoute({
getParentRoute: () => root,
path: '/$id',
component: () => <div />,
})

return createRouter({
history: createMemoryHistory({
initialEntries: ['/0'],
}),
scrollRestoration: true,
routeTree: root.addChildren([route]),
})
}

describe('speed', () => {
let id = 0
const router = createTestRouter()
let unsub = () => {}
let next: () => Promise<void> = () => Promise.reject()

bench('navigate', () => act(next), {
warmupIterations: 1000,
time: 10_000,
setup: async () => {
id = 0
let resolve: () => void = () => {}
unsub = router.subscribe('onRendered', () => resolve())

const navigate = (opts: NavigateOptions) =>
new Promise<void>((resolveNext) => {
resolve = resolveNext
router.navigate(opts)
})

next = () => {
const nextId = id++

return navigate({
to: '/$id',
params: { id: nextId },
search: { id: nextId },
replace: true,
})
}

render(<RouterProvider router={router} />)
await act(() => router.load())
},
teardown: () => {
cleanup()
unsub()
},
})
})
9 changes: 9 additions & 0 deletions benchmarks/client-nav/react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": ["speed.bench.tsx", "vitest.config.ts", "../vitest.setup.ts"]
}
17 changes: 17 additions & 0 deletions benchmarks/client-nav/react/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
plugins: [react()],
...(process.env.VITEST && {
resolve: {
conditions: ['development'],
},
}),
test: {
name: '@benchmarks/client-nav (react)',
watch: false,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
},
})
135 changes: 135 additions & 0 deletions benchmarks/client-nav/solid/speed.bench.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { cleanup, render } from '@solidjs/testing-library'
import { createEffect } from 'solid-js'
import { afterAll, beforeAll, bench, describe } from 'vitest'
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
useParams,
useSearch,
} from '@tanstack/solid-router'
import type { NavigateOptions } from '@tanstack/router-core'

function createTestRouter() {
function runPerfSelectorComputation(seed: number) {
let value = Math.trunc(seed) | 0

for (let index = 0; index < 100; index++) {
value = (value * 1664525 + 1013904223 + index) >>> 0
}

return value
}

const selectors = Array.from({ length: 20 }, (_, index) => index)

function Params() {
const params = useParams({
strict: false,
select: (params) => runPerfSelectorComputation(Number(params.id ?? 0)),
})

createEffect(() => {
void params()
})

return null
}

function Search() {
const search = useSearch({
strict: false,
select: (search) => runPerfSelectorComputation(Number(search.id ?? 0)),
})

createEffect(() => {
void search()
})

return null
}

function Root() {
return (
<>
{selectors.map(() => (
<Params />
))}
{selectors.map(() => (
<Search />
))}
<Outlet />
</>
)
}

const root = createRootRoute({
component: Root,
})

const route = createRoute({
getParentRoute: () => root,
path: '/$id',
component: () => {
console.time('render')
Comment thread
Sheraff marked this conversation as resolved.
Outdated
Comment thread
Sheraff marked this conversation as resolved.
Outdated
return <div />
},
})

return createRouter({
history: createMemoryHistory({
initialEntries: ['/0'],
}),
scrollRestoration: true,
routeTree: root.addChildren([route]),
})
}

describe('speed', () => {
let id = 0
let router: ReturnType<typeof createTestRouter>
let unsub = () => {}
let next: () => Promise<void> = () => Promise.reject()

beforeAll(async () => {
id = 0
router = createTestRouter()
let resolveRendered: () => void = () => {}
unsub = router.subscribe('onRendered', () => {
resolveRendered()
})

const navigate = (opts: NavigateOptions) =>
new Promise<void>((resolveNext) => {
resolveRendered = resolveNext
router.navigate(opts)
})

next = () => {
const nextId = String(id++)

return navigate({
to: '/$id',
params: { id: nextId },
search: { id: nextId },
replace: true,
})
}

render(() => <RouterProvider router={router} />)
await router.load()
})

afterAll(() => {
cleanup()
unsub()
})

bench('navigate', () => next(), {
warmupIterations: 1,
iterations: 100,
})
})
Loading
Loading