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
46 changes: 23 additions & 23 deletions examples/benchmark-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)]

Expand Down
7 changes: 6 additions & 1 deletion examples/benchmark-react/bench/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
304 changes: 304 additions & 0 deletions examples/benchmark-react/src/baseline/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Issue[] | null>(null);
useEffect(() => {
fetchIssueList().then(setIssues);
}, [refetchKey]);
const sorted = useMemo(() => (issues ? sortByTitle(issues) : []), [issues]);
if (!sorted.length) return null;
return (
<div data-sorted-list>
<PlainIssueList issues={sorted} limit={limit} />
</div>
);
}

function DetailView({
number,
refetchKey,
}: {
number: number;
refetchKey: number;
}) {
const [issue, setIssue] = useState<Issue | null>(null);
useEffect(() => {
fetchIssue({ number }).then(setIssue);
}, [number, refetchKey]);
if (!issue) return null;
return (
<div data-detail-view data-issue-number={number}>
<IssueRow issue={issue} />
</div>
);
}

function PinnedCard({
number,
refetchKey,
}: {
number: number;
refetchKey: number;
}) {
const [issue, setIssue] = useState<Issue | null>(null);
useEffect(() => {
fetchIssue({ number }).then(setIssue);
}, [number, refetchKey]);
if (!issue) return null;
return <PinnedCardView issue={issue} />;
}

function PinnedStrip({
numbers,
refetchKey,
}: {
numbers: number[];
refetchKey: number;
}) {
return (
<div data-pinned-strip style={PINNED_STRIP_STYLE}>
{numbers.map(n => (
<PinnedCard key={n} number={n} refetchKey={refetchKey} />
))}
</div>
);
}

function ListView({
count,
limit,
refetchKey,
}: {
count: number;
limit?: number;
refetchKey: number;
}) {
const [issues, setIssues] = useState<Issue[] | null>(null);
useEffect(() => {
fetchIssueList({ count }).then(setIssues);
}, [count, refetchKey]);
if (!issues) return null;
setCurrentIssues(issues);
return <PlainIssueList issues={issues} limit={limit} />;
}

function StateListView({
state,
count,
limit,
refetchKey,
}: {
state: string;
count: number;
limit?: number;
refetchKey: number;
}) {
const [issues, setIssues] = useState<Issue[] | null>(null);
useEffect(() => {
fetchIssueList({ state, count }).then(setIssues);
}, [state, count, refetchKey]);
if (!issues) return null;
return (
<div data-state-list={state}>
<span data-state-count>{issues.length}</span>
<PlainIssueList issues={issues} limit={limit} />
</div>
);
}

function DoubleListView({
count,
limit,
refetchKey,
}: {
count: number;
limit?: number;
refetchKey: number;
}) {
return (
<div style={DOUBLE_LIST_STYLE}>
<StateListView
state="open"
count={count}
limit={limit}
refetchKey={refetchKey}
/>
<StateListView
state="closed"
count={count}
limit={limit}
refetchKey={refetchKey}
/>
</div>
);
}

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 (
<div ref={containerRef} data-bench-harness>
{listViewCount != null && (
<ListView
count={listViewCount}
limit={renderLimit}
refetchKey={refetchKey}
/>
)}
{showSortedView && (
<SortedListView limit={renderLimit} refetchKey={refetchKey} />
)}
{showDoubleList && doubleListCount != null && (
<DoubleListView
count={doubleListCount}
limit={renderLimit}
refetchKey={refetchKey}
/>
)}
{detailIssueNumber != null && (
<DetailView number={detailIssueNumber} refetchKey={refetchKey} />
)}
{pinnedNumbers.length > 0 && (
<PinnedStrip numbers={pinnedNumbers} refetchKey={refetchKey} />
)}
</div>
);
}

renderBenchApp(BenchmarkHarness);
2 changes: 1 addition & 1 deletion examples/benchmark-react/webpack.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading