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.
- The shell and one leaf route. Establishes the layout, the metadata helper and the data conventions on something low-risk.
- 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.
- 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.
- 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.
