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.

    • Blazing-fast dev server with instant HMR
    • Minimal configuration and simple build setup
    • Excellent developer experience for SPA-style applications

    However, as the project grew, several requirements pushed us toward a more full-featured framework.

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

    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)

    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)

    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.

    1. Create a new Next.js project alongside the existing Vite project.
    2. Mirror the component structure.
    3. Convert routing from React Router to file-based routing.
    4. Move reusable components first.
    5. Gradually migrate pages.
    6. Remove Vite after all critical flows were stable.

    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.

    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)

    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:

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

    Link Migration

    React Router:

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

    Next.js:

    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:

    import logo from "/logo.svg";
    

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

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

    Or using the optimized image component:

    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:

    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.

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

    Usage:

    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 TypeRendering Strategy
    Marketing pagesStatic generation
    Blog pagesStatic generation with revalidation
    DashboardsServer rendering
    Interactive toolsClient-side rendering

    Example of static generation:

    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:

    const theme = localStorage.getItem("theme")
    

    After:

    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.

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

    Step 9: Updating Build Scripts

    Original Vite scripts:

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

    Next.js scripts:

    {
      "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

    MetricVite SPANext.js
    First Contentful Paint~2.4s~1.2s
    Time to Interactive~3.1s~1.8s
    SEO CrawlabilityLimitedFull HTML
    Bundle StrategySingle SPA bundleRoute-based splitting
    Initial HTMLMinimal shellFully 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.