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
PlayingClick play to listen to audio narration
Table of Contents
- •Create React App is Dead. What’s Next? (Vite vs. Next.js)
- •Introduction
- •Why This Matters
- •How It Works
- •Core Concepts
- •Examples & Code Walkthrough
- •1. Minimal Vite + React + TypeScript Setup
- •2. Next.js 14 app/ Directory with Streaming SSR
- •Best Practices
- •Common Mistakes & Anti‑Patterns
- •Performance Considerations
- •Real‑World Usage
- •Frequently Asked Questions (FAQ)
- •Conclusion
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 Pre‑bundle (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[Server‑Side 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
- Request entry – Both tools start with an HTTP request from the browser.
- 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. - 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.
- 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
| Concept | Vite | Next.js |
|---|---|---|
| Module Format | Native ESM in dev, Rollup (ESM/CommonJS) in prod | Mixed: Server components compiled to CommonJS for Node, client bundles ESM |
| Dependency Pre‑bundling | esbuild (single‑threaded, O(n) for deps) | Turbopack (incremental, parallel) or Webpack |
| Routing | Manual (React Router, wouter, etc.) | File‑system based (pages/ or app/) |
| Rendering Modes | SPA only (unless you add a custom SSR server) | SSR, SSG, ISR, Edge SSR, Streaming |
| API Layer | None (add Express/Fastify) | Built‑in API routes (pages/api or app/api) |
| Configuration Surface | vite.config.ts + Rollup plugins | next.config.js + Turbopack plugins (experimental) |
| TypeScript Integration | tsconfig.json + vite-plugin-checker | First‑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-pathsmirrors TypeScript path mapping without duplicating config.visualizeris gated behindmode === 'production'to avoid dev‑server overhead.esbuildminify 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:
revalidateopts into Incremental Static Regeneration (ISR) without a custom server.Suspenseboundary streams the chart HTML while the shell stays interactive.- Turbopack alias mirrors the Vite
tsconfigPathsbehavior, keeping import paths identical across repos.
Best Practices
- Lock the Node version – Both toolchains rely on native bindings (esbuild, Turbopack). Pin
nodeinpackage.json("engines": { "node": ">=20.10.0 <21" }) and enforce via.nvmrc+ CI. - Separate dev/prod configs – Keep
vite.config.tsandnext.config.jslean; use environment‑specific files (vite.config.prod.ts,next.config.prod.js) for heavy plugins (bundle analyzer, Sentry source maps). - Cache aggressively in CI – Cache
node_modules, Vite’snode_modules/.vite, and Next.js’s.next/cache. In GitHub Actions,actions/cache@v4with a key that includespackage-lock.jsonhash. - Adopt a shared TypeScript base – A monorepo
tsconfig.base.jsonwithpathseliminates drift between Vite and Next.js projects. - Instrument build metrics – Export
vite --profileJSON and Next.jsnext build --profileto a dashboard (Grafana, Datadog). Watch for regression >10 % in median build time.
Common Mistakes & Anti‑Patterns
| # | Mistake | Why It Hurts | Fix |
|---|---|---|---|
| 1 | Committing node_modules/.vite or .next/cache | Pollutes repo, causes flaky CI, bloats clone size. | Add to .gitignore; restore via CI cache. |
| 2 | Using require() in Vite library code | Breaks ESM‑only tree‑shaking, forces CommonJS shim. | Write library entry as export only; provide exports map in package.json. |
| 3 | Disabling 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). |
| 4 | Next.js getServerSideProps for every page | Forces full SSR on each request, kills edge caching. | Prefer fetch(..., { next: { revalidate: 60 } }) in Server Components or generateStaticParams for static routes. |
Performance Considerations
| Metric | Vite (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 size | Rollup tree‑shakes aggressively; typical 120 KB gz for a medium app | Next.js splits by route + React Server Components; initial HTML ~30 KB, client JS ~80 KB |
| Memory during build | esbuild ~150 MB, Rollup ~300 MB | Turbopack ~250 MB (parallel workers) |
| Scalability | Linear with number of entry points; good for micro‑frontends | Incremental 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
Suspensereduces 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.
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.