Next.jsReactFrontend

Next.js vs React: Was ist besser für dein Projekt?

20. Februar 202613 Min. Lesezeit

Die Frage ist falsch gestellt — aber die Antwort ist trotzdem wichtig

„Next.js oder React?" ist wie „Auto oder Motor?". Next.js ist React — mit Routing, Server-Rendering, API-Layer und Deployment-Pipeline drumherum. React liefert das Komponentenmodell. Next.js liefert alles andere.

Trotzdem ist die Frage berechtigt, denn die Entscheidung „React SPA mit Vite" vs. „Next.js App Router" hat massive Auswirkungen auf Architektur, Performance, Developer Experience und langfristige Wartbarkeit. Wir bauen wöchentlich mit beiden Varianten und zeigen dir hier den ehrlichen Vergleich — mit echtem Code, echten Zahlen und echten Projektentscheidungen.

Projektstruktur im Vergleich

Bevor wir in den Code einsteigen, hilft es zu sehen, wie sich dieselbe Anwendung in beiden Welten organisiert. Nehmen wir eine typische App mit Produktliste, Detailseite, Dashboard und API:

React SPA (Vite)                     Next.js App Router
────────────────────                 ────────────────────
src/                                 app/
├── main.tsx                         ├── layout.tsx
├── App.tsx                          ├── page.tsx
├── router.tsx          ←→           ├── produkte/
├── pages/                           │   ├── page.tsx
│   ├── Home.tsx                     │   └── [slug]/
│   ├── Produkte.tsx                 │       └── page.tsx
│   ├── ProduktDetail.tsx            ├── dashboard/
│   └── Dashboard.tsx                │   └── page.tsx
├── components/                      ├── api/
│   ├── Header.tsx                   │   └── produkte/
│   ├── ProduktCard.tsx              │       └── route.ts
│   └── LoadingSpinner.tsx           ├── components/
├── hooks/                           │   ├── Header.tsx
│   ├── useProdukte.ts               │   └── ProduktCard.tsx
│   └── useAuth.ts                   └── lib/
├── api/                                 ├── db.ts
│   └── client.ts                        └── auth.ts
└── lib/
    └── utils.ts

+ separates Backend (Express)        Alles in einem Repo.
  auf eigenem Port/Server            API Routes leben neben den Pages.

Der Unterschied ist sofort sichtbar: Bei React SPA brauchst du ein separates Backend, einen separaten Router, und du musst selbst entscheiden, wo was hingehört. Next.js gibt dir eine Convention, die dein Team vom ersten Tag an versteht.

Vergleich 1: Routing

React SPA — React Router v7

// src/router.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Home } from "./pages/Home";
import { Produkte } from "./pages/Produkte";
import { ProduktDetail } from "./pages/ProduktDetail";
import { Dashboard } from "./pages/Dashboard";
import { Layout } from "./components/Layout";
import { ProtectedRoute } from "./components/ProtectedRoute";

const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      { path: "/", element: <Home /> },
      { path: "/produkte", element: <Produkte /> },
      { path: "/produkte/:slug", element: <ProduktDetail /> },
      {
        path: "/dashboard",
        element: (
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        ),
      },
    ],
  },
]);

export function App() {
  return <RouterProvider router={router} />;
}

Next.js — App Router

app/
├── layout.tsx              ← Automatisch das Layout
├── page.tsx                ← Route: /
├── produkte/
│   ├── page.tsx            ← Route: /produkte
│   └── [slug]/
│       └── page.tsx        ← Route: /produkte/:slug
└── dashboard/
    ├── layout.tsx          ← Eigenes Layout mit Auth-Check
    └── page.tsx            ← Route: /dashboard
// app/produkte/[slug]/page.tsx
export default async function ProduktSeite({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const produkt = await getProdukt(slug);

  return <ProduktDetail produkt={produkt} />;
}

Der Punkt: Kein Router-Setup, keine Konfigurationsdatei, keine Import-Kette. Eine Datei im richtigen Ordner = eine Route. Das skaliert bei 5 Seiten genau wie bei 500.

Vergleich 2: Data Fetching

Hier wird der Unterschied am deutlichsten spürbar — sowohl im Code als auch in der User Experience.

React SPA — useEffect + TanStack Query

// src/pages/Produkte.tsx
import { useQuery } from "@tanstack/react-query";

export function Produkte() {
  const {
    data: produkte,
    isLoading,
    error,
  } = useQuery({
    queryKey: ["produkte"],
    queryFn: async () => {
      const res = await fetch("https://api.example.com/produkte");
      if (!res.ok) throw new Error("Fehler beim Laden");
      return res.json();
    },
  });

  if (isLoading) {
    return (
      <div className="flex justify-center p-12">
        <div className="animate-spin h-8 w-8 border-4 border-blue-500
                        border-t-transparent rounded-full" />
      </div>
    );
  }

  if (error) {
    return <p className="text-red-500">Fehler: {error.message}</p>;
  }

  return (
    <div className="grid grid-cols-3 gap-6">
      {produkte.map((p) => (
        <ProduktCard key={p.id} produkt={p} />
      ))}
    </div>
  );
}

Next.js — Server Component

// app/produkte/page.tsx
import { db } from "@/lib/db";
import { ProduktCard } from "@/components/ProduktCard";

export default async function ProdukteSeite() {
  const produkte = await db.produkt.findMany({
    orderBy: { createdAt: "desc" },
  });

  return (
    <div className="grid grid-cols-3 gap-6">
      {produkte.map((p) => (
        <ProduktCard key={p.id} produkt={p} />
      ))}
    </div>
  );
}

Was hier passiert: Die Next.js-Version hat keinen Loading-State, keinen Error-Boundary-Boilerplate, keine API-Zwischenschicht. Die Komponente fetcht Daten direkt auf dem Server — mit einem einzigen await. Der HTML-Output geht fertig gerendert an den Browser. Das JavaScript für db, findMany und die Fetch-Logik wird nicht an den Client geschickt.

Das heisst konkret: Die React-SPA-Version schickt TanStack Query (~13 KB gzipped) plus deine Fetch-Logik plus Loading-Spinner-Code an jeden Client. Die Server-Component-Version schickt nur das fertige HTML und den Code fuer die interaktive ProduktCard.

Vergleich 3: API Handling

React SPA — Separater Express-Server

// backend/src/routes/produkte.ts (Express)
import { Router } from "express";
import { db } from "../lib/db";
import { authenticate } from "../middleware/auth";

const router = Router();

router.get("/api/produkte", async (req, res) => {
  try {
    const { kategorie, sort } = req.query;

    const produkte = await db.produkt.findMany({
      where: kategorie ? { kategorie: String(kategorie) } : undefined,
      orderBy: { preis: sort === "asc" ? "asc" : "desc" },
    });

    res.json(produkte);
  } catch (error) {
    res.status(500).json({ error: "Interner Fehler" });
  }
});

router.post("/api/produkte", authenticate, async (req, res) => {
  const produkt = await db.produkt.create({ data: req.body });
  res.status(201).json(produkt);
});

export default router;

Dazu brauchst du: einen separaten Server-Prozess, CORS-Konfiguration, eigenes Error-Handling, Port-Management, separate Deployment-Pipeline.

Next.js — Route Handler

// app/api/produkte/route.ts
import { db } from "@/lib/db";
import { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const kategorie = searchParams.get("kategorie");
  const sort = searchParams.get("sort");

  const produkte = await db.produkt.findMany({
    where: kategorie ? { kategorie } : undefined,
    orderBy: { preis: sort === "asc" ? "asc" : "desc" },
  });

  return NextResponse.json(produkte);
}

export async function POST(request: NextRequest) {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await request.json();
  const produkt = await db.produkt.create({ data: body });

  return NextResponse.json(produkt, { status: 201 });
}

Gleiche Logik, gleiche Datenbankabfragen — aber kein separater Server, kein CORS, kein Port-Management. Die Route lebt direkt neben deinen Pages und wird zusammen deployt. Und wenn du Server Components verwendest, brauchst du fuer GET-Requests oft gar keine API Route, weil du direkt in der Komponente auf die Datenbank zugreifen kannst.

Vergleich 4: SEO und Meta-Tags

React SPA — react-helmet-async

// src/pages/ProduktDetail.tsx
import { Helmet } from "react-helmet-async";
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";

export function ProduktDetail() {
  const { slug } = useParams();
  const { data: produkt } = useQuery({
    queryKey: ["produkt", slug],
    queryFn: () => fetch(`/api/produkte/${slug}`).then((r) => r.json()),
  });

  if (!produkt) return null;

  return (
    <>
      <Helmet>
        <title>{produkt.name} | MeinShop</title>
        <meta name="description" content={produkt.beschreibung} />
        <meta property="og:title" content={produkt.name} />
        <meta property="og:image" content={produkt.bild} />
      </Helmet>
      <div>{/* ... Produkt-UI ... */}</div>
    </>
  );
}

Das Problem: Suchmaschinen sehen beim Crawl zuerst ein leeres <div id="root">. Der Helmet-Inhalt wird erst nach dem JavaScript-Download, dem API-Call und dem Rendering eingefuegt. Googlebot kann das mittlerweile, aber es ist langsamer, unzuverlaessiger und bei Social-Media-Previews (Open Graph) funktioniert es gar nicht.

Next.js — generateMetadata

// app/produkte/[slug]/page.tsx
import { db } from "@/lib/db";
import { Metadata } from "next";

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const produkt = await db.produkt.findUnique({ where: { slug } });

  return {
    title: `${produkt.name} | MeinShop`,
    description: produkt.beschreibung,
    openGraph: {
      title: produkt.name,
      images: [produkt.bild],
    },
  };
}

export default async function ProduktSeite({ params }: Props) {
  const { slug } = await params;
  const produkt = await db.produkt.findUnique({ where: { slug } });

  return <div>{/* ... Produkt-UI ... */}</div>;
}

Das Ergebnis: Meta-Tags werden auf dem Server gerendert und sind im initialen HTML enthalten. Jeder Crawler — Google, LinkedIn, Twitter, Slack-Previews — sieht sofort die richtigen Tags. Kein Flicker, kein Warten auf Client-JavaScript.

Server Components im Detail

Server Components sind der groesste architektonische Shift in React seit Hooks. Hier ein realistischeres Beispiel — ein Dashboard-Widget, das Umsatzdaten anzeigt:

Traditionelles React (Client Component)

"use client";
import { useState, useEffect } from "react";

export function UmsatzWidget() {
  const [daten, setDaten] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [zeitraum, setZeitraum] = useState("30d");

  useEffect(() => {
    setLoading(true);
    fetch(`/api/umsatz?zeitraum=${zeitraum}`)
      .then((r) => {
        if (!r.ok) throw new Error("API-Fehler");
        return r.json();
      })
      .then(setDaten)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [zeitraum]);

  if (loading) return <Skeleton className="h-48" />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div>
      <ZeitraumFilter value={zeitraum} onChange={setZeitraum} />
      <UmsatzChart daten={daten} />
      <p className="text-2xl font-bold">
        {new Intl.NumberFormat("de-DE", {
          style: "currency",
          currency: "EUR",
        }).format(daten.gesamt)}
      </p>
    </div>
  );
}

Next.js Server Component + Client-Interaktivitaet

// app/dashboard/components/UmsatzWidget.tsx (Server Component)
import { db } from "@/lib/db";
import { UmsatzChart } from "./UmsatzChart"; // Client Component
import { ZeitraumFilter } from "./ZeitraumFilter"; // Client Component

export default async function UmsatzWidget({
  zeitraum = "30d",
}: {
  zeitraum?: string;
}) {
  const daten = await db.bestellung.aggregate({
    _sum: { betrag: true },
    _count: true,
    where: {
      createdAt: {
        gte: getStartDate(zeitraum),
      },
    },
  });

  const verlauf = await db.$queryRaw`
    SELECT DATE(created_at) as tag, SUM(betrag) as umsatz
    FROM bestellungen
    WHERE created_at >= ${getStartDate(zeitraum)}
    GROUP BY DATE(created_at)
    ORDER BY tag
  `;

  return (
    <div>
      <ZeitraumFilter current={zeitraum} />
      <UmsatzChart daten={verlauf} />
      <p className="text-2xl font-bold">
        {new Intl.NumberFormat("de-DE", {
          style: "currency",
          currency: "EUR",
        }).format(daten._sum.betrag)}
      </p>
    </div>
  );
}

Was das Bundle betrifft: Die Client-Version schickt die gesamte Komponente plus fetch-Logik, Error-Handling und Loading-States an den Browser. Die Server-Version schickt nur den UmsatzChart und den ZeitraumFilter — die interaktiven Teile. Die Datenbankabfrage, die SQL-Logik, Prisma, die Aggregation — all das bleibt auf dem Server und erhoet dein Client-Bundle um exakt 0 Bytes.

Performance-Zahlen: Real-World-Vergleich

Typische Lighthouse-Werte fuer eine Marketing-Site mit App-Bereich (gemessen auf Mobilgeraet mit 4G-Throttling):

| Metrik | React SPA (Vite) | Next.js (App Router) | Differenz | |---------------------------|-------------------|----------------------|---------------| | First Contentful Paint | 2.4 s | 0.8 s | 3x schneller | | Largest Contentful Paint | 4.1 s | 1.6 s | 2.5x schneller | | Time to Interactive | 4.8 s | 2.1 s | 2.3x schneller | | Total Blocking Time | 680 ms | 120 ms | 5.7x weniger | | Lighthouse Performance | 52 | 94 | +42 Punkte | | Initiales JS-Bundle | ~210 KB gzip | ~85 KB gzip | -60% |

Warum der Unterschied so gross ist: Eine React SPA muss erst das gesamte JavaScript-Bundle laden, parsen und ausfuehren, bevor der Nutzer irgendetwas sieht. Next.js schickt server-gerendertes HTML, das sofort dargestellt wird — JavaScript wird danach nachgeladen und hydriert. Fuer Marketing-Seiten und Content-lastige Bereiche ist das ein enormer Vorteil.

Wichtig: Fuer reine App-Bereiche hinter einem Login (Dashboards, Tools) sind die Unterschiede deutlich kleiner, weil SEO und First-Paint weniger kritisch sind.

Entscheidungsbaum: Was brauchst du wirklich?

                         ┌──────────────────────┐
                         │   Neues Web-Projekt   │
                         └──────────┬───────────┘
                                    │
                         ┌──────────▼───────────┐
                         │  Braucht SEO-Traffic? │
                         └──────────┬───────────┘
                            Ja ─────┼───── Nein
                            │       │        │
                    ┌───────▼──┐    │   ┌────▼──────────────┐
                    │ Next.js  │    │   │ Fullstack in einem │
                    │          │    │   │ Repo gewuenscht?   │
                    └──────────┘    │   └────┬──────────────┘
                                    │   Ja ──┼──── Nein
                                    │   │    │       │
                                    │ ┌─▼────────┐ ┌─▼──────────┐
                                    │ │ Next.js  │ │ React SPA  │
                                    │ │          │ │ (Vite)     │
                                    │ └──────────┘ └────┬───────┘
                                    │                    │
                                    │            ┌───────▼────────┐
                                    │            │ Wird spaeter   │
                                    │            │ SEO relevant?  │
                                    │            └───────┬────────┘
                                    │           Ja ──────┼──── Nein
                                    │           │        │       │
                                    │    ┌──────▼─────┐  │  ┌───▼────────┐
                                    │    │ Doch lieber│  │  │ React SPA  │
                                    │    │ Next.js    │  │  │ ist perfekt│
                                    │    └────────────┘  │  └────────────┘

Die ehrliche Kurzversion: Wenn du nicht sicher bist, nimm Next.js. Die Lernkurve ist etwas steiler, aber du hast alle Optionen offen. Von Next.js zurueck zu einer React SPA migrieren ist einfach — umgekehrt ist es ein kompletter Rewrite.

Wann wir was einsetzen

Die Theorie ist das eine. Hier sind echte Entscheidungen aus unseren letzten Projekten:

Projekt A: E-Commerce Replatform

Entscheidung: Next.js

Ein Haendler mit 2.000+ Produktseiten brauchte schnelle Ladezeiten und perfektes SEO. Wir haben Static Site Generation (SSG) fuer Produktseiten eingesetzt, die bei jeder Aenderung im CMS automatisch per Incremental Static Regeneration (ISR) aktualisiert werden. Ergebnis: Produktseiten laden in unter 1 Sekunde, Organic Traffic stieg um 34% nach Relaunch.

Warum nicht React SPA? 2.000 Produktseiten ohne SSR = unsichtbar fuer Google. Kein Verhandlungsspielraum.

Projekt B: Internes Analytics Dashboard

Entscheidung: React SPA (Vite + TanStack Query)

Ein B2B-SaaS-Unternehmen brauchte ein internes Dashboard mit Echtzeit-Daten ueber WebSockets, komplexen Filterkombinationen und Drag-and-Drop-Widgets. Alles hinter einem Login, keine oeffentliche Seite.

Warum nicht Next.js? Null SEO-Anforderung, dafuer extrem interaktive UI mit WebSocket-Verbindungen, die permanent offen bleiben. Mit Server Components haetten wir staendig gegen das Framework gearbeitet, weil fast jede Komponente "use client" gebraucht haette. Hier war die einfachere Architektur die richtige Wahl.

Projekt C: SaaS mit Marketing-Site

Entscheidung: Next.js (hybrid)

Ein Startup brauchte beides: eine oeffentliche Marketing-Site mit Pricing-Page, Blog und Docs (SEO-kritisch) plus eine App hinter dem Login (interaktiv, kein SEO). Wir haben beides in einem Next.js-Projekt umgesetzt:

  • Marketing-Seiten: Static Site Generation, Server Components, generateMetadata fuer SEO
  • App-Bereich: Client Components mit TanStack Query, geschuetzt durch Middleware-Auth
  • Shared Components: Design-System, das in beiden Bereichen funktioniert

Warum nicht zwei getrennte Projekte? Ein Repo, ein Deployment, ein Design-System, ein Team. Die Wartungskosten fuer zwei getrennte Frontends haetten das Startup langfristig mehr gekostet als der initiale Mehraufwand.

Wann React SPA die bessere Wahl ist

Wir sind kein Next.js-Fanclub. Es gibt klare Faelle, in denen eine React SPA die bessere Architektur ist:

  • Electron-Apps oder Desktop-Software — kein Server vorhanden
  • Extrem interaktive Tools (Whiteboard, IDE, Grafik-Editor) — wo 95% der Logik im Client lebt
  • Micro-Frontends in einer bestehenden Architektur — wo du ein bestehendes SPA-Hosting nutzen musst
  • Bestehendes Backend-Team hat bereits eine REST/GraphQL-API — und du brauchst nur ein Frontend dafuer
  • Einfache interne Tools hinter Auth — wo SSR keinen messbaren Vorteil bringt

In diesen Faellen ist das schlankere Setup mit Vite, React Router und TanStack Query die ehrlichere Architektur. Weniger Abstraktion, schnellere Builds, einfacheres Mental Model.

Die technischen Unterschiede auf einen Blick

| Feature | React SPA (Vite) | Next.js (App Router) | |------------------------|------------------------------|-------------------------------| | Rendering | Client-only | Server + Client (hybrid) | | SEO | Schlecht (kein SSR) | Exzellent (SSR/SSG/ISR) | | Routing | React Router (manuell) | File-based (automatisch) | | Data Fetching | useEffect / TanStack Query | Server Components + await | | API Routes | Separater Server noetig | Integriert (Route Handlers) | | Image Optimization | Manuell | Automatisch (next/image) | | Bundle Size | Alles geht an den Client | Server-Code bleibt auf Server | | Deployment | Statisches Hosting (S3, CDN) | Vercel / Node-Server / Docker | | Lernkurve | Flacher | Steiler (Server vs. Client) | | Vendor Lock-in | Keins | Gering (Vercel-Optimierungen) |

Fazit

React vs. Next.js ist keine Glaubensfrage. Es ist eine Architekturentscheidung, die von deinen konkreten Anforderungen abhaengt. SEO, Performance, Team-Groesse, Deployment-Strategie — das sind die Faktoren, die zaehlen. Nicht Meinungen auf Twitter.

Unsere Faustregel nach Dutzenden Projekten mit beiden Varianten: Wenn dein Projekt eine oeffentliche Komponente hat, die in Suchmaschinen sichtbar sein muss, fuehrt an Next.js kein Weg vorbei. Fuer rein interne, hochinteraktive Anwendungen ist eine React SPA oft die sauberere Loesung.


Du planst ein neues Projekt und bist unsicher, welche Architektur die richtige ist? Wir helfen dir bei der Entscheidung — nicht theoretisch, sondern basierend auf dem, was wir jede Woche in Production deployen. Sprich mit uns ueber dein Projekt.

SS
SecretStack

KI-natives Fullstack Engineering Studio. Wir bauen, was andere nicht sehen — mit Senior Engineers, NDA ab dem ersten Kontakt.

NDA-first
Senior-only
120+ Projects

Next.js Projekt starten?

Du möchtest dein Projekt mit Senior Engineers besprechen?

Kostenlos beraten lassen