Skip to content

Benchmark jarl against react-router, and publish the results - #93

Open
randomdevpete wants to merge 2 commits into
task-530-retire-jarl-react-redux-and-native-packagesfrom
task-674-performance-comparison-vs-react-router
Open

Benchmark jarl against react-router, and publish the results#93
randomdevpete wants to merge 2 commits into
task-530-retire-jarl-react-redux-and-native-packagesfrom
task-674-performance-comparison-vs-react-router

Conversation

@randomdevpete

Copy link
Copy Markdown
Owner

Backs the README's "incredibly efficient" claim with an actual measurement against react-router, and publishes the methodology and results as a docs guide.

Based on task-530-retire-jarl-react-redux-and-native-packages (#89), not master.

Methodology

  • jarl jarl-atoms/jarl-react 2.6.0 (jotai 2.20.2, jotai-location 0.6.2) vs react-router 8.3.0 data router, on identical react/react-dom 19.2.8.
  • Node 24.15.0, Intel i7-1165G7, Linux (WSL2).
  • Both routers drive the same app (13 active-styled nav links, 10 non-routing components, four routed pages), sharing every router-agnostic component verbatim. The render test asserts both apps produce byte-identical HTML after mount and after every single navigation — that assertion is what makes the comparison like-for-like, and it fails loudly if either app drifts.
  • Timed numbers: median of 30 retained samples × 1000 operations, after 10 discarded warm-up samples, GC forced between samples, NODE_ENV=production, one forked process. Reported with p25/p75.
  • Reproduce with npm run bench from the repo root.

Headline numbers, wins and losses alike

Re-renders per navigation — a tie. 13/13 nav links, 1/1 changed page, 0/0 layout, 0/0 non-routing components. react-router's context model is just as precise here, and the write-up says so in bold. jarl's one measured deficit: every route-atom subscriber renders twice at mount (react-router renders once).

Throughput — jarl wins one, loses one:

workload jarl react-router
resolve URL → matched leaf (warm) 103 µs 375 µs
resolve, cold store per URL 117 µs 375 µs
client navigation via each API 100 µs 56 µs

Bundle (min+gzip): jarl 5.7 kB full cost / 2.0 kB with jotai external, react-router 28.3 kB — flagged in the guide as not a like-for-like feature set, since react-router carries its data APIs regardless.

It also corrected a false README claim found along the way: route atoms return a fresh object on every location change, so every subscriber of any route atom re-renders on any navigation. Atom-level subscription narrows which components subscribe, it does not skip unaffected routes. The README now says only what the harness measures, and links to the numbers.

What the numbers do not show

No real-browser timings (no layout/paint/input latency), no data loading on either side, one app shape and one route-table shape. Both READMEs state this.

Review changes made before opening

  • Cut the duplicated methodology essays from the harness source (a 17-line header on matching.benchmark.ts among others); that prose now lives once, in bench/README.md.
  • Deleted dead navEntries/NavEntry exports from bench/src/shape.ts.
  • Documented two fairness asymmetries that were previously unstated: jarl's leaf reads short-circuit at the first match (hence the deliberate early/middle/late/miss URL cycle), and router.navigate() does strictly more work than the atom write that loses to it — so that gap is understated in jarl's disfavour, not its favour.
  • Formatted Benchmarks.md with oxfmt (it was the only unformatted file in the tree).

Style exceptions

None. No exception: markers in the diff.

@randomdevpete

Copy link
Copy Markdown
Owner Author

I'm confused about the results. Client navigation is slated as slower than RR, but resolving a route takes significantly longer. Surely when navigating on the client a route must be resolved, so how can that be faster when the different is so great on resolution?

Let's almost come up with a rather more complicated scenario - multi-level deep nest routing, and also pit jotai's pure-start "switch" version vs react-router's route components (or maybe their routeconfig, which might be a bit more comparison)

Another comparison worth doing : client navigation with jarl pre-resolving nested async data via atoms, vs react router triggering a multi-level Suspense cascade (pretty sure I know which one is better, but it's good to benchmark as well, and have a bigger more interesting set of numbers to show)

@randomdevpete
randomdevpete force-pushed the task-530-retire-jarl-react-redux-and-native-packages branch from 5e0d9cf to 79927e6 Compare August 19, 2026 00:41
Reproducible comparison harness: per-navigation re-render counts with a
byte-identical-HTML parity assertion between the two apps, matching/resolve
and navigation throughput under plain Node, and min+gzip bundle size from the
published dist builds. Run from the repo root with `npm run bench`; the
deterministic parity/count test also runs under `npm test` in CI.

Ticket: 674
Adds the Benchmarks guide (results, methodology, honest caveats: re-render
ties, the mount double-render, and react-router's faster stateful navigate)
and replaces the README's unmeasured "incredibly efficient" line with a link
to the measured comparison.

Ticket: 674
@randomdevpete
randomdevpete force-pushed the task-674-performance-comparison-vs-react-router branch from cef9174 to 434880e Compare August 19, 2026 00:41
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