Skip to content

Application migration

React to Next.js Migration

Moving a Create React App or Vite SPA to Next.js - route by route, with the client-side data layer converted to server rendering and the SEO the SPA never had.

A single-page application and a Next.js application are not the same shape. The SPA renders an empty div and fills it from the browser; Next.js renders on the server and sends HTML. Everything that makes the migration interesting follows from that one difference, and none of it is a file move.

This is the migration for teams on Create React App, Vite, or a hand-rolled React setup - not for teams already on the Pages Router, which is a different problem with a different path.

What actually changes

Routing. React Router's component-based routes become a file tree. A <Route path="/products/:id"> becomes app/products/[id]/page.tsx. Nested routes with shared chrome become nested layouts, which is usually simpler than what was there.

Data fetching. This is the bulk of the work. A SPA fetches in useEffect after mount, which means a loading spinner, a waterfall, and nothing for a crawler to read. In Next.js the same fetch runs on the server before the response is sent. The component stops managing loading state and starts being an async function.

The bundle. Everything in a SPA is client code by definition. After the migration, most of it does not need to be - and the routes we move typically lose 40-60% of their JavaScript, because the tree only has to ship what is actually interactive.

Browser-only assumptions. window, localStorage and document at module scope crash on the server. Finding them is mechanical; deciding which belong in a client component and which were never needed is the judgement.

The order we work in

Next.js and your SPA can serve from the same domain during the migration, so nothing has to happen at once.

  1. The shell and one leaf route. Establishes the layout, the metadata helper and the data conventions on something low-risk.
  2. The routes search engines care about. Marketing pages, product pages, anything public. This is where the return is, because these are the pages that had no server-rendered HTML at all.
  3. Authenticated routes. Dashboards and settings. Often these stay mostly client-rendered and that is correct - there is no SEO value in a page behind a login, and the App Router is happy to leave it interactive.
  4. The old build, deleted. Not before.

What you should expect to gain

The honest version: the performance win is real but uneven, and the SEO win is usually the larger one.

A SPA's first paint waits for the bundle to download, parse and execute before anything appears. Server rendering removes that entirely from the critical path, which is why LCP moves most on first visits and cold caches, and least for a returning user with a warm cache.

The SEO change is more categorical. A crawler receiving an empty div and a script tag has nothing to index. After the migration it receives the content. For applications whose public pages were invisible, that is not an optimisation - it is the difference between existing in search results and not.

What we need to see first

The repository, or a list of routes and what each one fetches. Most of the estimate is in the data layer, not the component count: a hundred presentational components convert in a week, and one tangled global store can take longer than all of them.