docs/oss/migrating/rsc-performance-validation.md
Use this playbook when an RSC migration is intended to improve page performance. It defines the evidence package reviewers need before treating an RSC conversion as a performance win.
Part 9 of the RSC Migration Series | Previous: Flight Payload Optimization | Next: Mostly Static RSC Shell With a Tiny Sidecar
The cleanest merge signal is usually a local A/B comparison:
origin/main, or the explicit baseline branch the app already uses in
production.Review-app, staging, production PageSpeed, and production Lighthouse runs are useful context, but they are not automatically a clean A/B. CDN, hosting, cache warmth, data, image signatures, and deploy topology can differ enough to explain the result.
RSC should be compared against the path users actually have today. If the existing page uses
cached_react_component or cached_react_component_hash
with React on Rails Pro prerender caching, include a warm cached SSR baseline. A static page that is
already served from a warm SSR fragment cache can be very hard to beat on TTFB, FCP, or LCP, even
when RSC reduces hydration work and JavaScript bytes.
Use the matrix in Benchmarking RSC Against Warm SSR Caches to label cold uncached SSR, warm cached SSR, RSC cold, and RSC warm runs. The public tracker for that tradeoff is #4294.
Performance evidence is incomplete without visual evidence. A page is not faster in a useful sense if it is missing visible UI, fonts, images, layout wrappers, search bars, or mobile navigation behavior.
For every changed page and viewport:
This also protects performance interpretation. A lower Total Blocking Time can be real while LCP gets worse because CSS, fonts, or the LCP image moved later in the critical path.
Final merge evidence should use installable packages:
main source checkouts and local package tarballs are diagnostic only unless the tested
package has been published and remeasured.Keep one variable per run. Do not combine an app conversion, package bump, framework SHA, bundler configuration change, and static-shell sidecar rewrite in one measurement if the result needs to guide implementation.
Each run should leave durable artifacts. Name archives so a future reader can identify the stack, baseline, candidate, date, pages, and viewport set without opening the report.
Record:
Parse JSON or equivalent machine-readable artifacts. Do not rely on terminal scrollback or a single summary screenshot.
For ShakaPerf runs based on the HiChee lesson:
throttlingMethod: 'devtools' for this workflow.The methodology matters more than the tool. ShakaPerf is the known ShakaCode workflow; equivalent tooling is fine when it produces the same control/experiment, visual, performance, and archived evidence.
Include this checklist in the PR description or benchmark comment:
| Evidence | Required detail |
|---|---|
| URLs | Control URL and experiment URL |
| SHAs | Control SHA and experiment SHA |
| Package stack | React on Rails, React on Rails Pro, react-on-rails, react-on-rails-pro, react-on-rails-rsc, and tarball shasums when used |
| Pages/viewports | Every changed page and desktop/mobile viewport that matters |
| Visual diff | Diff pixels and percent for each page/viewport |
| Lighthouse score | Score for control and experiment, plus win/regression/no material change |
| FCP | Control, experiment, delta, verdict |
| Speed Index | Control, experiment, delta, verdict |
| LCP | Control, experiment, delta, verdict |
| TBT | Control, experiment, delta, verdict |
| Total downloads | Control, experiment, delta, verdict |
| JavaScript bytes | Control, experiment, delta, verdict |
| Caveats | Local-vs-production, cache, CDN, image, data, throttling, sample-size, or visual caveats |
Use honest verdicts. It is valid for an RSC migration to be a win on TBT and JavaScript bytes, a regression on LCP, and still worth pursuing for maintainability or future optimization. Say that plainly.
A HiChee public-page conversion moved the home and FAQ pages to mostly static RSC output. Earlier attempts produced misleading signals because they mixed package stacks, incomplete visual evidence, missing asset setup, and hosted PageSpeed context that was not a clean local A/B.
The final merge confidence came from a local master-vs-PR ShakaPerf run on the same machine. It tested home and FAQ in desktop and mobile viewports, paired visual regression with performance, used the published package stack, and treated review-app or production PageSpeed as context rather than the canonical merge signal. The published-stack run had clean visual diffs, materially lower JavaScript and download bytes, and large mobile Lighthouse gains on the changed pages. A separate diagnostic run against local framework main-tip packages was rejected because it failed visual parity, even though some performance numbers looked better.
The key lesson is not "RSC always wins." The lesson is that RSC performance work needs a disciplined evidence package: same-machine control/experiment, stable package stack, one variable per run, archived parsed output, and visual parity as a merge gate.
When the evidence points to unexpected RSC cost, use the existing troubleshooting guides before changing architecture:
'use client' modules.clientReferences
explains why globally emptying clientReferences is not a safe general optimization.For mostly static pages that still need a few browser behaviors, continue with Mostly Static RSC Shell With a Tiny Sidecar.