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
8 changes: 8 additions & 0 deletions benchmarks/client-nav/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ 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 framework-specific flame benchmarks (10 second loop, profiled with `@platformatic/flame`, forced to `NODE_ENV=production`):
Comment thread
Sheraff marked this conversation as resolved.

```bash
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
```

Typecheck benchmark sources:

```bash
Expand Down
48 changes: 48 additions & 0 deletions benchmarks/client-nav/jsdom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { JSDOM } from 'jsdom'

const dom = new JSDOM('<!doctype html><html><body></body></html>', {
url: 'http://localhost/',
})

const { window } = dom

function setGlobal(name: string, value: unknown) {
Object.defineProperty(globalThis, name, {
value,
configurable: true,
writable: true,
})
}

setGlobal('window', window)
setGlobal('document', window.document)
setGlobal('self', window)
setGlobal('navigator', window.navigator)
setGlobal('location', window.location)
setGlobal('history', window.history)
setGlobal('HTMLElement', window.HTMLElement)
setGlobal('Element', window.Element)
setGlobal('SVGElement', window.SVGElement)
setGlobal('DocumentFragment', window.DocumentFragment)
setGlobal('Node', window.Node)
setGlobal('MutationObserver', window.MutationObserver)
setGlobal('sessionStorage', window.sessionStorage)
setGlobal('localStorage', window.localStorage)
setGlobal('getComputedStyle', window.getComputedStyle.bind(window))

setGlobal(
'requestAnimationFrame',
window.requestAnimationFrame?.bind(window) ??
((callback: (time: number) => void) =>
setTimeout(() => callback(performance.now()), 16)),
)

setGlobal(
'cancelAnimationFrame',
window.cancelAnimationFrame?.bind(window) ??
((handle: number) => clearTimeout(handle)),
)

window.scrollTo = () => {}

export { window }
55 changes: 45 additions & 10 deletions benchmarks/client-nav/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
"build:react": "vite build --config ./react/vite.config.ts",
"build:solid": "vite build --config ./solid/vite.config.ts",
"build:vue": "vite build --config ./vue/vite.config.ts",
"test:perf": "vitest bench",
"test:perf:react": "vitest bench --config ./react/vite.config.ts ./react/speed.bench.tsx",
"test:perf:solid": "vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.tsx",
"test:perf:vue": "vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.tsx",
"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 ./react/vite.config.ts ./react/speed.bench.ts",
"test:perf:solid": "NODE_ENV=production vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.ts",
"test:perf:vue": "NODE_ENV=production vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.ts",
"test:types": "pnpm run test:types:react && pnpm run test:types:solid && pnpm run test:types:vue",
"test:types:react": "tsc -p ./react/tsconfig.json --noEmit",
"test:types:solid": "tsc -p ./solid/tsconfig.json --noEmit",
Expand All @@ -26,13 +29,13 @@
"vue": "^3.5.16"
},
"devDependencies": {
"@platformatic/flame": "^1.6.0",
"@codspeed/vitest-plugin": "^5.0.1",
"@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",
"@types/jsdom": "28.0.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare jsdom runtime dependency for flame benchmarks

The new flame harness now imports jsdom at runtime (benchmarks/client-nav/jsdom.ts), but this package only adds @types/jsdom here and never declares jsdom itself. This works only as long as a parent workspace happens to provide jsdom; in filtered/isolated installs (for example, running only @benchmarks/client-nav tasks) the flame scripts can fail with ERR_MODULE_NOT_FOUND when loading ../jsdom.ts.

Useful? React with 👍 / 👎.

"typescript": "^5.7.2",
"vite": "^7.3.1",
"vite-plugin-solid": "^2.11.10",
Expand All @@ -43,30 +46,62 @@
"build:react": {
"cache": false,
"dependsOn": [
"^build"
{
"projects": [
"@tanstack/react-router"
],
"target": "build"
}
]
},
"build:solid": {
"cache": false,
"dependsOn": [
"^build"
{
"projects": [
"@tanstack/solid-router"
],
"target": "build"
}
]
},
"build:vue": {
"cache": false,
"dependsOn": [
"^build"
{
"projects": [
"@tanstack/vue-router"
],
"target": "build"
}
]
},
"test:perf": {
"cache": false,
"dependsOn": [
"^build",
"build:react",
"build:solid",
"build:vue"
]
},
"test:flame:react": {
"cache": false,
"dependsOn": [
"build:react"
]
},
"test:flame:solid": {
"cache": false,
"dependsOn": [
"build:solid"
]
},
"test:flame:vue": {
"cache": false,
"dependsOn": [
"build:vue"
]
},
"test:perf:react": {
"cache": false,
"dependsOn": [
Expand Down
68 changes: 68 additions & 0 deletions benchmarks/client-nav/react/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { NavigateOptions } from '@tanstack/router-core'
import type * as App from './app'
import type { Root } from 'react-dom/client'

const appModulePath = './dist/app.js'
const { createTestRouter } = (await import(appModulePath)) as typeof App

export function setup() {
if (process.env.NODE_ENV !== 'production') {
console.warn(
'client-nav benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
)
}
let id = 0
let root: Root | undefined = undefined
Comment thread
Sheraff marked this conversation as resolved.
let container: HTMLDivElement | undefined = undefined
let unsub = () => {}
let next: () => Promise<void> = () => Promise.reject('Test not initialized')

async function before() {
id = 0
const { router, component } = createTestRouter()
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 },
// update search every 2 navigations, to still test them, but also measure the impact of granular re-rendering
search: { id: Math.floor(nextId / 2) },
replace: true,
})
}

const { createRoot } = await import('react-dom/client')

container = document.createElement('div')
document.body.append(container)
root = createRoot(container)
root.render(component)
await router.load()
}

function after() {
root?.unmount()
container?.remove()
unsub()
}

function tick() {
return next()
}

return {
before,
tick,
after,
}
}
34 changes: 34 additions & 0 deletions benchmarks/client-nav/react/speed.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { afterAll, beforeAll, bench, describe } from 'vitest'
import { setup } from './setup'

describe('client-nav', () => {
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-side navigation loop (react)',
async () => {
for (let i = 0; i < 10; i++) {
await test.tick()
}
},
{
warmupIterations: 100,
time: 10_000,
setup: test.before,
teardown: test.after,
},
)
})
75 changes: 0 additions & 75 deletions benchmarks/client-nav/react/speed.bench.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions benchmarks/client-nav/react/speed.flame.ts
Original file line number Diff line number Diff line change
@@ -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()
}
10 changes: 9 additions & 1 deletion benchmarks/client-nav/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"jsxImportSource": "react",
"types": ["node", "vite/client", "vitest/globals"]
},
"include": ["speed.bench.tsx", "vite.config.ts", "../vitest.setup.ts"]
"include": [
"speed.bench.ts",
"speed.flame.ts",
"../jsdom.ts",
"setup.ts",
"vite.config.ts",
"../vitest.setup.ts"
]
}
Loading
Loading