Skip to main content

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

TimeEvent
Week 0Team proposes RSC migration to reduce dashboard JavaScript.
Week 1Prototype shows lower client bundle size and faster lab LCP.
Week 3Migration ships to 20% of support users.
Day 2Support agents report customer details showing old status after edits.
Day 3RUM shows lower JS transfer but no improvement in task completion.
Day 4Team finds cache invalidation gaps after mutations.
Day 5Rollout pauses; interactive panels move back behind client boundaries.
Week 5Corrected 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

AlternativeWhy it was wrong
Revert all RSC usage permanentlyloses valid bundle and data-fetching benefits
Add manual refresh buttons everywhereshifts architecture failure onto users
Make every panel client-rendered againavoids cache problem but loses useful server composition
Increase cache TTL globallymakes stale-data failure worse

Corrected architecture

ConcernCorrection
Boundaryserver owns data orchestration; client owns immediate task continuity
Cachedocument route/entity cache keys and invalidation rules
Loadingalign Suspense boundaries to meaningful panels and user tasks
Mutationpair optimistic feedback with server revalidation
Observabilitytrack 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

  1. What user task should improve after this migration?
  2. Which state must remain client-local for task continuity?
  3. Which data becomes stale after each mutation?
  4. Which cache keys include tenant, role, locale, experiment, and entity dimensions?
  5. 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.