Skip to content

demo: Add benchmark-react with normalization and ref-stability scenarios#3783

Merged
ntucker merged 46 commits intomasterfrom
benchmark-react-normalization
Mar 19, 2026
Merged

demo: Add benchmark-react with normalization and ref-stability scenarios#3783
ntucker merged 46 commits intomasterfrom
benchmark-react-normalization

Conversation

@ntucker
Copy link
Collaborator

@ntucker ntucker commented Mar 9, 2026

Motivation

We need a browser-based benchmark for @data-client/react that highlights the performance advantages of normalization and referential equality: when one entity is updated, only components that use that entity get new references; others keep the same object reference so React can skip rerenders. This enables comparison against other React data libraries (TanStack Query, SWR) and surfaces regressions in CI.

Solution

  • New example examples/benchmark-react: Playwright-driven benchmark that runs a React app modeling GitHub Issues (with shared Users and Labels), measures mount/update duration via performance.measure(), and reports in customSmallerIsBetter format for rhysd/github-action-benchmark.
  • CI runs data-client only: Hot-path scenarios for data-client track regressions; competitor libraries (TanStack Query, SWR, baseline) are benchmarked locally for comparison only.
  • React Compiler support: yarn bench:run:compiler builds with babel-plugin-react-compiler (via @anansi/babel-preset's reactCompiler option) and labels results [compiler] for side-by-side comparison.
  • New Collection.moveWith() API: Analogous to addWith(), constructs a custom move schema. New unshift merge export. Tests, docs, and changeset included.
  • CI hardening: Adds concurrency/permissions to existing workflows (Node benchmark, bundle size, CodeQL, site preview/release) to reduce overlapping runs.

Data model

GitHub-shaped: Issue entities with nested User (assignee) and Label[]. Multiple issues share the same User, so a single user update propagates through normalization — the core advantage data-client demonstrates over non-normalized libraries.

Scenarios

Hot path (CI, data-client only)

Scenario What it exercises
getlist-100 / getlist-500 Full fetch → normalize → render pipeline
update-single-entity Store update → selective rerender
update-shared-user-500-mounted / update-shared-user-10000-mounted Normalization advantage at scale (one update, many subscribers)
ref-stability-issue-changed / ref-stability-user-changed Referential equality preservation (deterministic, count-based)
sorted-view-mount-500 / sorted-view-update-entity Query schema memoization vs useMemo sort
list-detail-switch Normalized cache lookup vs per-navigation fetch
unshift-item / delete-item / move-item CRUD mutations (Collection ops vs invalidate+refetch)
invalidate-and-resolve controller.invalidate → Suspense fallback → re-resolve (data-client only)

Local comparison only

Scenario What it exercises
memory-mount-unmount-cycle Heap delta after mount/unmount cycles (500 items × 10 cycles)

All hot-path scenarios run across all four libraries (data-client, TanStack Query, SWR, baseline) locally, except invalidate-and-resolve which is data-client only.

Key design decisions

  • No CPU throttling — raw timings without CDP slowdown for more consistent CI results
  • Interleaved library execution with shuffled order per round to reduce environmental variance
  • Three measurement layers: performance.measure() (JS-driven), React Profiler actualDuration (reconciliation), Chrome trace duration (full rendering pipeline)
  • Adaptive convergence: warmup runs discarded; measurement continues until 95% CI margin is within target % of mean, or max iterations reached. Reports median.
  • Validation suite (yarn validate:run): correctness checks for each library's benchmark harness
  • Report viewer (bench/report-viewer.html): filterable table (base/react-commit/trace), time-series charting via history file loading
  • Virtualized lists via react-window for large item counts
  • Service Worker network simulation: configurable per-method latency delays

Scripts

Script Description
yarn bench:run Build + serve + bench (all libraries locally)
yarn bench:run:compiler Same but with React Compiler enabled, results labelled [compiler]
yarn build:compiler Build with React Compiler only
yarn bench:compiler Run bench with [compiler] label only
yarn bench:dc Run data-client scenarios only
yarn bench:small / yarn bench:large Run only small or large scenario sizes
yarn validate:run Build + serve + run correctness validation

Collection.moveWith() + unshift

New public API addition in @data-client/endpoint:

  • Collection.moveWith(merge) — builds a move schema with custom add behavior (parallel to addWith())
  • unshift — convenience merge function that prepends to arrays

Changeset, tests (123 new lines in Collection.test.ts), docs (docs/rest/api/Collection.md), blog update, and playground type updates included.

Made with Cursor

@vercel
Copy link

vercel bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-site Ignored Ignored Preview Mar 19, 2026 2:08am

@changeset-bot
Copy link

changeset-bot bot commented Mar 9, 2026

🦋 Changeset detected

Latest commit: feca469

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@data-client/endpoint Minor
@data-client/rest Minor
@data-client/graphql Minor
@data-client/img Minor
example-benchmark Patch
example-benchmark-react Patch
normalizr-github-example Patch
normalizr-redux-example Patch
normalizr-relationships Patch
test-bundlesize Patch
coinbase-lite Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ntucker ntucker force-pushed the benchmark-react-normalization branch from af004f6 to a2cb25d Compare March 9, 2026 03:32
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Size Change: +16 B (+0.02%)

Total Size: 80.5 kB

Filename Size Change
examples/test-bundlesize/dist/rdcEndpoint.js 6.35 kB +16 B (+0.25%)
ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 3.18 kB
examples/test-bundlesize/dist/polyfill.js 307 B
examples/test-bundlesize/dist/rdcClient.js 10.2 kB
examples/test-bundlesize/dist/react.js 59.7 kB
examples/test-bundlesize/dist/webpack-runtime.js 726 B

compressed-size-action

@codecov
Copy link

codecov bot commented Mar 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.06%. Comparing base (84744e0) to head (feca469).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3783   +/-   ##
=======================================
  Coverage   98.05%   98.06%           
=======================================
  Files         151      151           
  Lines        2834     2843    +9     
  Branches      555      556    +1     
=======================================
+ Hits         2779     2788    +9     
  Misses         11       11           
  Partials       44       44           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Benchmark

Details
Benchmark suite Current: feca469 Previous: a8c10ca Ratio
normalizeLong 447 ops/sec (±0.96%) 448 ops/sec (±0.82%) 1.00
normalizeLong Values 410 ops/sec (±0.31%) 404 ops/sec (±0.48%) 0.99
denormalizeLong 285 ops/sec (±3.19%) 275 ops/sec (±2.97%) 0.96
denormalizeLong Values 269 ops/sec (±1.92%) 264 ops/sec (±2.36%) 0.98
denormalizeLong donotcache 1048 ops/sec (±0.15%) 1043 ops/sec (±0.15%) 1.00
denormalizeLong Values donotcache 774 ops/sec (±0.17%) 759 ops/sec (±0.20%) 0.98
denormalizeShort donotcache 500x 1592 ops/sec (±0.07%) 1431 ops/sec (±0.16%) 0.90
denormalizeShort 500x 862 ops/sec (±1.96%) 785 ops/sec (±2.10%) 0.91
denormalizeShort 500x withCache 6223 ops/sec (±0.09%) 4769 ops/sec (±0.62%) 0.77
queryShort 500x withCache 2753 ops/sec (±0.06%) 2611 ops/sec (±0.31%) 0.95
buildQueryKey All 54557 ops/sec (±0.32%) 58214 ops/sec (±0.33%) 1.07
query All withCache 5966 ops/sec (±0.32%) 5813 ops/sec (±0.25%) 0.97
denormalizeLong with mixin Entity 282 ops/sec (±2.37%) 277 ops/sec (±2.02%) 0.98
denormalizeLong withCache 6892 ops/sec (±0.22%) 6431 ops/sec (±0.09%) 0.93
denormalizeLong Values withCache 5134 ops/sec (±0.16%) 4658 ops/sec (±0.10%) 0.91
denormalizeLong All withCache 5853 ops/sec (±0.10%) 5468 ops/sec (±0.09%) 0.93
denormalizeLong Query-sorted withCache 6020 ops/sec (±0.15%) 6294 ops/sec (±0.09%) 1.05
denormalizeLongAndShort withEntityCacheOnly 1681 ops/sec (±0.21%) 1551 ops/sec (±0.25%) 0.92
getResponse 4596 ops/sec (±0.85%) 3770 ops/sec (±0.23%) 0.82
getResponse (null) 10594084 ops/sec (±1.03%) 9815415 ops/sec (±0.77%) 0.93
getResponse (clear cache) 267 ops/sec (±1.92%) 272 ops/sec (±2.12%) 1.02
getSmallResponse 3468 ops/sec (±0.14%) 3231 ops/sec (±0.39%) 0.93
getSmallInferredResponse 2522 ops/sec (±0.57%) 2476 ops/sec (±0.21%) 0.98
getResponse Collection 4631 ops/sec (±0.44%) 3763 ops/sec (±0.15%) 0.81
get Collection 4665 ops/sec (±0.20%) 3539 ops/sec (±0.31%) 0.76
get Query-sorted 5262 ops/sec (±0.29%) 5072 ops/sec (±0.11%) 0.96
setLong 453 ops/sec (±0.27%) 452 ops/sec (±0.54%) 1.00
setLongWithMerge 255 ops/sec (±0.22%) 258 ops/sec (±0.29%) 1.01
setLongWithSimpleMerge 272 ops/sec (±0.15%) 273 ops/sec (±0.18%) 1.00
setSmallResponse 500x 962 ops/sec (±0.13%) 903 ops/sec (±0.17%) 0.94

This comment was automatically generated by workflow using github-action-benchmark.

@ntucker ntucker force-pushed the benchmark-react-normalization branch from 5ad1b81 to 4df9d01 Compare March 12, 2026 03:10
@ntucker ntucker force-pushed the benchmark-react-normalization branch 2 times, most recently from c8bc040 to 081e48d Compare March 14, 2026 02:56
ntucker added 5 commits March 13, 2026 23:00
- Browser benchmark comparing @data-client/react (Playwright, customSmallerIsBetter).
- Scenarios: mount, update entity/author, ref-stability (item/author ref counts).
- Hot-path (CI) vs with-network (local): simulated delay for overfetch comparison.
- CI workflow runs hot-path only; reports to rhysd/github-action-benchmark.

Made-with: Cursor
);
const margin = 1.96 * (stdDev / Math.sqrt(trimmed.length));
return (margin / Math.abs(mean)) * 100 <= targetMarginPct;
}
Copy link

Choose a reason for hiding this comment

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

Convergence check uses mean, not median as documented

Low Severity

The isConverged docstring and the RunProfile.targetMarginPct JSDoc both state convergence is checked against the median, but the implementation computes margin / Math.abs(mean). Since computeStats reports the median as the final value, convergence is assessed against a different central tendency than the one reported. For skewed distributions (common with benchmark outliers), mean and median can diverge significantly, causing premature or delayed convergence relative to what the median-based report actually needs.

Additional Locations (1)
Fix in Cursor Fix in Web

ntucker and others added 2 commits March 18, 2026 00:11
…as HTML

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@ntucker ntucker merged commit 1f34136 into master Mar 19, 2026
28 checks passed
@ntucker ntucker deleted the benchmark-react-normalization branch March 19, 2026 12:49
@github-actions github-actions bot mentioned this pull request Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant