Skip to content

Commit e6f05d7

Browse files
committed
docs: Improve benchmark explanation clarity
1 parent b75d858 commit e6f05d7

File tree

3 files changed

+100
-15
lines changed

3 files changed

+100
-15
lines changed

docs/core/concepts/performance.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
1111
</head>
1212

1313

14-
[Normalized caching](./normalization.md) with entity-level memoization enables
14+
In addition to the data integirty benefits, [normalized caching](./normalization.md) with entity-level memoization enables
1515
significant performance gains for rich interactive applications.
1616

17-
1817
## React rendering benchmarks
1918

20-
Full rendering pipeline (fetch through paint) measured in a real browser via Playwright.
19+
Full rendering pipeline (fetch through DOM commit) measured in a real browser via Playwright.
2120
React baseline uses useEffect + useState from the React docs.
2221

2322
<center>
@@ -39,7 +38,10 @@ sources={{
3938
- **Mutation Propagation**: One store write updates every view that references the entity.
4039
- **Scaling**: Mutations with 10k items in the list rendered.
4140

42-
41+
These benchmarks measure the framework's impact within the larger system. That
42+
makes them most useful as comparisons between approaches, rather than as
43+
absolute measurements of an application's overall performance. We use them to
44+
guide library optimizations and catch performance regressions over time.
4345

4446
## Normalization benchmarks
4547

examples/benchmark-react/AGENTS.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# React Rendering Benchmark
2+
3+
Browser benchmark comparing `@data-client/react`, TanStack Query, SWR, and plain React. Webpack build, Playwright runner. See `README.md` for methodology and running instructions.
4+
5+
## Build & Run
6+
7+
```bash
8+
yarn build:benchmark-react # from repo root
9+
yarn workspace example-benchmark-react preview & # serve dist/ on port 5173
10+
cd examples/benchmark-react && yarn bench # all libs (local) or data-client only (CI)
11+
```
12+
13+
Filtering: `yarn bench --lib data-client --size small --action update`
14+
15+
## Architecture
16+
17+
**Runner → `window.__BENCH__` → React**: `bench/runner.ts` opens `localhost:5173/<lib>/` in Playwright, calls `BenchAPI` methods on `window.__BENCH__`, waits for `[data-bench-complete]` attribute, then collects `performance.measure` entries. This is the only runner↔app channel.
18+
19+
**Web Worker server**: All "network" goes to an in-memory Worker (`server.worker.ts` via `server.ts` RPC) with configurable latency. Keeps fake-server work off main thread.
20+
21+
**Shared vs library-specific**: `src/shared/` (harness, components, fixtures, resources, server) is identical across all apps. Each `src/<lib>/index.tsx` only contains data-layer wiring. Divergence from shared code breaks fairness.
22+
23+
**Webpack multi-entry**: `webpack.config.cjs` produces four apps at `dist/<lib>/index.html`. `@shared` path alias configured in Webpack + `tsconfig.json`.
24+
25+
## Key Design Decisions
26+
27+
- **MutationObserver timing**: `measureMount`/`measureUpdate` in `benchHarness.tsx` use `MutationObserver` on `[data-bench-harness]`, not React lifecycle. Mount waits for `[data-bench-item]`/`[data-sorted-list]`. Update triggers on first mutation batch, or waits for `isReady` predicate on multi-phase updates.
28+
- **Proxy API**: `window.__BENCH__` is a `Proxy``apiRef.current`. `registerAPI` merges library actions with shared defaults. Methods always reflect current React state; adding new `BenchAPI` methods needs no registration boilerplate.
29+
- **renderLimit**: Update scenarios store 1000 items but render only 100 — isolates cache-propagation cost from DOM reconciliation.
30+
- **Expensive UserView**: `components.tsx` `UserView` does deliberate hash/string/date work. Libraries preserving referential equality skip it on unrelated updates; others pay per row.
31+
- **BenchGCPolicy**: data-client's custom `GCPolicy` — zero expiry, no interval timer. Prevents GC during timing; `sweep()` called explicitly for memory scenarios.
32+
33+
## Scenario System
34+
35+
`BASE_SCENARIOS` in `bench/scenarios.ts` × `LIBRARIES` via `flatMap`. `onlyLibs` restricts to specific libs. CI runs data-client hot-path only (no memory/startup/deterministic). Memory is opt-in locally (`--action memory`). Convergent timing uses single page load with adaptive iterations and early stopping on statistical convergence. Ref-stability scenarios run once (deterministic count, not ops/s).
36+
37+
## Update Data Flow
38+
39+
1. Runner calls `window.__BENCH__.updateEntity(1)`
40+
2. `measureUpdate` marks `update-start`, invokes action, `MutationObserver` detects DOM change, marks `update-end` + sets `data-bench-complete`
41+
3. Runner reads `performance.measure('update-duration')`
42+
4. **Core asymmetry**: data-client propagates via one store write; TanStack Query/SWR/baseline invalidate + re-fetch from Worker
43+
44+
## Adding / Modifying
45+
46+
**New scenario**: Add to `BASE_SCENARIOS` → add action to `BenchAPI` in `types.ts` if new → implement in each `src/<lib>/index.tsx` (or use `onlyLibs`) → set `preMountAction`/`mountCount` if setup needed.
47+
48+
**New library**: `src/<lib>/index.tsx` using `registerAPI` → add to `LIBRARIES` in `scenarios.ts` → webpack entry + `HtmlWebpackPlugin``package.json` dep.
49+
50+
**Shared components**: Changes to `components.tsx` or `resources.ts` shift all four libraries equally (by design).
51+
52+
## Data Attributes
53+
54+
| Attribute | Flow | Purpose |
55+
|---|---|---|
56+
| `data-app-ready` | harness → runner | `__BENCH__` available |
57+
| `data-bench-harness` | lib → runner | Container for MutationObserver |
58+
| `data-bench-complete` | harness → runner | Iteration finished |
59+
| `data-bench-timeout` | harness → runner | 30s timeout (error) |
60+
| `data-bench-item` | components → harness | Mount detection |
61+
| `data-sorted-list` | lib views → harness | Sorted-view mount detection |
62+
| `data-detail-view` | lib views → harness | Multi-view detection |
63+
| `data-issue-number` | components → runner/harness | Item identity assertion |
64+
| `data-title` | components → lib views | Text content assertion |
65+
| `data-state-list` | lib views → harness | Move-item verification |
66+
67+
## Environment Variables
68+
69+
| Variable | Effect |
70+
|---|---|
71+
| `CI` | data-client hot-path only; tighter convergence |
72+
| `REACT_COMPILER=false` | Disables React Compiler at build |
73+
| `BENCH_LABEL=<tag>` | Appends `[<tag>]` to result names |
74+
| `BENCH_PORT` | Preview port (default 5173) |
75+
| `BENCH_TRACE=true` | Chrome tracing for duration scenarios |

examples/benchmark-react/README.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
# React Rendering Benchmark
22

3-
Browser-based benchmark comparing `@data-client/react`, TanStack Query, and SWR on mount/update scenarios. Built with Webpack via `@anansi/webpack-config`. Results are reported to CI via `rhysd/github-action-benchmark`.
3+
Browser-based benchmark for `@data-client/react` measuring mount/update scenarios. Includes TanStack Query, SWR, and a plain-React baseline for reference. Built with Webpack via `@anansi/webpack-config`. Results are reported to CI via `rhysd/github-action-benchmark`.
44

55
## Comparison to Node benchmarks
66

77
The repo has two benchmark suites:
88

99
- **`examples/benchmark`** (Node) — Measures the JS engine only: `normalize`/`denormalize`, `Controller.setResponse`/`getResponse`, reducer throughput. No browser, no React. Use it to validate core and normalizr changes.
10-
- **`examples/benchmark-react`** (this app) — Measures the full React rendering pipeline: same operations driven in a real browser, with layout and paint. Use it to validate `@data-client/react` and compare against other data libraries.
10+
- **`examples/benchmark-react`** (this app) — Measures the full React rendering pipeline: same operations driven in a real browser, with layout and paint. Use it to validate `@data-client/react` changes; other libraries are included for reference.
1111

1212
## Methodology
1313

1414
- **What we measure:** Wall-clock time from triggering an action (e.g. `init(100)` or `updateUser('user0')`) until a MutationObserver detects the expected DOM change in the benchmark container. Optionally we also record React Profiler commit duration and, with `BENCH_TRACE=true`, Chrome trace duration.
15-
- **Why:** Normalized caching should show wins on shared-entity updates (one store write, many components update), ref stability (fewer new object references), and derived-view memoization (`Query` schema avoids re-sorting when entities haven't changed). See [js-framework-benchmark "How the duration is measured"](https://github.com/krausest/js-framework-benchmark/wiki/How-the-duration-is-measured) for a similar timeline-based approach.
15+
- **Why:** Scenarios are chosen to exercise areas where caching strategies differ: shared-entity updates, referential stability, and derived-view memoization. See [js-framework-benchmark "How the duration is measured"](https://github.com/krausest/js-framework-benchmark/wiki/How-the-duration-is-measured) for a similar timeline-based approach.
1616
- **Statistical:** Warmup runs are discarded; we report median and 95% CI (as percentage of median). Timing scenarios (navigation and mutation) use **convergent mode**: a single page load per scenario, with warmup iterations followed by adaptive measurement iterations where each iteration produces one sample and convergence is checked inline. This eliminates page-reload overhead between samples for faster, lower-variance results. Deterministic scenarios (ref-stability) run once. Memory scenarios use a separate outer loop with a fresh page per round.
1717
- **No CPU throttling:** Runs at native speed with more samples for statistical significance rather than artificial slowdown. Convergent timing scenarios use 5 warmup + up to 50 measurement iterations (small) or 3 warmup + up to 40 (large). Early stopping triggers when 95% CI margin drops below the target percentage.
1818

19+
## Comparison philosophy
20+
21+
The primary purpose is to track data-client's own performance — catch regressions and validate improvements. Other libraries are included for context; CI runs data-client only.
22+
23+
Scenarios are designed to isolate the data framework layer: fetching, caching, update propagation, and rendering in response to data changes. Real-world applications will have additional performance considerations (routing, animation, third-party scripts, etc.) beyond what is measured here.
24+
25+
All implementations share presentational components, fixture data, fetch functions, and the `useBenchState` harness. They only diverge where each library's data layer requires it, using idiomatic patterns from that library's documentation. No implementation builds custom state management on top of its library.
26+
1927
## Scenario categories
2028

21-
- **Hot path (in CI, data-client only)** — JS-only: init (fetch + render), update propagation, ref-stability, sorted-view. No simulated network. CI runs only `data-client` scenarios to track our own regressions; competitor libraries are benchmarked locally for comparison.
22-
- **With network (local comparison)** — Same shared-author update but with simulated network delay (consistent ms per "request"). Used to compare overfetching: data-client needs one store update (1 × delay); non-normalized libs typically invalidate/refetch multiple queries (N × delay). **Not run in CI** — run locally with `yarn bench` (no `CI` env) to include these.
29+
- **Hot path (in CI, data-client only)** — JS-only: init (fetch + render), update propagation, ref-stability, sorted-view. No simulated network. CI runs only data-client scenarios to track regressions; other libraries are benchmarked locally.
30+
- **With network (local)** — Same shared-author update but with simulated network delay (consistent ms per "request"). Normalized caches propagate via a single store update; query-keyed caches invalidate and refetch affected queries. **Not run in CI** — run locally with `yarn bench` (no `CI` env) to include these.
2331
- **Memory (local only)** — Heap delta after repeated mount/unmount cycles.
2432
- **Startup (local only)** — FCP and task duration via CDP `Performance.getMetrics`.
2533

@@ -28,17 +36,17 @@ The repo has two benchmark suites:
2836
**Hot path (CI)**
2937

3038
- **Get list** (`getlist-100`, `getlist-500`) — Time to show a ListView component that auto-fetches 100 or 500 issues from the list endpoint, then renders (unit: ops/s). Exercises the full fetch + normalization + render pipeline.
31-
- **Get list sorted** (`getlist-500-sorted`) — Mount 500 issues through a sorted/derived view. data-client uses `useQuery(sortedIssuesQuery)` with `Query` schema memoization; competitors use `useMemo` + sort.
39+
- **Get list sorted** (`getlist-500-sorted`) — Mount 500 issues through a sorted/derived view. data-client uses `useQuery(sortedIssuesQuery)` with `Query` schema memoization; other libraries use `useMemo` + sort.
3240
- **Update entity** (`update-entity`) — Time to update one issue and propagate to the UI (unit: ops/s).
3341
- **Update entity sorted** (`update-entity-sorted`) — After mounting a sorted view, update one entity. data-client's `Query` memoization avoids re-sorting when sort keys are unchanged.
34-
- **Update entity multi-view** (`update-entity-multi-view`) — Update one issue that appears simultaneously in a list, a detail panel, and a pinned-cards strip. Exercises cross-query entity propagation: normalized cache updates once and all three views reflect the change; non-normalized libraries must invalidate and refetch each query independently.
35-
- **Update user (scaling)** (`update-user`, `update-user-10000`) — Update one shared user with 1,000 or 10,000 mounted issues to test subscriber scaling. Normalized cache: one store update, all views of that user update.
36-
- **Ref-stability** (`ref-stability-issue-changed`, `ref-stability-user-changed`) — Count of components that received a **new** object reference after an update (unit: count; smaller is better). Normalization keeps referential equality for unchanged entities.
42+
- **Update entity multi-view** (`update-entity-multi-view`) — Update one issue that appears simultaneously in a list, a detail panel, and a pinned-cards strip. Normalized caches propagate via a single store write; query-keyed caches invalidate and refetch each query.
43+
- **Update user (scaling)** (`update-user`, `update-user-10000`) — Update one shared user with 1,000 or 10,000 mounted issues to test subscriber scaling.
44+
- **Ref-stability** (`ref-stability-issue-changed`, `ref-stability-user-changed`) — Count of components that received a **new** object reference after an update (unit: count; smaller is better).
3745
- **Invalidate and resolve** (`invalidate-and-resolve`) — data-client only; invalidates a cached endpoint and immediately re-resolves. Measures Suspense boundary round-trip.
3846

3947
**With network (local comparison)**
4048

41-
- **Update shared user with network** (`update-shared-user-with-network`) — Same as above with a simulated delay (e.g. 50 ms) per "request." data-client propagates via normalization (no extra request); other libs invalidate/refetch the list endpoint.
49+
- **Update shared user with network** (`update-shared-user-with-network`) — Same as above with a simulated delay (e.g. 50 ms) per "request."
4250

4351
**Memory (local only)**
4452

@@ -99,7 +107,7 @@ Regressions >5% on stable scenarios or >15% on volatile scenarios are worth inve
99107
## Interpreting results
100108

101109
- **Higher is better** for throughput (ops/s). **Lower is better** for ref-stability counts and heap delta (bytes).
102-
- **Ref-stability:** data-client's normalized cache keeps referential equality for unchanged entities, so `issueRefChanged` and `userRefChanged` should stay low. Non-normalized libs typically show higher counts because they create new object references for every cache write.
110+
- **Ref-stability:** `issueRefChanged` and `userRefChanged` count how many components received a new object reference. Normalized caches preserve referential equality for unchanged entities; query-keyed caches typically create new references on each cache write.
103111
- **React commit:** Reported as `(react commit)` suffix entries. These measure React Profiler `actualDuration` and isolate React reconciliation cost from layout/paint.
104112
- **Report viewer:** Toggle the "Base metrics", "React commit", and "Trace" checkboxes to filter the comparison table. Use "Load history" to compare multiple runs over time.
105113

0 commit comments

Comments
 (0)