Case Study: RSC Migration Regression
Incident summary
A team migrated a dashboard route to a server-component-heavy architecture to reduce client JavaScript. Bundle size improved, but support users reported stale panels, confusing loading behavior, and slower completion of customer lookup tasks.
The migration was technically successful by the metric chosen before launch. It failed by the user workflow metric that mattered.
Timeline
| Time | Event |
|---|---|
| Week 0 | Team proposes RSC migration to reduce dashboard JavaScript. |
| Week 1 | Prototype shows lower client bundle size and faster lab LCP. |
| Week 3 | Migration ships to 20% of support users. |
| Day 2 | Support agents report customer details showing old status after edits. |
| Day 3 | RUM shows lower JS transfer but no improvement in task completion. |
| Day 4 | Team finds cache invalidation gaps after mutations. |
| Day 5 | Rollout pauses; interactive panels move back behind client boundaries. |
| Week 5 | Corrected architecture ships with route data contract and mutation revalidation. |
Production signals
- client JavaScript decreased
- lab LCP improved
- customer lookup completion time worsened
- stale data reports increased
- panel-level loading spinners appeared during completed tasks
- mutation success events were not followed by corresponding data refresh
Root cause
The team treated RSC as a performance feature instead of a boundary and data-ownership decision. Server/client placement changed without redesigning cache keys, invalidation, loading boundaries, or task continuity.
Architecture failures
- success metric was "less JavaScript" rather than "faster correct task completion"
- cache keys did not include all route, entity, role, and freshness dimensions
- mutations did not revalidate affected server-rendered data
- Suspense boundaries followed component structure, not user tasks
- temporary interaction state moved too far server-side
- rollout dashboard did not track stale-data symptoms
Bad alternatives
| Alternative | Why it was wrong |
|---|---|
| Revert all RSC usage permanently | loses valid bundle and data-fetching benefits |
| Add manual refresh buttons everywhere | shifts architecture failure onto users |
| Make every panel client-rendered again | avoids cache problem but loses useful server composition |
| Increase cache TTL globally | makes stale-data failure worse |
Corrected architecture
| Concern | Correction |
|---|---|
| Boundary | server owns data orchestration; client owns immediate task continuity |
| Cache | document route/entity cache keys and invalidation rules |
| Loading | align Suspense boundaries to meaningful panels and user tasks |
| Mutation | pair optimistic feedback with server revalidation |
| Observability | track task completion, stale reports, mutation-refresh latency, INP, errors, and JS size |
Corrected flow
Prevention controls
- route rendering matrix must include cache and mutation effects
- RSC migration ADR must define server/client ownership
- every mutation lists affected server-rendered data
- stale-data regression fixtures become part of release checks
- rollout dashboard tracks workflow metrics, not only bundle metrics
- Suspense boundary review checks user meaning, not just tree position
Review questions
- What user task should improve after this migration?
- Which state must remain client-local for task continuity?
- Which data becomes stale after each mutation?
- Which cache keys include tenant, role, locale, experiment, and entity dimensions?
- What production signal proves the migration worked?
Reusable lesson
Rendering architecture is data architecture. Moving code to the server can reduce JavaScript while making the product worse if cache, mutation, and loading semantics are not designed around user tasks.
Source lens
Use Part III RSC boundaries, server functions, cache layering, Part VI React rendering performance, and Future Radar React rendering.