diff --git a/examples/benchmark-react/README.md b/examples/benchmark-react/README.md index 90b59dd57927..522667542fe5 100644 --- a/examples/benchmark-react/README.md +++ b/examples/benchmark-react/README.md @@ -51,14 +51,14 @@ The repo has two benchmark suites: ## Expected results -Illustrative **relative** results with **SWR = 100%** (baseline). For **throughput** rows, each value is (library ops/s ÷ SWR ops/s) × 100 — **higher is faster**. For **ref-stability** rows, the ratio uses the “refs changed” count — **lower is fewer components that saw a new object reference**. Figures are rounded from the **Latest measured results** table below (network simulation on); absolute ops/s will vary by machine, but **library-to-library ratios** are usually similar. +Illustrative **relative** results with **baseline = 100%** (plain React useState/useEffect, no data library). For **throughput** rows, each value is (library ops/s ÷ baseline ops/s) × 100 — **higher is faster**. For **ref-stability** rows, the ratio uses the “refs changed” count — **lower is fewer components that saw a new object reference**. Figures are rounded from the **Latest measured results** table below (network simulation on); absolute ops/s will vary by machine, but **library-to-library ratios** are usually similar. -| Category | Scenarios (representative) | data-client | tanstack-query | swr | -|---|---|---:|---:|---:| -| Navigation | `getlist-100`, `getlist-500`, `getlist-500-sorted` | ~97% | ~99% | **100%** | -| Navigation | `list-detail-switch-10` | **~468%** | ~98% | 100% | -| Mutations | `update-entity`, `update-user`, `update-entity-sorted`, `update-entity-multi-view`, `unshift-item`, `delete-item`, `move-item` | **~4600%** | ~98% | 100% | -| Scaling (10k items) | `update-user-10000` | **~1953%** | ~82% | 100% | +| Category | Scenarios (representative) | data-client | tanstack-query | swr | baseline | +|---|---|---:|---:|---:|---:| +| Navigation | `getlist-100`, `getlist-500`, `getlist-500-sorted` | ~96% | ~98% | ~99% | **100%** | +| Navigation | `list-detail-switch-10` | **~949%** | ~199% | ~203% | 100% | +| Mutations | `update-entity`, `update-user`, `update-entity-sorted`, `update-entity-multi-view`, `unshift-item`, `delete-item`, `move-item` | **~4486%** | ~96% | ~99% | 100% | +| Scaling (10k items) | `update-user-10000` | **~2006%** | ~84% | ~103% | 100% | ## Latest measured results (network simulation on) @@ -67,22 +67,22 @@ Median ops/s per scenario; range is approximate 95% CI margin from the runner (` Run: **2026-03-21**, Linux (WSL2), `yarn build:benchmark-react`, static preview + `env -u CI npx tsx bench/runner.ts --network-sim true` (all libraries; memory scenarios not included). Numbers are **machine-specific**; use them for relative comparison between libraries, not as absolutes. -| Scenario | data-client | tanstack-query | swr | -|---|---:|---:|---:| -| **Navigation** | | | | -| `getlist-100` | 11.20 ± 0.03 | 11.27 ± 0.02 | 11.43 ± 0.07 | -| `getlist-500` | 9.78 ± 0.12 | 10.01 ± 0.13 | 10.16 ± 0.13 | -| `getlist-500-sorted` | 9.82 ± 0.16 | 10.08 ± 0.13 | 10.21 ± 0.07 | -| `list-detail-switch-10` | 6.93 ± 1.02 | 1.45 ± 0.04 | 1.48 ± 0.08 | -| **Mutations** | | | | -| `update-entity` | 357.14 ± 11.48 | 7.01 ± 0.02 | 7.02 ± 0.02 | -| `update-user` | 333.33 ± 14.44 | 7.01 ± 0.02 | 7.17 ± 0.03 | -| `update-entity-sorted` | 312.50 ± 23.44 | 7.08 ± 0.00 | 7.07 ± 0.03 | -| `update-entity-multi-view` | 357.14 ± 52.30 | 6.82 ± 0.34 | 6.88 ± 0.39 | -| `update-user-10000` | 97.09 ± 7.73 | 4.07 ± 0.02 | 4.97 ± 0.02 | -| `unshift-item` | 285.71 ± 4.90 | 6.92 ± 0.02 | 7.16 ± 0.00 | -| `delete-item` | 312.50 ± 9.77 | 6.93 ± 0.01 | 7.15 ± 0.01 | -| `move-item` | 285.71 ± 10.61 | 6.39 ± 0.02 | 6.83 ± 0.00 | +| Scenario | data-client | tanstack-query | swr | baseline | +|---|---:|---:|---:|---:| +| **Navigation** | | | | | +| `getlist-100` | 11.20 ± 0.03 | 11.27 ± 0.02 | 11.43 ± 0.07 | 11.55 ± 0.02 | +| `getlist-500` | 9.78 ± 0.12 | 10.01 ± 0.13 | 10.16 ± 0.13 | 10.22 ± 0.07 | +| `getlist-500-sorted` | 9.82 ± 0.16 | 10.08 ± 0.13 | 10.21 ± 0.07 | 10.29 ± 0.06 | +| `list-detail-switch-10` | 6.93 ± 1.02 | 1.45 ± 0.04 | 1.48 ± 0.08 | 0.73 ± 0.00 | +| **Mutations** | | | | | +| `update-entity` | 357.14 ± 11.48 | 7.01 ± 0.02 | 7.02 ± 0.02 | 7.22 ± 0.00 | +| `update-user` | 333.33 ± 14.44 | 7.01 ± 0.02 | 7.17 ± 0.03 | 7.22 ± 0.01 | +| `update-entity-sorted` | 312.50 ± 23.44 | 7.08 ± 0.00 | 7.07 ± 0.03 | 7.28 ± 0.01 | +| `update-entity-multi-view` | 357.14 ± 52.30 | 6.82 ± 0.34 | 6.88 ± 0.39 | 7.14 ± 0.36 | +| `update-user-10000` | 97.09 ± 7.73 | 4.07 ± 0.02 | 4.97 ± 0.02 | 4.84 ± 0.03 | +| `unshift-item` | 285.71 ± 4.90 | 6.92 ± 0.02 | 7.16 ± 0.00 | 7.16 ± 0.02 | +| `delete-item` | 312.50 ± 9.77 | 6.93 ± 0.01 | 7.15 ± 0.01 | 7.16 ± 0.01 | +| `move-item` | 285.71 ± 10.61 | 6.39 ± 0.02 | 6.83 ± 0.00 | 6.82 ± 0.00 | [Measured on a Ryzen 9 7950X; 64 GB RAM; Ubuntu (WSL2); Node 24.12.0; Chromium (Playwright)] diff --git a/examples/benchmark-react/bench/scenarios.ts b/examples/benchmark-react/bench/scenarios.ts index a984d5fae3cc..887340777ace 100644 --- a/examples/benchmark-react/bench/scenarios.ts +++ b/examples/benchmark-react/bench/scenarios.ts @@ -186,7 +186,12 @@ const BASE_SCENARIOS: BaseScenario[] = [ }, ]; -export const LIBRARIES = ['data-client', 'tanstack-query', 'swr'] as const; +export const LIBRARIES = [ + 'data-client', + 'tanstack-query', + 'swr', + 'baseline', +] as const; export const SCENARIOS: Scenario[] = LIBRARIES.flatMap(lib => BASE_SCENARIOS.filter( diff --git a/examples/benchmark-react/src/baseline/index.tsx b/examples/benchmark-react/src/baseline/index.tsx new file mode 100644 index 000000000000..3011a175811c --- /dev/null +++ b/examples/benchmark-react/src/baseline/index.tsx @@ -0,0 +1,304 @@ +import { + moveItemIsReady, + renderBenchApp, + useBenchState, +} from '@shared/benchHarness'; +import { + DOUBLE_LIST_STYLE, + IssueRow, + PINNED_STRIP_STYLE, + PinnedCardView, + PlainIssueList, +} from '@shared/components'; +import { + FIXTURE_USERS, + FIXTURE_USERS_BY_LOGIN, + FIXTURE_ISSUES_BY_NUMBER, + sortByTitle, +} from '@shared/data'; +import { setCurrentIssues } from '@shared/refStability'; +import { + fetchIssue, + fetchIssueList, + updateIssue, + updateUser as serverUpdateUser, + createIssue, + deleteIssue, +} from '@shared/server'; +import type { Issue } from '@shared/types'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +function SortedListView({ + limit, + refetchKey, +}: { + limit?: number; + refetchKey: number; +}) { + const [issues, setIssues] = useState(null); + useEffect(() => { + fetchIssueList().then(setIssues); + }, [refetchKey]); + const sorted = useMemo(() => (issues ? sortByTitle(issues) : []), [issues]); + if (!sorted.length) return null; + return ( +
+ +
+ ); +} + +function DetailView({ + number, + refetchKey, +}: { + number: number; + refetchKey: number; +}) { + const [issue, setIssue] = useState(null); + useEffect(() => { + fetchIssue({ number }).then(setIssue); + }, [number, refetchKey]); + if (!issue) return null; + return ( +
+ +
+ ); +} + +function PinnedCard({ + number, + refetchKey, +}: { + number: number; + refetchKey: number; +}) { + const [issue, setIssue] = useState(null); + useEffect(() => { + fetchIssue({ number }).then(setIssue); + }, [number, refetchKey]); + if (!issue) return null; + return ; +} + +function PinnedStrip({ + numbers, + refetchKey, +}: { + numbers: number[]; + refetchKey: number; +}) { + return ( +
+ {numbers.map(n => ( + + ))} +
+ ); +} + +function ListView({ + count, + limit, + refetchKey, +}: { + count: number; + limit?: number; + refetchKey: number; +}) { + const [issues, setIssues] = useState(null); + useEffect(() => { + fetchIssueList({ count }).then(setIssues); + }, [count, refetchKey]); + if (!issues) return null; + setCurrentIssues(issues); + return ; +} + +function StateListView({ + state, + count, + limit, + refetchKey, +}: { + state: string; + count: number; + limit?: number; + refetchKey: number; +}) { + const [issues, setIssues] = useState(null); + useEffect(() => { + fetchIssueList({ state, count }).then(setIssues); + }, [state, count, refetchKey]); + if (!issues) return null; + return ( +
+ {issues.length} + +
+ ); +} + +function DoubleListView({ + count, + limit, + refetchKey, +}: { + count: number; + limit?: number; + refetchKey: number; +}) { + return ( +
+ + +
+ ); +} + +function BenchmarkHarness() { + const [refetchKey, setRefetchKey] = useState(0); + const triggerRefetch = useCallback(() => setRefetchKey(k => k + 1), []); + + const { + listViewCount, + showSortedView, + showDoubleList, + doubleListCount, + detailIssueNumber, + pinnedNumbers, + renderLimit, + containerRef, + measureUpdate, + registerAPI, + } = useBenchState(); + + const updateEntity = useCallback( + (number: number) => { + const issue = FIXTURE_ISSUES_BY_NUMBER.get(number); + if (!issue) return; + measureUpdate(() => + updateIssue({ + number, + title: `${issue.title} (updated)`, + }).then(triggerRefetch), + ); + }, + [measureUpdate, triggerRefetch], + ); + + const updateUser = useCallback( + (login: string) => { + const user = FIXTURE_USERS_BY_LOGIN.get(login); + if (!user) return; + measureUpdate(() => + serverUpdateUser({ + login, + name: `${user.name} (updated)`, + }).then(triggerRefetch), + ); + }, + [measureUpdate, triggerRefetch], + ); + + const unshiftItem = useCallback(() => { + const user = FIXTURE_USERS[0]; + measureUpdate(() => + createIssue({ title: 'New Issue', user }).then(triggerRefetch), + ); + }, [measureUpdate, triggerRefetch]); + + const deleteEntity = useCallback( + (number: number) => { + measureUpdate(() => deleteIssue({ number }).then(triggerRefetch)); + }, + [measureUpdate, triggerRefetch], + ); + + const moveItem = useCallback( + (number: number) => { + measureUpdate( + () => updateIssue({ number, state: 'closed' }).then(triggerRefetch), + () => moveItemIsReady(containerRef, number), + ); + }, + [measureUpdate, triggerRefetch, containerRef], + ); + + const updateEntityMultiView = useCallback( + (number: number) => { + const issue = FIXTURE_ISSUES_BY_NUMBER.get(number); + if (!issue) return; + const expected = `${issue.title} (updated)`; + measureUpdate( + () => updateIssue({ number, title: expected }).then(triggerRefetch), + () => { + const container = containerRef.current!; + const listTitle = container.querySelector( + `[data-issue-number="${number}"] [data-title]`, + ); + const detailTitle = container.querySelector( + '[data-detail-view] [data-title]', + ); + const pinnedTitle = container.querySelector( + `[data-pinned-number="${number}"] [data-title]`, + ); + return [listTitle, detailTitle, pinnedTitle].every( + el => el?.textContent === expected, + ); + }, + ); + }, + [measureUpdate, triggerRefetch, containerRef], + ); + + registerAPI({ + updateEntity, + updateUser, + updateEntityMultiView, + unshiftItem, + deleteEntity, + moveItem, + }); + + return ( +
+ {listViewCount != null && ( + + )} + {showSortedView && ( + + )} + {showDoubleList && doubleListCount != null && ( + + )} + {detailIssueNumber != null && ( + + )} + {pinnedNumbers.length > 0 && ( + + )} +
+ ); +} + +renderBenchApp(BenchmarkHarness); diff --git a/examples/benchmark-react/webpack.config.cjs b/examples/benchmark-react/webpack.config.cjs index ffa8f5868793..91e5f676a9d5 100644 --- a/examples/benchmark-react/webpack.config.cjs +++ b/examples/benchmark-react/webpack.config.cjs @@ -2,7 +2,7 @@ const { makeConfig } = require('@anansi/webpack-config'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); -const LIBRARIES = ['data-client', 'tanstack-query', 'swr']; +const LIBRARIES = ['data-client', 'tanstack-query', 'swr', 'baseline']; const entries = {}; for (const lib of LIBRARIES) {