advanced 3 min answer

A client-rendered single-page application has poor loading performance and poor search visibility across 140 routes. The team wants server rendering with streaming. There is no appetite for a rewrite and the application must keep shipping weekly. What is the sequence, and where can it go wrong?

ssrstreamingmigrationincrementalrendering
Show the full answer Hide the answer

The sequence

  1. Pick the routes by value, not by difficulty. Usually 5 to 10 routes carry most of the entry traffic and all of the search value: the landing pages, the category and detail pages, the search results. The account settings page does not need server rendering, ever. Migrating everything is the failure mode; migrating the entry points is the work.
  2. Put the server-rendering runtime in place behind a per-route switch, with the existing client-rendered application as the default. Nothing changes for users. This step is infrastructure only, and it is where the deployment model, the caching layer and the observability get sorted out.
  3. Make the data layer fetchable on the server for the first route. This is the actual migration, and it is where the time goes: code that reads window, document, localStorage or browser-only APIs during render has to be moved to effects or guarded. Each such access is a small edit and there are hundreds of them, which is why the route-by-route order matters — you fix them in the order that pays.
  4. Ship one route to 5% of traffic with both metrics and correctness checks. Compare the loading metric and interaction responsiveness against the client-rendered version, and watch the hydration success rate, because a mismatch between server and client markup is the characteristic new failure.
  5. Add streaming only once a route is stable server-rendered. Streaming is a second change with its own risks: the response starts before the page is fully known, so error handling moves — you cannot send a 500 status after the first byte has left, and a failure mid-stream needs a client-visible fallback.
  6. Repeat per route, and stop when the remaining routes do not benefit. A mixed application is a legitimate end state, not a half-finished migration.

Where data can diverge, and how you would know

The new failure class is server and client rendering the same component differently: a date formatted in the server's locale, a random id, a feature flag resolved differently, a value read from localStorage that the server does not have. The symptom is a hydration mismatch, which ranges from a silent re-render to a broken component.

The signals that catch it: hydration success rate per route, a count of hydration mismatch warnings in production, and a synthetic test that asserts interactivity rather than appearance. Without those, a mismatch looks fine in a screenshot and is broken in a user's hands.

The point of no return

Enabling server rendering for a route that search engines have indexed with client-rendered content is reversible in minutes if the switch is per route. The genuinely hard-to-reverse step is changing the data-fetching architecture so components can no longer assume a browser: that touches the whole codebase and nobody will undo it. Do it deliberately, on a branch that lands as a no-op for the existing application, and validate that the client-rendered path still works after it.

The rollback at each stage

Per-route switch means the rollback is a configuration change. Keep the client-rendered build deployable for at least a quarter after the last route moves, because the failure you cannot predict is a server-rendering runtime incident, and its mitigation is serving the old path.

How long it really takes

For 140 routes with a team of four: infrastructure and the first route in 6 to 8 weeks, the first five routes in a quarter, and the browser-API cleanup spread across two. The estimate teams give is usually a third of the real number, because they cost the rendering change and not the data-layer change, which is where the work is.

When this is the wrong answer

If the loading problem is a 3 MB bundle and 12 blocking third-party scripts, server rendering will not fix it — it moves where the HTML is produced and the page still has to download and execute everything to become interactive. Measure first: if the time to first content is bad, server rendering helps; if it is the time from content to interactive, the fix is payload and main-thread work, and the migration costs a quarter for nothing. And if search visibility is the only driver, pre-rendering the handful of indexable routes at build time is often the whole solution at a tenth of the cost.