Skip to content

Commit 6b79752

Browse files
ntuckercursoragent
andauthored
demo(benchmark-react): reduce variance with in-page sub-iterations (#3811)
* demo(benchmark-react): reduce CI benchmark variance below ±10 - Increase warmup and measurement iterations for CI (small: 5+25, large: 3+20) - Tighten convergence targets (small: 5%, large: 8%) - Switch from stddev to MAD-based CI margin for robustness against outliers - Increase inter-scenario GC settle time from 50ms to 200ms Made-with: Cursor * demo(benchmark-react): add in-page sub-iterations and reduce variance - Run multiple ops per page visit (default 5), returning the median duration as one sample. Eliminates page-navigation overhead between measurements and dramatically reduces variance. - Add resetStore() to BenchAPI for clearing caches between mount sub-iterations (data-client, tanstack-query, swr). - Vary mutation data each sub-iteration (incrementing counter for titles, toggling moveItem direction) to ensure real DOM changes. - Add waitForPaint between mutation sub-iterations to prevent server-resolution renders from bleeding into the next measurement. - Report variance as percentage instead of absolute values. - Reduce warmup/minMeasurement counts since sub-iterations provide sufficient noise reduction. - Fix SWR mount sub-iterations: add revalidateOnMount + dedupingInterval: 0 to ensure fresh fetches after cache.clear(). - Update README with latest results showing ~6778% mutation throughput advantage for data-client (up from ~4442% with more accurate measurement). Made-with: Cursor * fix: bugbot * demo(benchmark-react): reduce warmup/measurement counts for faster CI With 5 sub-iterations per round providing sufficient noise reduction, lower warmup (small: 3→2, large: 2→1) and max measurement caps (small CI: 20→15, large CI: 15→12) to cut ~30-40s from CI runtime. Made-with: Cursor * Fix benchmark range format for single sample Co-authored-by: Nathaniel Tucker <me@ntucker.me> * demo(benchmark-react): fix stats bugs and add per-scenario opsPerRound - Replace z=1.96 with t-distribution critical values for accurate CI on small samples (n=3-15) - Fix even-length median calculation in scaledMAD/isConverged/computeStats - Fix median===0 premature convergence (now requires margin===0 too) - Fix invalidateAndResolve title accumulation bug using fixture data - Clamp deleteEntity sub-iteration args to mountCount bound - Remove dead cdp parameter from runScenario - Add per-scenario opsPerRound override to Scenario type - Set opsPerRound=9 for update-entity-sorted, =5 for list-detail-switch-10 - Update README with remeasured results and variance tiers Made-with: Cursor --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 2c98dde commit 6b79752

11 files changed

Lines changed: 445 additions & 181 deletions

File tree

examples/benchmark-react/README.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ The repo has two benchmark suites:
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.
1515
- **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.
16-
- **Statistical:** Warmup runs are discarded; we report median and 95% CI. Libraries are interleaved per round to reduce environmental variance.
17-
- **No CPU throttling:** Runs at native speed with more samples for statistical significance rather than artificial slowdown. Small (cheap) scenarios use 3 warmup + 15 measurement runs locally (10 in CI); large (expensive) scenarios use 1 warmup + 4 measurement runs.
16+
- **Statistical:** Warmup runs are discarded; we report median and 95% CI (as percentage of median). Libraries are interleaved per round to reduce environmental variance. Each round runs multiple sub-iterations per page visit and reports the median, further reducing per-sample noise. The default is 5 sub-iterations; individual scenarios can override this via `opsPerRound` in `bench/scenarios.ts` (e.g. `update-entity-sorted` uses 9, `list-detail-switch-10` uses 5).
17+
- **No CPU throttling:** Runs at native speed with more samples for statistical significance rather than artificial slowdown. Small (cheap) scenarios use 2 warmup + up to 12 measurement rounds locally; large (expensive) scenarios use 1 warmup + up to 6 measurement rounds. Early stopping triggers when 95% CI margin drops below the target percentage.
1818

1919
## Scenario categories
2020

@@ -55,10 +55,10 @@ Illustrative **relative** results with **baseline = 100%** (plain React useState
5555

5656
| Category | Scenarios (representative) | data-client | tanstack-query | swr | baseline |
5757
|---|---|---:|---:|---:|---:|
58-
| Navigation | `getlist-100`, `getlist-500`, `getlist-500-sorted` | ~95% | ~97% | ~99% | **100%** |
59-
| Navigation | `list-detail-switch-10` | **~851%** | ~233% | ~247% | 100% |
60-
| Mutations | `update-entity`, `update-user`, `update-entity-sorted`, `update-entity-multi-view`, `unshift-item`, `delete-item`, `move-item` | **~4442%** | ~97% | ~99% | 100% |
61-
| Scaling (10k items) | `update-user-10000` | **~6408%** | ~94% | ~100% | 100% |
58+
| Navigation | `getlist-100`, `getlist-500`, `getlist-500-sorted` | ~97% | ~100% | ~100% | **100%** |
59+
| Navigation | `list-detail-switch-10` | **~1652%** | ~231% | ~230% | 100% |
60+
| Mutations | `update-entity`, `update-user`, `update-entity-sorted`, `update-entity-multi-view`, `unshift-item`, `delete-item`, `move-item` | **~6994%** | ~97% | ~99% | 100% |
61+
| Scaling (10k items) | `update-user-10000` | **~9713%** | ~94% | ~100% | 100% |
6262

6363

6464
## Latest measured results (network simulation on)
@@ -70,28 +70,28 @@ Run: **2026-03-22**, Linux (WSL2), `yarn build:benchmark-react`, static preview
7070
| Scenario | data-client | tanstack-query | swr | baseline |
7171
|---|---:|---:|---:|---:|
7272
| **Navigation** | | | | |
73-
| `getlist-100` | 18.48 ± 0.02 | 18.62 ± 0.07 | 19.12 ± 0.02 | 19.34 ± 0.09 |
74-
| `getlist-500` | 11.45 ± 0.21 | 11.92 ± 0.18 | 11.96 ± 0.04 | 12.06 ± 0.08 |
75-
| `getlist-500-sorted` | 11.48 ± 0.39 | 11.81 ± 0.22 | 12.00 ± 0.34 | 12.08 ± 0.37 |
76-
| `list-detail-switch-10` | 6.13 ± 0.74 | 1.68 ± 0.07 | 1.78 ± 0.12 | 0.72 ± 0.00 |
73+
| `getlist-100` | 20.16 ± 0.7% | 20.58 ± 0.8% | 20.58 ± 0.8% | 20.58 ± 0.0% |
74+
| `getlist-500` | 12.05 ± 0.9% | 12.55 ± 0.0% | 12.61 ± 0.9% | 12.69 ± 1.4% |
75+
| `getlist-500-sorted` | 12.56 ± 1.4% | 12.72 ± 0.5% | 12.79 ± 0.9% | 12.80 ± 1.4% |
76+
| `list-detail-switch-10` | 12.06 ± 12.5% | 1.69 ± 1.0% | 1.68 ± 1.1% | 0.73 ± 0.1% |
7777
| **Mutations** | | | | |
78-
| `update-entity` | 333.33 ± 4.22 | 6.95 ± 0.00 | 6.94 ± 0.02 | 7.17 ± 0.00 |
79-
| `update-user` | 322.58 ± 11.79 | 6.97 ± 0.01 | 7.15 ± 0.00 | 7.15 ± 0.02 |
80-
| `update-entity-sorted` | 285.71 ± 30.41 | 7.04 ± 0.01 | 7.05 ± 0.02 | 7.23 ± 0.01 |
81-
| `update-entity-multi-view` | 344.83 ± 16.69 | 5.89 ± 0.77 | 5.89 ± 0.82 | 5.97 ± 0.05 |
82-
| `update-user-10000` | 98.04 ± 5.79 | 1.44 ± 0.01 | 1.53 ± 0.00 | 1.53 ± 0.01 |
83-
| `unshift-item` | 285.71 ± 11.11 | 6.89 ± 0.02 | 7.11 ± 0.01 | 7.11 ± 0.01 |
84-
| `delete-item` | 312.50 ± 14.76 | 6.87 ± 0.01 | 7.09 ± 0.01 | 7.10 ± 0.00 |
85-
| `move-item` | 256.41 ± 8.77 | 6.34 ± 0.06 | 6.80 ± 0.01 | 6.77 ± 0.01 |
78+
| `update-entity` | 555.56 ± 8.4% | 6.99 ± 0.3% | 6.99 ± 0.3% | 7.17 ± 0.3% |
79+
| `update-user` | 571.90 ± 12.8% | 6.94 ± 0.5% | 7.18 ± 0.0% | 7.16 ± 0.0% |
80+
| `update-entity-sorted` | 588.24 ± 8.0% | 7.10 ± 0.3% | 7.09 ± 0.4% | 7.28 ± 0.0% |
81+
| `update-entity-multi-view` | 555.56 ± 0.0% | 7.06 ± 0.3% | 7.08 ± 0.3% | 7.26 ± 0.2% |
82+
| `update-user-10000` | 151.52 ± 10.8% | 1.46 ± 0.5% | 1.56 ± 0.2% | 1.56 ± 1.3% |
83+
| `unshift-item` | 425.72 ± 5.0% | 6.90 ± 0.1% | 7.13 ± 0.3% | 7.14 ± 0.3% |
84+
| `delete-item` | 526.32 ± 7.2% | 6.89 ± 0.3% | 7.13 ± 0.5% | 7.12 ± 1.0% |
85+
| `move-item` | 285.71 ± 4.0% | 6.55 ± 0.5% | 6.99 ± 0.5% | 6.92 ± 0.8% |
8686

8787
[Measured on a Ryzen 9 7950X; 64 GB RAM; Ubuntu (WSL2); Node 24.12.0; Chromium (Playwright)]
8888

8989
## Expected variance
9090

9191
| Category | Scenarios | Typical run-to-run spread |
9292
|---|---|---|
93-
| **Stable** | `getlist-*`, `update-entity`, `ref-stability-*` | 2-5% |
94-
| **Moderate** | `update-user-*`, `update-entity-sorted`, `update-entity-multi-view` | 5-10% |
93+
| **Stable** | `getlist-*`, `update-entity`, `update-entity-sorted`, `ref-stability-*` | 2-5% |
94+
| **Moderate** | `update-user-*`, `update-entity-multi-view`, `list-detail-switch-10` | 5-10% |
9595
| **Volatile** | `memory-mount-unmount-cycle`, `startup-*`, `(react commit)` suffixes | 10-25% |
9696

9797
Regressions >5% on stable scenarios or >15% on volatile scenarios are worth investigating.
@@ -189,8 +189,8 @@ Regressions >5% on stable scenarios or >15% on volatile scenarios are worth inve
189189

190190
Scenarios are classified as `small` or `large` based on their cost:
191191

192-
- **Small** (3 warmup + 15 measurement): `getlist-100`, `update-entity`, `ref-stability-*`, `invalidate-and-resolve`, `unshift-item`, `delete-item`
193-
- **Large** (1 warmup + 4 measurement): `getlist-500`, `getlist-500-sorted`, `update-user`, `update-user-10000`, `update-entity-sorted`, `update-entity-multi-view`, `list-detail-switch-10`
192+
- **Small** (2 warmup + 3–12 measurement): `getlist-100`, `update-entity`, `ref-stability-*`, `invalidate-and-resolve`, `unshift-item`, `delete-item`
193+
- **Large** (1 warmup + 3–6 measurement): `getlist-500`, `getlist-500-sorted`, `update-user`, `update-user-10000`, `update-entity-sorted`, `update-entity-multi-view`, `list-detail-switch-10`
194194
- **Memory** (opt-in, 1 warmup + 3 measurement): `memory-mount-unmount-cycle` — run with `--action memory`
195195

196196
When running all scenarios (`yarn bench`), each group runs with its own warmup/measurement count. Use `--size` to run only one group.

0 commit comments

Comments
 (0)