Date captured: 2026-04-12
Compared implementations:
- baseline Inertia plus Webpack from the
gumroad-rsc-baselineworktree - current Inertia plus Rspack plus React 19 from this repo on
jg-codex/react19-rspack
Important framing:
- this comparison isolates the tooling branch, not the final runtime hypothesis
Rspackwas expected to improve build and dev-loop performanceReact Server Componentsare the part that may justify extra page-level complexity on runtime performance
Measured surface:
- route:
/dashboard - base URL:
https://gumroad.dev - user:
seller@gumroad.com
Local setup notes:
- both measurements used the same Docker-backed local services and the same local database
- route-level dashboard measurements required local Elasticsearch indices for
product_page_views,purchases, andconfirmed_follower_events - the dashboard harness authenticates over HTTP before loading the page in Chrome, so the measurement is not dependent on browser login behavior
- artifact paths in this document point at local files under
output/playwright/dashboard-perf/; they are intentionally ignored in git, so treat them as checkout-relative paths rather than GitHub links
| Metric | Baseline Webpack | Current Rspack | Delta |
|---|---|---|---|
| Cold production build | 25.19s |
11.25s |
-55.3% |
| Cold development build | 16.24s |
5.28s |
-67.5% |
Built inertia entrypoint bytes |
763,454 |
803,865 |
+5.3% |
| Dashboard navigation duration | 475.33ms |
483.30ms |
+1.7% |
| Dashboard response end | 358.23ms |
338.00ms |
-5.7% |
| Dashboard LCP | 497.33ms |
509.33ms |
+2.4% |
| Dashboard packs transfer | 250,170 bytes |
349,054 bytes |
+39.5% |
| Dashboard JS request count | 9 |
11 |
+22.2% |
| Largest dashboard JS chunk | 160,657 bytes |
263,358 bytes |
+63.9% |
Artifacts:
- baseline screenshot:
output/playwright/dashboard-perf/baseline-webpack-dashboard.png - current screenshot:
output/playwright/dashboard-perf/current-rspack-dashboard.png - baseline metrics JSON:
output/playwright/dashboard-perf/baseline-webpack-dashboard-metrics.json - current metrics JSON:
output/playwright/dashboard-perf/current-rspack-dashboard-metrics.json
The developer-experience win is real.
- Rspack is dramatically faster for cold builds in both development and production.
- That is already a legitimate Shakapacker positioning point for Inertia apps.
The runtime win remained unproven on the full dashboard.
- The dashboard route was not materially faster under the Rspack branch.
- That was expected, because the architecture was still the same Inertia page.
Date captured: 2026-04-12
Compared implementations:
- current Inertia plus Rspack dashboard baseline from this repo
- first isolated React Server Components via React on Rails Pro demo at
/dashboard/rsc_demo
Important caveats:
- the
RSCdemo numbers below are from an earlier isolated 3-run average - the current
/dashboardroute became too noisy in the same browser harness after the demo landed - this comparison proved technical feasibility, but it was not the cleanest control surface
| Metric | Current Rspack Dashboard | First RSC Demo | Delta |
|---|---|---|---|
| Navigation duration | 483.30ms |
550.73ms |
+14.0% |
| Response end | 338.00ms |
486.43ms |
+43.9% |
| LCP | 509.33ms |
573.33ms |
+12.6% |
| JS transfer | 349,054 bytes |
37,377 bytes |
-89.3% |
| JS request count | 11 |
3 |
-72.7% |
Artifacts:
- isolated RSC metrics JSON:
output/playwright/dashboard-perf/rsc-isolated-3-dashboard-rsc-demo-metrics.json - dashboard asset comparison JSON:
output/playwright/dashboard-perf/dashboard-vs-rsc-asset-comparison.json
That first pass proved something important, but not yet the thing we needed most.
- It proved that we can build a bounded React Server Components via React on Rails Pro surface against real Gumroad data.
- It proved that the isolated route can cut shipped client-side JavaScript very aggressively.
- It did not prove a page-performance win.
The next step had to be a cleaner control surface.
Date captured: 2026-04-12
Compared implementations:
- warmed matched Inertia control at
/dashboard/inertia_demo - warmed matched React Server Components via React on Rails Pro demo at
/dashboard/rsc_demo
Why this comparison matters more:
- both routes render the same reduced creator-home slice
- both routes use the same presenter-backed seller data
- both routes now share the same outer
inertialayout - this isolates architecture more cleanly than comparing the RSC demo against the full dashboard
Artifacts:
- Inertia control metrics JSON:
output/playwright/dashboard-perf/inertia-demo-control-warm-trimmed-3-dashboard-inertia-demo-metrics.json - RSC matched metrics JSON:
output/playwright/dashboard-perf/rsc-demo-warm-trimmed-3-dashboard-rsc-demo-metrics.json - warmed matched comparison JSON:
output/playwright/dashboard-perf/warmed-matched-inertia-vs-rsc-comparison.json
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Navigation duration | 492.03ms |
429.90ms |
-12.6% |
| Response end | 344.90ms |
371.20ms |
+7.6% |
| LCP | 496.00ms |
452.00ms |
-8.9% |
| HTML response transfer | 14,401 bytes |
15,444 bytes |
+7.2% |
| JS request count | 5 |
1 |
-80.0% |
Additional context:
- the Inertia control still ships a
data-pageblob of about5,789bytes - the RSC demo removes that Inertia payload entirely on this route
- these warmed runs were captured with one explicit server warmup request and the standalone Node Renderer process running via
bin/dev - the final RSC sample also uses the same outer
inertialayout as the control route, so the remaining delta is less likely to be a layout artifact - the response-end pass reduced the raw RSC response from about
36.9KBto about15.1KBand the inline RSC script from about25.4KBto about8.9KB - the browser-side
htmlBytessnapshot is dramatically smaller for the RSC route after load, but that number reflects post-render DOM state rather than raw network response size, so it is useful context rather than the primary claim
This is the first evidence that supports a real performance positioning story.
- The
RSCroute is still slower to finish the initial HTML response. - Even with that server-response cost, the warmed matched
RSCdemo is faster on total navigation duration. - More importantly, the warmed matched
RSCdemo is also faster onLCP, which is the most relevant user-visible win we have measured so far. - The remaining response-end gap stayed roughly the same even after most of the raw transfer gap disappeared, which points to renderer or streaming overhead rather than just HTML size.
That means the story is now more precise:
Rspackis the build and dev-loop win.- A carefully bounded React Server Components via React on Rails Pro surface can also produce a user-visible page-load win.
- The tradeoff is not free, because the server response is still modestly slower than the Inertia control even after the response payload was almost fully normalized.
This is promising, but it is still not enough for an upstream adoption recommendation by itself.
- The win exists on a reduced comparison surface, not on the full dashboard.
- The measurements are still local-development measurements, not production-like traces.
- The earlier grouped averages were captured before the benchmark runner could require a compatible Chrome and chromedriver pair, so they should be treated as directional rather than final.
Date captured: 2026-04-14
What changed in this pass:
- added route-scoped
Server-Timingto both/dashboard/inertia_demoand/dashboard/rsc_demo - updated the benchmark harness to record those timings in the JSON summaries
- re-ran the demo pair on a dedicated local renderer port after finding that port
3800was occupied by an unrelated renderer process
Important caveat:
- the first 3-run Inertia batch was captured before the RSC batch
- rerunning the Inertia control after the RSC batch improved the control by about
9-10% - that means cache order matters enough that the stricter comparison is the post-RSC Inertia rerun against the RSC batch
Artifacts:
- instrumented Inertia first-pass JSON:
output/playwright/dashboard-perf/inertia-demo-server-timing-3-dashboard-inertia-demo-metrics.json - instrumented Inertia post-RSC JSON:
output/playwright/dashboard-perf/inertia-demo-server-timing-3-post-rsc-dashboard-inertia-demo-metrics.json - instrumented RSC JSON:
output/playwright/dashboard-perf/rsc-demo-server-timing-3-dashboard-rsc-demo-metrics.json
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Navigation duration | 585.03ms |
461.97ms |
-21.0% |
| Response end | 433.43ms |
396.50ms |
-8.5% |
| LCP | 610.67ms |
484.00ms |
-20.7% |
| HTML response transfer | 14,244 bytes |
15,265 bytes |
+7.2% |
| JS request count | 6 |
1 |
-83.3% |
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
Controller action_total |
253.73ms |
229.94ms |
-9.4% |
Presenter compare_props |
225.14ms |
194.60ms |
-13.6% |
Presenter compare_creator_home |
206.88ms |
181.21ms |
-12.4% |
sql.active_record |
99.53ms |
84.58ms |
-15.0% |
render_dispatch |
24.84ms |
23.30ms |
-6.2% |
This was an important intermediate result, but it should not be treated as the headline benchmark anymore.
- The earlier matched result already showed a user-visible win on navigation duration and
LCP. - The new instrumented pass showed that, on that local setup, the
RSCroute could also stay ahead onresponseEndagainst a more-warmed Inertia rerun. - The route-scoped timings suggested the RSC route might also be doing less app-side work in the controller/presenter path on this reduced surface.
- The first Inertia batch overstated the gap because measurement order changed cache warmth, which is exactly why this follow-up is more credible than the raw first-pass numbers.
The right conclusion is not "RSC is now proven faster everywhere."
The right conclusion is:
- the current local evidence was strong enough to justify a stricter benchmark method
- the next step was to validate the result under balanced route ordering
- broader migration claims still needed a more disciplined comparison
Date captured: 2026-04-14
What changed in this pass:
- added
scripts/perf/compare_dashboard_routes.rb - rotated route order by cycle instead of running grouped batches
- used
4cycles so each route ran first twice and second twice
Artifacts:
- balanced alternating comparison JSON:
output/playwright/dashboard-perf/dashboard-demo-alternating-4-comparison.json - balanced alternating run directory:
output/playwright/dashboard-perf/dashboard-demo-alternating-4-runs
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Navigation duration | 568.47ms |
501.53ms |
-11.8% |
| Response end | 423.23ms |
441.65ms |
+4.4% |
| LCP | 602.00ms |
525.00ms |
-12.8% |
| HTML response transfer | 14,240.5 bytes |
15,265.0 bytes |
+7.2% |
| JS request count | 6 |
1 |
-83.3% |
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
Controller action_total |
250.50ms |
278.32ms |
+11.1% |
Presenter compare_props |
226.41ms |
236.16ms |
+4.3% |
Presenter compare_creator_home |
209.89ms |
220.35ms |
+5.0% |
sql.active_record |
120.42ms |
120.99ms |
+0.5% |
render_dispatch |
20.57ms |
23.61ms |
+14.8% |
The alternating runner also makes route-order sensitivity explicit:
- Inertia when first: navigation
545.10ms, response end395.00ms - Inertia when second: navigation
591.85ms, response end451.45ms - RSC when first: navigation
502.90ms, response end443.25ms - RSC when second: navigation
500.15ms, response end440.05ms
That means the Inertia control is more sensitive to whether it goes first or second in the cycle, while the RSC route is comparatively stable. But the aggregate result is still the one that matters, and the aggregate result keeps the user-visible win while preserving a modest server-side tradeoff.
Date captured: 2026-04-14
What changed in this pass:
- re-ran the alternating comparison with a matching
Chrome 147andChromeDriver 147 - extended the run to
8cycles so each route ran first four times and second four times - recovered the final comparison summary from the completed per-run JSON files after the long run exited before writing the aggregate file
Artifacts:
- clean-driver comparison JSON:
output/playwright/dashboard-perf/dashboard-demo-alternating-8-clean-driver-comparison.json - clean-driver run directory:
output/playwright/dashboard-perf/dashboard-demo-alternating-8-clean-driver-runs
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Navigation duration | 544.80ms |
396.65ms |
-27.2% |
| Response end | 385.55ms |
375.75ms |
-2.5% |
| LCP | 568.00ms |
426.00ms |
-25.0% |
- one RSC run reported a
19.3sduration on cacheddashboard_rsc_demo_styles.css - that resource showed
0transfer bytes and a normalresponseEndof356.20ms, so the spike was a dev-asset timing anomaly rather than a slow server response - the same outlier inflated the RSC mean
navigation durationto2822.07msand meanLCPto2847.50ms, which makes the mean unsuitable as the headline statistic for this repeat
What still matters from this repeat:
- the matched-driver rerun removed the earlier browser-driver mismatch caveat
- route-scoped server averages were roughly neutral-to-favorable for the RSC route on this pass
- median user-visible metrics still favored the RSC route
- the recovered comparison JSON now carries median and
p95primary-metric deltas plus per-path slowest-pack-resource summaries, so this outlier is visible without manual per-run inspection
That means the clean-driver repeat increased confidence in the benchmark discipline, but it also showed that development-mode asset timing is still noisy enough that a production-like rerun is the next real step.
Date captured: 2026-04-15
What changed in this pass:
- found that another local repo had reclaimed port
3035with a plain-HTTP webpack dev server - confirmed Rails was proxying
https://app.gumroad.dev/packs/...requests to that wrong listener, which explained the browser failures and500asset responses - added JS-side support for
SHAKAPACKER_DEV_SERVER_*overrides so the custom Webpack and Rspack config stays in sync with Ruby/Shakapacker - restarted Puma and
bin/shakapacker-dev-serverwithSHAKAPACKER_DEV_SERVER_PORT=3036 - manually verified both
/dashboard/inertia_demoand/dashboard/rsc_demoin a signed-in browser session before rerunning the benchmark - reran the balanced alternating comparison with a matching
Chrome 147andChromeDriver 147pair, using--reuse-existingto recover the fourth sample after one transient ChromeDriver navigation timeout
Artifacts:
- corrected clean-port comparison JSON:
output/playwright/dashboard-perf/dashboard-demo-alternating-4-clean-driver-port-3036-matched-driver-comparison.json - corrected clean-port run directory:
output/playwright/dashboard-perf/dashboard-demo-alternating-4-clean-driver-port-3036-matched-driver-runs
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Navigation duration | 473.02ms |
437.63ms |
-7.5% |
| Response end | 328.73ms |
371.20ms |
+12.9% |
| LCP | 507.00ms |
451.00ms |
-11.0% |
| HTML response transfer | 14,329.75 bytes |
15,265.0 bytes |
+6.5% |
| JS request count | 6 |
1 |
-83.3% |
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
Controller action_total |
159.53ms |
216.15ms |
+35.5% |
Presenter compare_props |
141.38ms |
168.62ms |
+19.3% |
Presenter compare_creator_home |
130.82ms |
156.64ms |
+19.7% |
sql.active_record |
75.01ms |
87.30ms |
+16.4% |
render_dispatch |
15.75ms |
26.95ms |
+71.1% |
- Inertia when first: navigation
466.75ms - Inertia when second: navigation
479.30ms - RSC when first: navigation
465.90ms - RSC when second: navigation
409.35ms
This rerun matters because it removes a false local failure mode from the story.
- the earlier inability to load
/dashboard/inertia_demowas not an Inertia rendering regression - it was a local asset-proxy mismatch caused by another repo using the default Shakapacker dev-server port
- once that was corrected, the user-visible RSC advantage remained
- the server-side tradeoff became more pronounced, which is exactly the kind of clarity this repo needs for positioning
This is the better current headline than the earlier mixed-port local runs. It keeps the “RSC can win on user-visible metrics” claim intact while making it harder to hide the cost behind a broken local environment.
Date captured: 2026-04-15
What changed in this follow-up:
- repeated the clean-port rerun at
8cycles instead of4 - kept the same corrected
SHAKAPACKER_DEV_SERVER_PORT=3036setup - kept the matching
Chrome 147andChromeDriver 147pair - completed without the earlier
19.3sdev-asset outlier
Artifacts:
- corrected clean-port
8-cycle comparison JSON:output/playwright/dashboard-perf/dashboard-demo-alternating-8-clean-driver-port-3036-matched-driver-comparison.json - corrected clean-port
8-cycle run directory:output/playwright/dashboard-perf/dashboard-demo-alternating-8-clean-driver-port-3036-matched-driver-runs
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Navigation duration | 457.16ms |
402.29ms |
-12.0% |
| Response end | 320.70ms |
335.96ms |
+4.8% |
| LCP | 501.00ms |
421.00ms |
-16.0% |
| HTML response transfer | 14,332.38 bytes |
15,265.0 bytes |
+6.5% |
| JS request count | 6 |
1 |
-83.3% |
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
Controller action_total |
163.10ms |
169.74ms |
+4.1% |
Presenter compare_props |
144.80ms |
145.49ms |
+0.5% |
Presenter compare_creator_home |
134.52ms |
136.14ms |
+1.2% |
sql.active_record |
76.67ms |
77.10ms |
+0.6% |
render_dispatch |
15.76ms |
18.63ms |
+18.2% |
- the user-visible RSC win held up on a longer clean-port run
- the server-side tradeoff shrank from the first corrected 4-cycle rerun to a modest gap
- the earlier mixed-port
8-cycle repeat is now less useful as a headline than this corrected8-cycle result
This is the previous clean-port benchmark headline and remains useful as a development-mode diagnostic artifact. The production-like compiled-asset repeat below supersedes it as the current local headline benchmark for the repo.
Date captured: 2026-04-30 UTC
What changed in this follow-up:
- built Shakapacker/Rspack assets with
RENDERER_PASSWORD=benchmarkRendererPassword RAILS_ENV=production NODE_ENV=production bin/shakapacker - built the standalone RSC demo bundles with
RENDERER_PASSWORD=benchmarkRendererPassword RAILS_ENV=production NODE_ENV=production npm run build:rsc-demo - ran Rails without the Shakapacker dev server
- ran the React on Rails Pro Node renderer as a dedicated process with
RENDERER_PASSWORD=benchmarkRendererPassword,RENDERER_PORT=3800,RENDERER_WORKERS_COUNT=2, andRENDERER_LOG_LEVEL=warn - initialized fresh local Elasticsearch indexes with
DevTools.delete_all_indices_and_reindex_all - kept the matching
Chrome 147andChromeDriver 147pair - completed an
8-cycle alternating run by recovering the last two samples with--reuse-existingafter a late SeleniumNet::ReadTimeout
Artifacts:
- tracked production-like
8-cycle comparison JSON:docs/performance-artifacts/production-like-alternating-8-reindexed/comparison.json - tracked production-like
8-cycle raw metrics directory:docs/performance-artifacts/production-like-alternating-8-reindexed/runs
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
| Median navigation duration | 775.40ms |
607.15ms |
-21.7% |
| Median response end | 644.80ms |
588.80ms |
-8.7% |
| Median LCP | 794.00ms |
634.00ms |
-20.2% |
| Median HTML transfer | 14,223 B |
12,373 B |
-13.0% |
| JS request count | 6 |
1 |
-83.3% |
| p95 response end | 730.62ms |
768.25ms |
+5.2% |
| Metric | Inertia demo | RSC demo | Delta |
|---|---|---|---|
Median controller action_total |
346.87ms |
339.20ms |
-2.2% |
Median presenter compare_props |
311.50ms |
294.38ms |
-5.5% |
Median sql.active_record |
130.74ms |
128.87ms |
-1.4% |
Median render_dispatch |
30.01ms |
26.18ms |
-12.8% |
p95 sql.active_record |
151.58ms |
164.19ms |
+8.3% |
- the RSC user-visible win survived compiled assets and a dedicated renderer process
- median server-side route timing is now roughly neutral-to-favorable for the RSC route
p95 responseEndis the clearest remaining caution- the RSC route currently streams its payload inline, so the benchmark's
/rsc_payload/browser-resource fields are empty on this implementation
This is now the best local headline benchmark for the repo.
At the time of capture, this was the benchmark result to use for review and positioning.
- The
RSCroute still wins on total navigation duration. - The
RSCroute still wins onLCP. - The latest production-like repeat also wins on median
responseEnd. - Tail response timing still needs work because p95
responseEndis modestly worse for the RSC route.
That gives us a cleaner, more defensible story:
- the user-visible win is still real
- the client-JS reduction is still dramatic
- the current server-side tradeoff has narrowed to tail behavior and renderer/SQL profiling details
- the benchmark method is now strong enough that evaluators can focus on product value instead of measurement discipline
Today’s credible story is:
Shakapacker + Rspackcan deliver immediate build and dev-loop wins for a real Inertia app.React 19 + Rspackis technically viable here.- React Server Components via React on Rails Pro now has matched-surface evidence of a user-visible win on a stricter production-like alternating benchmark.
Today’s non-credible story is:
- "The full Gumroad dashboard is already faster under the current RSC work."
- "The server is universally faster with RSC."
The next demo only helps if the matched React Server Components via React on Rails Pro implementation continues to beat the matched Inertia control on metrics that matter:
- equal or better LCP
- equal or better total navigation duration
- ideally equal or better response end once the implementation is tuned further
- fewer client-side requests or bytes for the page
- with server-response costs that are understandable and defensible
If the RSC demo cannot keep that balance, then it should be positioned as a composition or product-shaping experiment, not an adoption recommendation.
Keep the branches and claims narrow:
- Keep
jg-codex/react19-rspackfocused on bundler viability and build-speed wins. - Treat React 19 type cleanup as a separate stacked branch if needed.
- Keep the matched
/dashboard/inertia_demoand/dashboard/rsc_demopair as the primary performance comparison surface. - Keep using the alternating comparison runner instead of grouped batches for future local claims.
- Run headline local comparisons with
--require-driver-matchso mismatched Chrome and chromedriver pairs fail fast instead of silently adding noise. - Repeat the production-like comparison on a stable deployed environment before broadening the pitch.
- Add renderer-internal timing or expose a measurable RSC payload resource before making stronger claims about where the remaining tail latency comes from.