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?
Show the full answer Hide the answer
The sequence
- 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.
- 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.
- 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,localStorageor 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. - 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.
- 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.
- 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.