Web Development10 min read

Create React App is Dead. What’s Next? (Vite vs. Next.js)

Create React App (CRA) served a purpose for years: a zero‑config scaffold that let teams spin up a React codebase in seconds. But the ecosystem moved on. Webpac...

Listen to Article

Click play to listen to audio narration

Create React App is Dead. What’s Next? (Vite vs. Next.js)

Introduction

Create React App (CRA) served a purpose for years: a zero‑config scaffold that let teams spin up a React codebase in seconds. But the ecosystem moved on. Webpack’s single‑threaded compilation became a bottleneck, the dev server lagged on large monorepos, and the “eject” path turned into a maintenance nightmare. In our production clusters we saw build times climb past ten minutes and hot‑module replacement (HMR) stalls that broke developer flow. The decision to retire CRA wasn’t emotional — it was a cost‑benefit analysis that favored tooling built on native ES modules and a compiler that understands React at the framework level.

Why This Matters

If you ship a React front‑end today, you’re choosing between two fundamentally different execution models:

  • Vite – a dev server that serves native ES modules, leveraging esbuild for pre‑bundling and Rollup for production. It stays framework‑agnostic; you decide the routing, data‑fetching, and rendering strategy.
  • Next.js – a full‑stack React framework that owns the build, routing, server‑side rendering (SSR), static generation (SSG), and API routes. It adds a compilation layer (Turbopack in v14+) that rewrites React components into optimized server/client bundles.

The choice influences CI/CD pipeline complexity, edge‑deployment eligibility, and the mental model your team adopts for “where does this code run?”. Ignoring the trade‑offs leads to hidden costs: duplicated build logic, inconsistent caching, or a runtime that can’t stream HTML to the edge.

How It Works

Below is a high‑level flow of what happens when a developer runs npm run dev in a Vite project versus a Next.js project. The diagram highlights where the tooling diverges: module graph construction, transformation, and the dev server’s request handling.

flowchart TD
    subgraph Vite_Dev_Server
        A[Browser Request] --> B[Vite HTTP Server]
        B --> C{Is entry HTML?}
        C -- Yes --> D[Transform index.html]
        C -- No --> E[Resolve Module Graph]
        E --> F[esbuild Prebundle (deps)]
        F --> G[Rollup Plugin Pipeline]
        G --> H[Serve Transformed Module]
    end

    subgraph Next_Dev_Server
        I[Browser Request] --> J[Next.js Dev Server (Node)]
        J --> K{Page / API Route?}
        K -- Page --> L[React Server Components Compiler]
        K -- API --> M[API Route Handler]
        L --> N[Turbopack / Webpack Module Graph]
        N --> O[ServerSide Render / Stream]
        O --> P[Send HTML + Client Chunks]
        M --> Q[JSON Response]
    end

    style Vite_Dev_Server fill:#f9f,stroke:#333,stroke-width:2px
    style Next_Dev_Server fill:#bbf,stroke:#333,stroke-width:2px

Step‑by‑step breakdown

  1. Request entry – Both tools start with an HTTP request from the browser.
  2. Vite serves the raw index.html, then rewrites <script type="module"> imports on‑the‑fly. Dependencies are pre‑bundled once with esbuild (fast, Go‑based) and cached.
  3. Next.js routes the request to either a page component (triggering the React Server Components compiler) or an API route. The compiler builds a module graph using Turbopack (Rust‑based) or Webpack, then streams the rendered HTML.
  4. HMR – Vite pushes granular module updates via WebSocket; Next.js performs fast refresh at the page/component boundary, preserving React state where possible.

Core Concepts

ConceptViteNext.js
Module FormatNative ESM in dev, Rollup (ESM/CommonJS) in prodMixed: Server components compiled to CommonJS for Node, client bundles ESM
Dependency Pre‑bundlingesbuild (single‑threaded, O(n) for deps)Turbopack (incremental, parallel) or Webpack
RoutingManual (React Router, wouter, etc.)File‑system based (pages/ or app/)
Rendering ModesSPA only (unless you add a custom SSR server)SSR, SSG, ISR, Edge SSR, Streaming
API LayerNone (add Express/Fastify)Built‑in API routes (pages/api or app/api)
Configuration Surfacevite.config.ts + Rollup pluginsnext.config.js + Turbopack plugins (experimental)
TypeScript Integrationtsconfig.json + vite-plugin-checkerFirst‑class, tsconfig.json drives compilation

Understanding these knobs lets you predict build‑time scaling. For a 200‑component monorepo, Vite’s esbuild pre‑bundle stays under 2 s; Next.js’s Turbopack incremental graph can drop a full rebuild from 45 s to <10 s after the first run.

Examples & Code Walkthrough

1. Minimal Vite + React + TypeScript Setup

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ mode }) => ({
  // Resolve path aliases from tsconfig.json
  plugins: [
    react({
      // Enable Fast Refresh for all components
      fastRefresh: true,
    }),
    tsconfigPaths(),
    // Bundle analysis only in CI
    mode === 'production' && visualizer({ open: false, gzipSize: true }),
  ].filter(Boolean),

  // Production‑only Rollup tweaks
  build: {
    target: 'es2022',
    minify: 'esbuild',
    rollupOptions: {
      output: {
        // Deterministic hashes for long‑term caching
        entryFileNames: 'assets/[name]-[hash].js',
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]',
      },
    },
  },

  // Dev server hardening
  server: {
    port: 3000,
    strictPort: true,
    hmr: { overlay: true },
  },
}));

Comments:

  • vite-tsconfig-paths mirrors TypeScript path mapping without duplicating config.
  • visualizer is gated behind mode === 'production' to avoid dev‑server overhead.
  • esbuild minify is faster than Terser and produces comparable output for modern targets.

2. Next.js 14 app/ Directory with Streaming SSR

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { getUserMetrics } from '@/lib/data';
import { MetricsChart } from '@/components/MetricsChart';
import { Skeleton } from '@/components/ui/Skeleton';

export const revalidate = 60; // ISR: regenerate at most once per minute

async function DashboardContent() {
  // Server‑only data fetch, runs at request time (or at build for static)
  const metrics = await getUserMetrics();
  return <MetricsChart data={metrics} />;
}

export default function DashboardPage() {
  return (
    <section aria-labelledby="dashboard-heading">
      <h1 id="dashboard-heading">Dashboard</h1>
      {/* Stream the chart while the rest of the page is interactive */}
      <Suspense fallback={<Skeleton variant="chart" />}>
        <DashboardContent />
      </Suspense>
    </section>
  );
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable Turbopack for local dev (experimental but stable in 14.2+)
  experimental: {
    turbo: {
      // Resolve aliases identical to Vite setup
      resolveAlias: {
        '@/*': './src/*',
      },
    },
  },
  // Optimize images for edge delivery
  images: {
    remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }],
  },
  // Strict CSP for production
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.example.com;",
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Comments:

  • revalidate opts into Incremental Static Regeneration (ISR) without a custom server.
  • Suspense boundary streams the chart HTML while the shell stays interactive.
  • Turbopack alias mirrors the Vite tsconfigPaths behavior, keeping import paths identical across repos.

Best Practices

  1. Lock the Node version – Both toolchains rely on native bindings (esbuild, Turbopack). Pin node in package.json ("engines": { "node": ">=20.10.0 <21" }) and enforce via .nvmrc + CI.
  2. Separate dev/prod configs – Keep vite.config.ts and next.config.js lean; use environment‑specific files (vite.config.prod.ts, next.config.prod.js) for heavy plugins (bundle analyzer, Sentry source maps).
  3. Cache aggressively in CI – Cache node_modules, Vite’s node_modules/.vite, and Next.js’s .next/cache. In GitHub Actions, actions/cache@v4 with a key that includes package-lock.json hash.
  4. Adopt a shared TypeScript base – A monorepo tsconfig.base.json with paths eliminates drift between Vite and Next.js projects.
  5. Instrument build metrics – Export vite --profile JSON and Next.js next build --profile to a dashboard (Grafana, Datadog). Watch for regression >10 % in median build time.

Common Mistakes & Anti‑Patterns

#MistakeWhy It HurtsFix
1Committing node_modules/.vite or .next/cachePollutes repo, causes flaky CI, bloats clone size.Add to .gitignore; restore via CI cache.
2Using require() in Vite library codeBreaks ESM‑only tree‑shaking, forces CommonJS shim.Write library entry as export only; provide exports map in package.json.
3Disabling React Fast Refresh globally (fastRefresh: false)Loses state on every edit, slows iteration.Keep enabled; if a component misbehaves, fix the component (e.g., avoid side‑effects in module scope).
4Next.js getServerSideProps for every pageForces full SSR on each request, kills edge caching.Prefer fetch(..., { next: { revalidate: 60 } }) in Server Components or generateStaticParams for static routes.

Performance Considerations

MetricVite (esbuild + Rollup)Next.js (Turbopack)
Cold dev start~300 ms (esbuild pre‑bundle)~1.2 s (Turbopack graph build)
HMR latency<50 ms per module (WebSocket)~80 ms (Fast Refresh + Turbopack)
Production bundle sizeRollup tree‑shakes aggressively; typical 120 KB gz for a medium appNext.js splits by route + React Server Components; initial HTML ~30 KB, client JS ~80 KB
Memory during buildesbuild ~150 MB, Rollup ~300 MBTurbopack ~250 MB (parallel workers)
ScalabilityLinear with number of entry points; good for micro‑frontendsIncremental compilation scales sub‑linearly; better for monorepos with shared UI packages

Big‑O perspective: Vite’s dependency graph construction is O(D) where D = number of external deps (esbuild). Next.js’s Turbopack builds a O(F log F) graph where F = total files, but caches incremental results, making subsequent runs near O(Δ) (changed files only).

Real‑World Usage

  • Netflix – Uses Vite for internal tooling dashboards (React + TypeScript) because the team values zero‑config dev server and fast HMR across 30+ micro‑frontends. They ship a custom Rollup plugin that injects feature‑flag manifests at build time.
  • Vercel (Next.js creator) – Dogfoods Next.js on the edge for the Vercel dashboard. Streaming SSR with Suspense reduces Time‑To‑First‑Byte (TTFB) to <200 ms globally. Turbopack cuts their monorepo CI build from 12 min to 3 min.
  • Shopify – Adopted Vite for the “Hydrogen” storefront SDK. The SDK ships as an ESM‑only package; Vite’s native ESM dev server matches the production runtime (no bundler mismatch).
  • Airbnb – Migrated a legacy CRA codebase to Next.js app/ directory to enable ISR for listing pages. They report a 35 % reduction in server‑side render CPU after moving data fetching to Server Components.

Frequently Asked Questions (FAQ)

Q: Can I run Vite and Next.js in the same monorepo?
A: Yes. Share a tsconfig.base.json and a package.json workspace. Keep each app in its own folder (apps/web-vite, apps/web-next) with independent lockfiles or a single root lockfile if versions align.

Q: Does Vite support React Server Components?
A: Not natively. You can experiment with @vitejs/plugin-react + a custom SSR server, but you lose the integrated streaming and data‑fetching model that Next.js provides out‑of‑the‑box.

Q: Is Turbopack production‑ready?
A: As of Next.js 14.2 it’s stable for next dev. Production builds still use Webpack by default; enable turbo: { resolveAlias: … } only for local development.

Q: How do I migrate a large CRA codebase without a big‑bang rewrite?
A: 1) Add Vite as a dev dependency, create vite.config.ts mirroring CRA’s aliases. 2) Run both dev servers side‑by‑side, route traffic via a local proxy. 3) Incrementally move routes to Vite/Next.js, delete react-scripts once coverage >90 %.

Q: What about CSS‑in‑JS (styled‑components, emotion) performance?
A: Vite’s esbuild transforms CSS‑in‑JS at ~2× speed vs. Babel. Next.js 14 adds styledComponents compiler option (SWC) that strips runtime in production. Prefer compile‑time extraction for both.

Conclusion

CRA’s retirement is a signal, not a crisis. The ecosystem now offers two mature paths: Vite for lightweight, framework‑agnostic SPAs and libraries; Next.js for full‑stack React with SSR, streaming, and edge deployment. Choose based on where your HTML must be generated and how your team prefers to manage routing and data fetching. Instrument your builds, cache aggressively, and keep the TypeScript config shared — those habits pay dividends regardless of the toolchain.

Tags:#web development#dead#create#react
L

Written by Lead Frontend & Web Architect

Editorial staff persona leading coverage on modern web architectures, state management, web performance optimization, and client-side framework engineering.

View Profile
Recommended For You

Related Articles

Quick:
Navigate Select
Loading search index...