ViteNext.jsMigrationFunctionality Improvement

    From Vite to Next.js: How We Migrated Our App

    March 11, 20267 min read

    Migrating a Production React App from Vite to Next.js

    Why We Moved From Vite to Next.js

    Vite served us really well in the early stages of development.

  1. Blazing-fast dev server: with instant HMR
  2. Minimal configuration: and simple build setup
  3. Excellent developer experience: for SPA-style applications
  4. However, as the project grew, several requirements pushed us toward a more full-featured framework.

  5. SEO & social sharing: We needed server-side rendering (SSR) and static generation (SSG).
  6. Routing conventions: Maintaining large React Router configurations was becoming harder to scale.
  7. Backend logic near the UI: We wanted APIs closer to the frontend without running a separate backend service for everything.
  8. Deployment simplicity: A framework with clear production conventions simplifies CI/CD and hosting.
  9. In short:

    > Vite is excellent for building fast client-side applications, but we needed a full-stack React framework with built-in rendering strategies.

    ---

    Architecture Before Migration (Vite SPA)

    bash
    flowchart LR: U[User] --> B[Browser] B --> CDN[CDN / Static Hosting] CDN --> S["React SPA (Vite Build)"] S --> A["Backend API"] A --> DB[(Database)]

    Characteristics

    * Client-side rendering only

    * Initial HTML is mostly empty

    * Data fetched after page load

    * SEO depends heavily on client-side execution

    ---

    Architecture After Migration (Next.js Hybrid Rendering)

    bash
    flowchart LR: User --> Browser Browser --> NextServer[Next.js Server] NextServer --> Render[SSR / SSG Rendering] Render --> API[API Routes or External APIs] API --> DB[(Database)]

    Characteristics

    * Server-side rendering

    * Static generation for public pages

    * API routes colocated with UI

    * Improved SEO and performance

    ---

    High-Level Migration Strategy

    Instead of rewriting the application, we chose a **phased migration strategy**.

  10. Create a new Next.js project alongside the existing Vite project.
  11. Mirror the component structure.
  12. Convert routing from React Router to file-based routing.
  13. Move reusable components first.
  14. Gradually migrate pages.
  15. Remove Vite after all critical flows were stable.
  16. This allowed both applications to run side-by-side during development.

    ---

    Step 1: Creating the Next.js Project

    We started by scaffolding a new Next.js application.

    bash
    npx create-next-app@latest

    Configuration choices:

    * TypeScript enabled

    * ESLint enabled

    * App Router enabled

    * Import alias enabled (`@/*`)

    We then recreated a structure similar to the existing Vite project.

    src/
      components/
      lib/
      utils/
    
    public/

    This made it easier to move files without large refactors.

    ---

    Step 2: Routing Migration

    Routing in Vite (React Router)

    tsx
    import { BrowserRouter, Routes, Route } from "react-router-dom"; import Home from "./pages/Home"; import About from "./pages/About"; ReactDOM.createRoot(document.getElementById("root")!).render( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </BrowserRouter> );

    Routing in Next.js

    Next.js uses **file-based routing**.

    app/
      page.tsx        -> /
      about/
        page.tsx      -> /about

    Example page:

    tsx
    export default function HomePage() { return <div>Home</div>; }

    ---

    Link Migration

    React Router:

    tsx
    import { Link } from "react-router-dom"; <Link to="/about">About</Link>

    Next.js:

    tsx
    import Link from "next/link"; <Link href="/about">About</Link>

    After migration, React Router was completely removed.

    ---

    Step 3: Moving Shared Components

    Most UI components migrated **without changes**.

    Vite structure:

    src/
      components/
        Button.tsx
        Layout.tsx
      pages/
        Home.tsx

    Next.js structure:

    src/
      components/
        Button.tsx
        Layout.tsx
    
    app/
      page.tsx

    Only components depending on React Router required changes.

    Examples:

    * `useNavigate`

    * `useLocation`

    These were replaced with:

    * `next/navigation`

    * `Link`

    * layout-based navigation

    ---

    Step 4: Handling Static Assets

    In Vite:

    tsx
    import logo from "/logo.svg";

    In Next.js, assets are served from the `public` folder.

    tsx
    <img src="/logo.svg" alt="Logo" />

    Or using the optimized image component:

    tsx
    import Image from "next/image"; <Image src="/logo.svg" alt="Logo" width={120} height={40} />

    ---

    Step 5: Environment Variables

    Vite uses:

    import.meta.env.VITE_API_URL

    Next.js requires browser-exposed variables to start with `NEXT_PUBLIC_`.

    Example `.env` change:

    VITE_API_URL=https://api.example.com

    becomes:

    NEXT_PUBLIC_API_URL=https://api.example.com

    Usage in code:

    ts
    const apiUrl = process.env.NEXT_PUBLIC_API_URL

    ---

    Step 6: Replacing Vite Plugins

    Many Vite plugins were no longer required.

    Path Aliases

    Configured in `tsconfig.json`.

    json
    { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }

    Usage:

    ts
    import Button from "@/components/Button"

    ---

    Code Splitting

    Next.js automatically performs:

    * route-based code splitting

    * bundle optimization

    * lazy loading

    This required no manual configuration.

    ---

    SVG Handling

    Options used in Next.js:

    * store SVGs in `public/`

    * import as components using loaders

    * render using `next/image`

    ---

    Step 7: Introducing SSR and Static Generation

    One of the biggest advantages of Next.js is **hybrid rendering**.

    We categorized routes into three types:

    | Page Type | Rendering Strategy |

    | ----------------- | ----------------------------------- |

    | Marketing pages | Static generation |

    | Blog pages | Static generation with revalidation |

    | Dashboards | Server rendering |

    | Interactive tools | Client-side rendering |

    Example of static generation:

    tsx
    export async function generateStaticParams() { const res = await fetch("https://api.example.com/posts") const posts = await res.json() return posts.map((post: any) => ({ slug: post.slug })) }

    Benefits:

    * improved SEO

    * faster first paint

    * reduced client-side JavaScript

    ---

    Step 8: Fixing Client-Side Assumptions

    Migrating from a SPA exposed several issues.

    Common examples:

    * direct `window` access

    * `localStorage` during render

    * DOM-only libraries

    Example fix.

    Before:

    tsx
    const theme = localStorage.getItem("theme")

    After:

    tsx
    const [theme, setTheme] = useState<string | null>(null) useEffect(() => { setTheme(localStorage.getItem("theme")) }, [])

    ---

    Handling Browser-Only Libraries

    For components requiring the DOM we used dynamic imports.

    tsx
    import dynamic from "next/dynamic" const Map = dynamic(() => import("@/components/Map"), { ssr: false })

    ---

    Step 9: Updating Build Scripts

    Original Vite scripts:

    json
    { "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" } }

    Next.js scripts:

    json
    { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" } }

    CI pipeline changes:

    * replace `vite build` with `next build`

    * ensure Node version compatibility

    * verify `.next` output works with hosting platform

    ---

    Step 10: Testing for Behavioral Parity

    Before fully switching, we ensured both apps behaved identically.

    Testing included:

    * authentication flows

    * dashboards

    * public pages

    * API requests

    During migration both apps ran simultaneously.

    Vite app  -> localhost:5173
    Next app  -> localhost:3000

    Once parity was achieved:

    * Vite configuration was removed

    * dependencies cleaned up

    * old scripts deleted

    ---

    Performance Improvements

    | Metric | Vite SPA | Next.js |

    | ---------------------- | ----------------- | --------------------- |

    | First Contentful Paint | ~2.4s | ~1.2s |

    | Time to Interactive | ~3.1s | ~1.8s |

    | SEO Crawlability | Limited | Full HTML |

    | Bundle Strategy | Single SPA bundle | Route-based splitting |

    | Initial HTML | Minimal shell | Fully rendered |

    Key improvements:

    * faster first paint

    * improved SEO indexing

    * reduced client-side JavaScript

    ---

    Final Project Structure

    project-root/
    
    app/
      layout.tsx
      page.tsx
      dashboard/
        page.tsx
      blog/
        [slug]/
          page.tsx
    
    src/
      components/
        Navbar.tsx
        Button.tsx
        Card.tsx
    
      lib/
        api.ts
        utils.ts
    
      hooks/
    
    public/
    
    next.config.js
    tsconfig.json
    package.json

    ---

    Lessons Learned

    * Start with a clean Next.js project.

    * Move framework-agnostic code first.

    * Migrate routing early.

    * Expect SSR compatibility issues.

    * Introduce Next.js features incrementally.

    ---

    When Should You Move From Vite to Next.js?

    You should consider switching if:

    * SEO matters

    * your application has many routes

    * you want server-side rendering

    * you want backend logic near the UI

    You should probably stay with Vite if:

    * your application is a pure SPA

    * SEO does not matter

    * you already have a separate backend

    * simplicity is more important than framework features

    Both tools are excellent — they simply solve **different problems**.

    ---

    Final Thoughts

    Vite provided an incredible development experience and allowed us to move quickly in the early stages.

    However, as our application grew, we needed:

    * better SEO

    * server-side rendering

    * stronger routing conventions

    * tighter frontend-backend integration

    Next.js provided these capabilities **without forcing us to abandon React or rewrite our components**.

    The migration was not a rewrite.

    It was a **structured evolution of the architecture**.

    Same React components.

    Better rendering strategy.

    A more scalable platform.