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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions benchmarks/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ Read the relevant README before running benchmarks. Compare baseline and candida

Use `test:perf:<react|solid|vue>` through Nx. Plain memory runs only smoke-test; memory metrics require CodSpeed. Client navigation benchmarks do not cover Start hydration or client server-function calls.

Run `vitest bench` directly only with `--mode benchmark`, as the Nx targets do. Vitest 5 otherwise uses Vite's `test` mode, where vite-plugin-solid injects a jest-dom setup file that benchmark packages cannot resolve.

For bundle-size work, follow the full optimization skill required by the root guide. Full API fixtures are maintained manually: when public hooks/components change, update matching `bundle-size/scenarios/*-full/src/routes/__root.tsx` fixtures. Save baseline results separately because each run overwrites `bundle-size/results/current.json`; report final gzip deltas and explain remaining growth.
16 changes: 8 additions & 8 deletions benchmarks/client-nav/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ scenarios follow these navigation/mount conventions:
- Scenarios behave like a real user app: navigation happens through `<Link>` 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.
- Apps are mounted and disposed outside timing by `benchLifecycle` (`benchmarks/cpu-simulation.ts`): CodSpeed measures a single invocation, so every invocation gets a freshly mounted app; plain `vitest bench` keeps one app per warmup/run phase. Bench functions must be `async` functions, because CodSpeed's Vitest 5 provider awaits only `AsyncFunction`s.
- 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).
Expand All @@ -102,7 +103,7 @@ Run a single scenario app manually (after building it through Nx):

```bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav-<scenario>-<framework>:build:client --outputStyle=stream --skipRemoteCache
cd benchmarks/client-nav && NODE_ENV=production vitest bench --config ./scenarios/<scenario>/<framework>/vite.config.ts
cd benchmarks/client-nav && NODE_ENV=production vitest bench --mode benchmark --config ./scenarios/<scenario>/<framework>/vite.config.ts
```

Run framework-specific flame benchmarks (10 second loop, profiled with `@platformatic/flame`, forced to `NODE_ENV=production`):
Expand Down Expand Up @@ -178,11 +179,10 @@ task cancellation, and window disposal also run outside measurement. Diagnostic
tests count Link renders or reactive evaluations and check they stop before the
measured region ends; counting is disabled in the timed workload.

CodSpeed uses suite `beforeEach`/`afterEach` hooks. Ordinary Vitest instead
installs Tinybench's public Task iteration hooks from its stage-level `setup`.
Using only stage-level setup would let later iterations reuse an already
hydrated document. The hydration unit tests cover fresh-state setup through
both paths.
The bench's own `beforeEach`/`afterEach` hooks prepare and dispose every
sample outside timing, in both CodSpeed and ordinary Vitest runs. Per-phase
setup would let later iterations reuse an already hydrated document. The
hydration unit tests cover repeated fresh-state setup.

Each scenario is included in its framework's aggregate build and CodSpeed run.
Land the benchmark independently to establish main's baseline before comparing a
Expand All @@ -196,7 +196,7 @@ construction. `scenarios/mount` includes construction but also rendering and
loading. To isolate initialization, use the core construction benchmark:

```bash
CI=1 NX_DAEMON=false pnpm nx run @tanstack/router-core:test:unit --outputStyle=stream --skipRemoteCache -- bench tests/route-tree-construction.bench.ts --run --testNamePattern=1000.routes --outputJson /tmp/route-tree-construction.json
CI=1 NX_DAEMON=false pnpm nx run @tanstack/router-core:test:unit --outputStyle=stream --skipRemoteCache -- bench tests/route-tree-construction.bench.ts --run --testNamePattern=1000.routes --reporter=json --outputFile=/tmp/route-tree-construction.json
```

It covers 100, 1,000, and 10,000 static, dynamic, nested, or mixed routes.
Expand All @@ -220,7 +220,7 @@ CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:cl
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:ssr --outputStyle=stream --skipRemoteCache -- --run

# Select a feature and save the normal Vitest JSON report.
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:client --outputStyle=stream --skipRemoteCache -- --run -t "updater|optional|splat" --outputJson /tmp/link-perf.json
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/react-link-performance:test:perf:client --outputStyle=stream --skipRemoteCache -- --run -t "updater|optional|splat" --reporter=json --outputFile=/tmp/link-perf.json
```

The cases cover repeated versus unique destination params, updater functions,
Expand Down
17 changes: 8 additions & 9 deletions benchmarks/client-nav/link-performance/client.bench.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, bench, describe } from 'vitest'
import { test } from 'vitest'
import { benchLifecycle } from '../../cpu-simulation'
import { LINK_CASES } from './cases'
import { createClientScenario, samplingOptions } from './scenario'
import type * as App from './src/client'
Expand All @@ -10,7 +11,7 @@ if (app.serverEnvironment !== false) {
}

for (const { id, label } of LINK_CASES) {
describe(label, () => {
test(label, async ({ bench }) => {
const scenario = createClientScenario(app, id)
function finish() {
try {
Expand All @@ -19,13 +20,11 @@ for (const { id, label } of LINK_CASES) {
scenario.teardown()
}
}
beforeEach(scenario.setup)
afterEach(finish)

bench(`client Links: ${id}`, scenario.batch, {
...samplingOptions,
setup: scenario.setup,
teardown: finish,
})
await bench(
`client Links: ${id}`,
benchLifecycle({ before: scenario.setup, after: finish }),
scenario.batch,
).run(samplingOptions)
})
}
4 changes: 2 additions & 2 deletions benchmarks/client-nav/link-performance/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"cache": false,
"dependsOn": ["build:client"],
"options": {
"command": "NODE_ENV=production vitest bench --config ./link-performance/vite.client.config.ts",
"command": "NODE_ENV=production vitest bench --mode benchmark --config ./link-performance/vite.client.config.ts",
"cwd": "benchmarks/client-nav"
}
},
Expand All @@ -45,7 +45,7 @@
"cache": false,
"dependsOn": ["build:ssr"],
"options": {
"command": "NODE_ENV=production vitest bench --config ./link-performance/vite.ssr.config.ts",
"command": "NODE_ENV=production vitest bench --mode benchmark --config ./link-performance/vite.ssr.config.ts",
"cwd": "benchmarks/client-nav"
}
},
Expand Down
17 changes: 8 additions & 9 deletions benchmarks/client-nav/link-performance/ssr.bench.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, bench, describe } from 'vitest'
import { test } from 'vitest'
import { benchLifecycle } from '../../cpu-simulation'
import { LINK_CASES } from './cases'
import { createSsrScenario, samplingOptions } from './scenario'
import type * as App from './src/ssr'
Expand All @@ -10,7 +11,7 @@ if (app.serverEnvironment !== true) {
}

for (const { id, label } of LINK_CASES) {
describe(label, () => {
test(label, async ({ bench }) => {
const scenario = createSsrScenario(app, id)
function finish() {
try {
Expand All @@ -19,13 +20,11 @@ for (const { id, label } of LINK_CASES) {
scenario.teardown()
}
}
beforeEach(scenario.setup)
afterEach(finish)

bench(`SSR Links: ${id}`, scenario.batch, {
...samplingOptions,
setup: scenario.setup,
teardown: finish,
})
await bench(
`SSR Links: ${id}`,
benchLifecycle({ before: scenario.setup, after: finish }),
scenario.batch,
).run(samplingOptions)
})
}
10 changes: 5 additions & 5 deletions benchmarks/client-nav/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"test:flame:react": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./react/speed.flame.ts",
"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 ./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:perf": "NODE_ENV=production vitest bench --mode benchmark --config ./vitest.config.ts",
"test:perf:react": "NODE_ENV=production vitest bench --mode benchmark --config ./vitest.react.config.ts",
"test:perf:solid": "NODE_ENV=production vitest bench --mode benchmark --config ./vitest.solid.config.ts",
"test:perf:vue": "NODE_ENV=production vitest bench --mode benchmark --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",
Expand All @@ -31,7 +31,7 @@
},
"devDependencies": {
"@platformatic/flame": "^1.6.0",
"@codspeed/vitest-plugin": "^5.7.1",
"@codspeed/vitest-plugin": "^6.0.1",
"@tanstack/router-plugin": "workspace:^",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^6.0.1",
Expand Down
35 changes: 10 additions & 25 deletions benchmarks/client-nav/react/speed.bench.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
import { afterAll, beforeAll, bench, describe } from 'vitest'
import { test } from 'vitest'
import { setup } from './setup'

describe('client-nav', () => {
const test = setup()
test('client-nav', async ({ bench }) => {
const scenario = 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(
await bench(
'client-side navigation loop (react)',
{ beforeAll: scenario.before, afterAll: scenario.after },
async () => {
for (let i = 0; i < 10; i++) {
await test.tick()
await scenario.tick()
}
},
{
warmupIterations: 100,
time: 10_000,
setup: test.before,
teardown: test.after,
},
)
).run({
warmupIterations: 100,
time: 10_000,
})
})
1 change: 1 addition & 0 deletions benchmarks/client-nav/react/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ export default defineConfig({
watch: false,
environment: 'jsdom',
setupFiles: [`${rootDir}vitest.setup.ts`],
benchmark: { include: ['react/speed.bench.ts'] },
},
})
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import { afterEach, beforeEach, bench, describe } from 'vitest'
import { test } from 'vitest'
import { benchLifecycle } from '../../../../cpu-simulation'
import { benchOptions, ticksPerIteration } from '../shared'
import { setup } from './setup'

describe('client-async-pipeline', () => {
const test = setup()
test('client-async-pipeline', async ({ bench }) => {
const scenario = setup()

/**
* Running `vitest bench` ignores suite hooks, so Tinybench uses the `setup`
* and `teardown` options below. CodSpeed bypasses those options but supports
* `beforeEach` and `afterEach`. Only one lifecycle path runs in either
* environment, and every measured invocation receives fresh application state.
*/

beforeEach(test.before)
afterEach(test.after)

bench(
await bench(
'client-async-pipeline navigation loop (react)',
benchLifecycle(scenario),
async () => {
for (let i = 0; i < ticksPerIteration; i++) {
await test.tick()
await scenario.tick()
}
await test.finishBatch()
},
{
...benchOptions,
setup: test.before,
teardown: test.after,
await scenario.finishBatch()
},
)
).run(benchOptions)
})
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import { afterEach, beforeEach, bench, describe } from 'vitest'
import { test } from 'vitest'
import { benchLifecycle } from '../../../../cpu-simulation'
import { benchOptions, ticksPerIteration } from '../shared'
import { setup } from './setup'

describe('client-async-pipeline', () => {
const test = setup()
test('client-async-pipeline', async ({ bench }) => {
const scenario = setup()

/**
* Running `vitest bench` ignores suite hooks, so Tinybench uses the `setup`
* and `teardown` options below. CodSpeed bypasses those options but supports
* `beforeEach` and `afterEach`. Only one lifecycle path runs in either
* environment, and every measured invocation receives fresh application state.
*/

beforeEach(test.before)
afterEach(test.after)

bench(
await bench(
'client-async-pipeline navigation loop (solid)',
benchLifecycle(scenario),
async () => {
for (let i = 0; i < ticksPerIteration; i++) {
await test.tick()
await scenario.tick()
}
await test.finishBatch()
},
{
...benchOptions,
setup: test.before,
teardown: test.after,
await scenario.finishBatch()
},
)
).run(benchOptions)
})
31 changes: 9 additions & 22 deletions benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import { afterEach, beforeEach, bench, describe } from 'vitest'
import { test } from 'vitest'
import { benchLifecycle } from '../../../../cpu-simulation'
import { benchOptions, ticksPerIteration } from '../shared'
import { setup } from './setup'

describe('client-async-pipeline', () => {
const test = setup()
test('client-async-pipeline', async ({ bench }) => {
const scenario = setup()

/**
* Running `vitest bench` ignores suite hooks, so Tinybench uses the `setup`
* and `teardown` options below. CodSpeed bypasses those options but supports
* `beforeEach` and `afterEach`. Only one lifecycle path runs in either
* environment, and every measured invocation receives fresh application state.
*/

beforeEach(test.before)
afterEach(test.after)

bench(
await bench(
'client-async-pipeline navigation loop (vue)',
benchLifecycle(scenario),
async () => {
for (let i = 0; i < ticksPerIteration; i++) {
await test.tick()
await scenario.tick()
}
await test.finishBatch()
},
{
...benchOptions,
setup: test.before,
teardown: test.after,
await scenario.finishBatch()
},
)
).run(benchOptions)
})
31 changes: 9 additions & 22 deletions benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import { afterEach, beforeEach, bench, describe } from 'vitest'
import { test } from 'vitest'
import { benchLifecycle } from '../../../../cpu-simulation'
import { benchOptions, ticksPerIteration } from '../shared'
import { setup } from './setup'

describe('client-control-flow', () => {
const test = setup()
test('client-control-flow', async ({ bench }) => {
const scenario = setup()

/**
* Running `vitest bench` ignores suite hooks, so Tinybench uses the `setup`
* and `teardown` options below. CodSpeed bypasses those options but supports
* `beforeEach` and `afterEach`. Only one lifecycle path runs in either
* environment, and every measured invocation receives fresh application state.
*/

beforeEach(test.before)
afterEach(test.after)

bench(
await bench(
'client-control-flow navigation loop (react)',
benchLifecycle(scenario),
async () => {
for (let i = 0; i < ticksPerIteration; i++) {
await test.tick()
await scenario.tick()
}
await test.finishBatch()
},
{
...benchOptions,
setup: test.before,
teardown: test.after,
await scenario.finishBatch()
},
)
).run(benchOptions)
})
Loading
Loading