internal/analysis/rsc-fouc-shakaperf-investigation.md
| Area | Status | Finding |
|---|---|---|
| React on Rails dummy app | Proven | Current main fixes the RSC use client CSS FOUC repro. |
| Old/pre-fix React on Rails | Proven | Reproduces the bug as an unstyled first paint. |
| ShakaPerf coverage | Proven for dummy app | ShakaPerf catches the old/fixed difference and passes fixed/fixed. |
| hichee downstream validation | Not proven yet | Package/manifest evidence exists. ShakaPerf browser proof is still TODO. |
| Unit tests | Not delivered here | Add after hichee is ShakaPerf-proven and/or the upstream package fix is released/pinned. |
The bug is CSS for a React Server Components client boundary arriving too late or not being emitted with the RSC response.
Failure mode:
'use client' boundary.The fixed behavior is that RSC CSS is represented before hydration, so the boundary is styled at first visible paint.
Old/pre-fix vs current/fixed first paint. ShakaPerf reports a visual change (25.48%) on the RSC CSS probe.
Current/fixed vs current/fixed first paint. Same test, same fixed side on both servers. ShakaPerf reports no difference (0.00%).
Natural first-visible assertion. This does not block app JS; the old/pre-fix side fails the first-visible computed-style assertion.
Old/pre-fix selector capture: unstyled first paint.
Current/fixed selector capture: styled first paint.
| Test | Control | Experiment | ShakaPerf result | Investigation result | Purpose |
|---|---|---|---|---|---|
| Deterministic first-paint visual test | old/pre-fix | current/fixed | FAIL: 1 visual mismatch | PASS: real A/B diff detected | Blocks app JS before navigation to isolate server-rendered first paint. |
| Deterministic first-paint visual test | current/fixed | current/fixed | PASS | PASS | Control run proving the test is stable when both sides are fixed. |
| Natural first-visible assertion | old/pre-fix | current/fixed | FAIL: assertion error | PASS: old side is unstyled at first visible frame | Does not block JS. Uses waitUntil: "commit" and RAF computed-style assertion. |
| Natural first-visible assertion | current/fixed | current/fixed | PASS | PASS | Control run proving the assertion is not inherently flaky. |
| Evidence | Result | Artifact |
|---|---|---|
| Old/pre-fix first-paint probe image | 840x24, expected green CSS 0.0%, white/plain background 95.12%, dark text 1.45%. | image |
| Current/fixed first-paint probe image | 214x36, expected green CSS 70.35%, white/plain background 0.0%. | image |
| Old/pre-fix vs current/fixed visual diff | 1851 diff pixels. | ShakaPerf report screenshot, selector diff |
| Current/fixed vs current/fixed visual diff | 0 diff pixels. | ShakaPerf report screenshot, report |
| Natural first-visible old/pre-fix assertion | backgroundColor: rgba(0, 0, 0, 0), color: rgb(0, 0, 0), padding: 0px, borderRadius: 0px, width: 840, height: 24. | log, old image, fixed image |
This is not a global ShakaPerf limitation.
Normal settled screenshots can miss transient FOUC because ShakaPerf captures after page preparation and the test function complete. For this bug, two valid patterns worked:
beforeNavigate, then compare the server-rendered first paint;waitUntil: "commit", and assert computed styles on the first visible animation frame.For a pure visual "capture exactly at first visible frame" report card, ShakaPerf would need a first-class capture hook inside the test function or a dedicated first-visible capture mode.
Artifacts: rsc-fouc-shakaperf-artifacts/
Artifact index: rsc-fouc-shakaperf-artifacts/README.md
| Area | Artifacts |
|---|---|
| Setup/test bundle | README |
| Deterministic first-paint AB test | rsc-fouc.abtest.ts |
| Natural first-visible assertion AB test | natural-first-visible-assertion.abtest.ts |
| Main ShakaPerf config | abtests.config.ts |
| Twin server setup | Dockerfile, Procfile |
| Generated ShakaPerf instructions followed | setup/generated-shakaperf-skills/ |
| Run | Report | Log |
|---|---|---|
| Old/pre-fix vs current/fixed deterministic first paint | full-report.html | log |
| Current/fixed vs current/fixed deterministic first paint | full-report.html | log |
| Old/pre-fix vs current/fixed natural first-visible assertion | full-report.html | log |
| Current/fixed vs current/fixed natural first-visible assertion | full-report.html | log |
Reference: react_on_rails#3587
General shape:
[email protected] locally so client-reference manifest entries include css?: string[];<link rel="stylesheet" precedence="ror-rsc"> elements before renderToPipeableStream;References:
General shape:
globalThis.__reactFlightClientManifest during server render so React can find the manifest;$RC to $RR where needed so replacement waits for CSS.| Layer | Downstream implementation | Upstream package implementation |
|---|---|---|
| Manifest CSS metadata | Yes, via local pnpm patch against [email protected]. | Yes, in package/plugin code. |
| Where stylesheet links are emitted | React on Rails Pro renderer wraps the RSC tree. | Package/loader layer wraps client references. |
| Deferred Suspense CSS gating | Relies on React 19 stylesheet precedence behavior for emitted links. | Adds React $RR behavior for deferred replacement waiting on CSS. |
| Rails asset prefix handling | Explicitly handled by the downstream resolver. | Needs integration/pinning verification downstream. |
| Status | Merged downstream bridge. | Open/upstream package work. |
Conclusion: the implementations overlap but are not identical. The downstream implementation is a bridge/integration fix for React on Rails. The upstream package implementation is the more complete package-level direction. Once the upstream package fix is released and pinned, the downstream bridge should be reviewed for deletion or reduction to Rails-specific integration only.
Reference: react_on_rails#3577
Status:
react-on-rails-rsc 19.0.5-rc.5;Current npm dist-tags observed during the investigation:
| Tag | Version |
|---|---|
latest | 19.0.4 |
rc | 19.0.5-rc.5 |
Reference: private shakacode/hichee#9379.
This is not proven with ShakaPerf yet. What we have so far is package and manifest evidence:
react-on-rails-rsc-19.0.5-rc.1;make exports;Manifest evidence from the local production build artifacts:
| hichee side | Manifest entries | Entries with CSS | Unique CSS files |
|---|---|---|---|
| base | 92 | 0 | 0 |
| PR head | 101 | 87 | 5 |
No hichee browser/ShakaPerf proof is claimed here. That is still the next step.
No new unit tests are included in this PR.
The safest order is:
Good unit-test targets after that:
| Test | Location / owner |
|---|---|
client-reference manifest entries include CSS metadata for CSS imported behind 'use client'; | upstream package or downstream patch if bridge remains |
CSS href resolver handles prefix, dedupe, sort, and missing css; | downstream React on Rails Pro if resolver remains |
| renderer emits React 19 stylesheet links from manifest CSS; | downstream React on Rails Pro if renderer bridge remains |
| unpatched/no-CSS manifests behave as no-op; | downstream React on Rails Pro |
ReScript make exports are classified as component-shaped client references; | upstream package if ReScript support is required there |
| hichee RSC page does not first-paint unstyled; | ShakaPerf visual/assertion coverage, not unit-level |
react_on_rails#3577 after the upstream package fix is released/pinned.react_on_rails#3587 should stay, shrink to Rails asset integration, or be removed after upstream package adoption.