Next.js Pages Router zu App Router: Praxisleitfaden mit Migrationsstrategie und Fallen-Checkliste

Der Tech Lead stellte in der Besprechung die Frage: „Können wir unser Next.js-12-Projekt auf 14 heben?“
Ich starrte auf den zwei Jahre alten Code – beim letzten React-17-Upgrade hatten wir eine Woche lang Bugs gefixt, der Support war am Limit.
Diesmal wirkte es anders. Abends in der Doku: App Router, Server Components, verschachtelte Layouts, bessere Performance – verlockend. Dann die Migrationsseite: API-Vergleichstabellen, getServerSideProps, _app.js aufteilen – Kopfschmerzen.
Noch unangenehmer: Die offizielle „schrittweise Migration“ klingt gut, aber beim Wechsel zwischen /pages und /app sehen Nutzer Loading-Spinner – die UX wird schlechter, nicht besser.
Zwei Wochen Fallen, Community-Threads und verschiedene Ansätze später hatte ich eine Strategie, der ich vertraue. In diesem Artikel teile ich:
- Wie Sie einschätzen, ob sich die Migration lohnt
- Zwei Strategien mit echten Vor- und Nachteilen (nicht nur Theorie aus der Doku)
- getServerSideProps Schritt für Schritt mit Code
- Sieben Fallen, in die ich selbst getappt bin – und wie Sie sie vermeiden
Wenn Sie unsicher sind oder mitten in der Migration hängen, soll Ihnen das ein paar Umwege ersparen.
Warum migrieren? Erst die Rechnung aufmachen
Nicht jedes Projekt lohnt den Aufwand.
Ein Bekannter fragte, ob ihre bald endende Kampagnen-Site auf den App Router soll. Mein Rat: nein – Code, der in sechs Monaten weg ist, braucht kein Upgrade.
Wann lohnt es sich? Meine Kriterien:
Bedarf an verschachtelten Layouts
Das war bei uns der Hauptgrund. Unser SaaS-Backend: Sidebar, Top-Nav, Content – drei Ebenen. Im Pages Router renderte die Sidebar bei jedem Seitenwechsel komplett neu.
Nutzer spürten ein Flackern – nicht langsames Netz, sondern Layout-Neuzeichnung.
Verschachtelte Layouts im App Router lösen das. WorkOS berichtet von „deutlich besserem Login ohne Loading oder Layout-Jitter“ – bei uns passt das: nur der Content-Bereich aktualisiert sich, die Navigation bleibt stabil.
Performance-Potenzial
First Paint über drei Sekunden? Der App Router kann helfen.
Unsere Produktliste nutzte früher getServerSideProps – jeder Refresh wartete auf das komplette serverseitige HTML. Mit Server Components streamen wir Listendaten vom Server; First Paint von 3,2 s auf 1,8 s.
Aber: Nicht jede Seite wird schneller. Rein clientseitige Interaktion (z. B. Canvas-Editor) profitiert kaum – manchmal wird es durch eine zusätzliche Abstraktionsschicht etwas langsamer.
Langfristige Wartung
Projekt drei Jahre oder länger? Früher migrieren, früher profitieren. Vercel stellt neue Features primär für den App Router bereit; der Pages Router ist im Wartungsmodus.
Ich wollte nicht in zwei Jahren erneut migrieren – mit dann vielleicht wieder anderen APIs und mehr Fallen.
Wann ich abrate
- Projekt geht bald offline – unnötiger Aufwand
- Kleine statische Site (≤5 Seiten) – Nutzen zu gering
- Team kennt React 18 noch nicht – erst Suspense und Server Components verstehen
- Viele alte Drittanbieter-Bibliotheken – Kompatibilitätsprobleme warten
Kernbotschaft: Nicht migrieren um der Migration willen. Welches konkrete Problem löst der Wechsel? Wenn die Antwort „keins, nur Neugier“ ist – lieber warten.
Unsere Rechnung: zwei Wochen Aufwand für bessere UX und weniger Technikschulden über drei Jahre – für uns stimmig. Wie sieht es bei Ihrem Projekt aus?
Zwei Migrationsstrategien im Vergleich
Die Doku empfiehlt „schrittweise Migration“ – langsam, Seite für Seite.
In der Praxis hat das einen harten Nachteil.
Die Falle der schrittweisen Migration
Szenario: Startseite liegt unter /app, Produktdetail noch unter /pages. Nutzer klickt von der Startseite ins Detail – weißer Bildschirm, Spinner, dann Inhalt.
Grund: Wechsel zwischen App Router und Pages Router – für Next.js zwei getrennte Welten; das JavaScript-Bundle muss neu geladen werden. UX wie vor zehn Jahren.
WorkOS formuliert es so: Navigation zwischen Routern fühlt sich an wie ein Sprung zwischen zwei unabhängigen Apps. Sie wollten schrittweise migrieren – und haben es aufgegeben.
Ist schrittweise also unmöglich? Nein.
Passt gut, wenn:
- Seiten wenig gekoppelt sind (z. B. Blog)
- Sie ganze Module auf einmal migrieren (zuerst User-Bereich, dann Shop)
- Loading beim Router-Wechsel akzeptabel ist
Bei einem Tech-Blog habe ich das gesehen – ok. Bei SaaS oder E-Commerce würde ich es meiden.
WorkOS Zero-Downtime-Ansatz
Für komplexe Projekte: Seiten unter /app/new neu bauen, Versionswechsel per Query-Parameter.
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/:path*',
destination: '/new/:path*',
has: [
{
type: 'query',
key: 'new',
value: 'true',
},
],
},
]
},
}
Normale Nutzer sehen unter /dashboard die alte Version; mit ?new=true die neue.
QA, Produkt und Design testen produktiv – Nutzer merken nichts. Wenn alles steht: /app/new nach /app, /pages löschen, fertig.
So haben wir es gemacht. Kein User-Bug vor dem Go-Live – eine Woche mit echten Daten getestet.
Ablauf:
- Next.js 14 upgraden – /pages vorerst unangetastet
- Routing-Hooks migrieren –
next/router→next/navigation, Kompatibilität mit beiden Routern - Verzeichnis /app/new – Seitenstruktur dort neu
- Bestehende Komponenten wiederverwenden – aus /pages importieren, nicht alles neu schreiben
- rewrites konfigurieren – wie oben, Umschalten mit
?new=true - Intern testen + Canary – Team auf neuer Version, Bugs fixen
- Produktiv – /app/new → /app, rewrites und /pages entfernen
Bei uns: zehn Arbeitstage – sechs Tage Seiten, drei Tage Bugs, ein Tag Launch.
Meine Empfehlung
- Weniger als 10 Seiten, geringe Kopplung → schrittweise
- Mehr als 10 Seiten, hohe UX-Anforderungen → Zero-Downtime
- Neues Projekt → direkt App Router
Feature-Entwicklung parallel zur Migration habe ich probiert – zwei Code-Stile, unangenehm. Entweder zwei Wochen fokussiert migrieren – oder noch warten.
getServerSideProps in der Praxis migrieren
Die häufigste Frage: „getServerSideProps gibt es nicht mehr – wie hole ich Daten?“
Im App Router ist es oft einfacher – mit anderem Denkmodell.
Von „getrennt“ zu „zusammen“
Pages Router: Datenschicht (getServerSideProps) und UI getrennt; Next.js ruft die Datenfunktion serverseitig auf und übergibt props.
App Router: Die Seitenkomponente ist async und lädt selbst:
// ❌ Alt: pages/project/[id].tsx
export async function getServerSideProps(context) {
const { id } = context.params
const res = await fetch(`https://api.example.com/projects/${id}`)
const project = await res.json()
return {
props: { project }
}
}
export default function ProjectPage({ project }) {
return <h1>{project.title}</h1>
}
// ✅ Neu: app/project/[id]/page.tsx
export default async function ProjectPage({ params }) {
const { id } = params
const res = await fetch(`https://api.example.com/projects/${id}`, {
cache: 'no-store' // Wichtig – entspricht getServerSideProps
})
const project = await res.json()
return <h1>{project.title}</h1>
}
Zwei große Fallen folgen sofort.
Falle 1: Falsche Cache-Konfiguration
Standard: fetch im App Router ist gecacht (wie getStaticProps), nicht bei jedem Request frisch.
Bei uns blieb der Produktpreis nach der Migration stehen – Nutzer beschwerten sich über veraltete Preise. Ursache: Cache.
Merktabelle:
getServerSideProps→cache: 'no-store'getStaticProps→cache: 'force-cache'(Standard)getStaticProps + revalidate→next: { revalidate: 60 }
Falle 2: Client-State
Seiten mit getServerSideProps hatten oft Filter, Sortierung – Client-State.
Problem: async Server Components dürfen kein useState, useEffect usw.
Lösung: Komponenten trennen.
// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
const products = await fetchProducts() // Server
return <ProductList initialData={products} /> // an Client Component
}
// components/ProductList.tsx (Client Component)
'use client' // Diese Zeile!
import { useState } from 'react'
export function ProductList({ initialData }) {
const [products, setProducts] = useState(initialData)
const [filter, setFilter] = useState('')
// Client-Filter
const filtered = products.filter(p => p.name.includes(filter))
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
Server lädt Daten, Client übernimmt Interaktion – klar getrennt.
Nicht die ganze Seite mit "use client" markieren – dann verlieren Server Components ihren Sinn.
Praktische Migrationsschritte
Schritt 1: Komponenten trennen
Noch in /pages: reine Darstellung vs. Stateful – testen.
Schritt 2: Nach /app verschieben
- Darstellung →
app/[route]/page.tsx, async, Daten dort laden - Stateful → eigene Datei mit
"use client" - getServerSideProps entfernen
Vorteil: Schnelles Rollback, wenn etwas schiefgeht.
Cookies und Header
Früher context.req.cookies – jetzt:
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = cookies()
const token = cookieStore.get('auth-token')
// Token für User-Daten nutzen...
}
Analog: headers(), redirect() aus next/headers bzw. next/navigation – vollständige Liste in der offiziellen Doku.
Sieben häufige Fallen und Lösungen
Diese sieben habe ich alle selbst erlebt – jeweils mindestens eine Stunde Debug.
Falle 1: Serverfehler verschwinden
Symptom: Seite rendert nicht, kein Fehler in der Konsole – Loading-Skeleton oder Leerseite.
Bei uns: API geändert, Seite leer. Kein Fehler in der Konsole. Ohne error.tsx schluckt Next.js den Serverfehler und zeigt Suspense-Fallback.
Lösung: error.tsx pro Route:
// app/dashboard/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Fehler: {error.message}</h2>
<button onClick={reset}>Erneut versuchen</button>
</div>
)
}
In der Entwicklung: Stacktrace. In Produktion: verständliche Meldung.
Falle 2: useRouter „funktioniert nicht“
Symptom: useRouter().push() navigiert nicht oder Methoden fehlen.
Ursache: next/router und next/navigation sind verschiedene APIs.
// ❌ Falsch gedacht
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard') // push existiert – Verhalten anders!
Besser oft die Link-Komponente:
// ✅ Praxis
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
const router = useRouter()
router.push('/dashboard') // geht, aber Link ist oft klarer
import Link from 'next/link'
<Link href="/dashboard">Zum Dashboard</Link>
Spickzettel:
| Pages Router | App Router |
|---|---|
useRouter().push(url) | useRouter().push(url) (vorhanden, Link bevorzugen) |
useRouter().pathname | usePathname() |
useRouter().query | useSearchParams() |
useRouter().asPath | usePathname() + useSearchParams() |
Falle 3: Dynamic Import bricht
Symptom: next/dynamic rendert nicht – Fehler zu useState nur in Client Components.
Ursache: Server Components rendern serverseitig; client-only Bibliotheken (Charts) scheitern.
// ❌ Fehler
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export default function Page() {
return <Chart data={data} />
}
Chart braucht window – auf dem Server nicht vorhanden.
Lösung: page.tsx als Client Component – oder Chart in Client Component auslagern:
// app/charts/page.tsx
import { ClientChart } from './ClientChart'
export default function Page() {
return <ClientChart />
}
// app/charts/ClientChart.tsx
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export function ClientChart() {
return <Chart data={data} />
}
Falle 4: Flackern beim Seitenwechsel
Symptom: Link-Klick – ganze Seite neu, Header und Sidebar flackern.
Ursache: Kein oder falsches layout.tsx; Navigation in jeder page.tsx.
Richtig:
// app/layout.tsx (Root – alle Seiten)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header /> {/* Top-Nav – bleibt stabil */}
{children}
</body>
</html>
)
}
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar /> {/* Sidebar bleibt beim Wechsel innerhalb Dashboard */}
<main>{children}</main>
</div>
)
}
Wechsel zwischen /dashboard/analytics und /dashboard/settings – nur main aktualisiert sich.
Falle 5: 404 greift nicht
Symptom: Eigene 404 erscheint nicht – Next.js-Standard.
Ursache: /pages/404.js kollidiert mit app/not-found.tsx.
Lösung: /pages/404.js und /pages/500.js löschen:
// app/not-found.tsx
export default function NotFound() {
return <h1>Seite nicht gefunden</h1>
}
404 aus der Seite auslösen:
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const data = await fetchData(params.id)
if (!data) {
notFound()
}
return <div>{data.title}</div>
}
Falle 6: Dev-Server wird immer langsamer
Symptom: Start ok, nach einigen Edits HMR 10 Sekunden, dann Absturz.
Ehrlich: Keine perfekte Lösung.
Bekanntes Next.js-14-Problem – FlightControl schreibt vom Dev-Server so schlecht, dass sie fast alle neuen Features dafür aufgeben würden; Neustart alle 20 Minuten.
Workarounds:
- Dev-Server regelmäßig neu starten (bei mir alle 15 Minuten)
next dev --turbo(Turbopack – schneller, gelegentliche Bugs)- Unnötige Server Components reduzieren – manche Seiten sind als Client Component sinnvoller
Next.js 15 soll hier besser werden – noch nicht im Detail getestet.
Falle 7: Drittanbieter-Bibliotheken
Symptom: Framer Motion, Lottie – window oder document fehlt.
Ursache: Rein clientseitige Bibliotheken in Server Components.
Lösung:
- Betroffene Komponenten mit
"use client" - Neuere Versionen mit React-18-Support prüfen
- Notfalls SSR-fähige Alternative
Häufig client-only:
- Framer Motion (Exit-Animationen – offene Issues)
- swiper, slick-carousel
- ECharts, Chart.js
- react-dnd, dnd-kit
Bei starker Abhängigkeit vorher GitHub-Issues lesen.
Optimierung nach der Migration
Migration ist nicht das Ende.
Weniger Client-JavaScript
Stärke der Server Components.
Unsere Produktliste: früher ~120 KB React (gzip). Nach Migration: Darstellung serverseitig, Filter/Sortierung clientseitig – ~45 KB.
Prüfen:
npm run build
Build-Ausgabe: (Static) / (SSR) vs. ○ (Client Component). Viele ○ → zu viel "use client".
Tipps:
- Statischer Inhalt → Server Component
- Formulare, Buttons → Client Component
- Nicht die ganze Seite clientseitig – nur betroffene Kinder
Cache sinnvoll nutzen
Im App Router feiner als im Pages Router.
// Kein Cache – Echtzeitdaten
fetch(url, { cache: 'no-store' })
// 60 s Cache, dann Revalidierung
fetch(url, { next: { revalidate: 60 } })
// Dauerhaft – statische Daten
fetch(url, { cache: 'force-cache' })
Produktliste: 60 s revalidate – Daten frisch genug, API-Last −60 %.
Performance messen
Vorher/Nachher:
- FCP – erstes sichtbares Content-Fragment
- TTI – Seite voll interaktiv
- CLS – Layout-Verschiebung beim Laden
Mit Vercel Analytics: FCP 3,2 s → 1,8 s, TTI 5,1 s → 3,3 s.
Nicht jede Seite wird schneller – Canvas-Editor bei uns praktisch unverändert.
Nicht überoptimieren
Nicht jede Komponente für Server Components zerlegen.
Ich habe ein Formular in 20 Mini-Komponenten geteilt – Lesbarkeit runter, Wartung teurer.
Faustregel: Braucht die Komponente useState/useEffect → "use client". Server Components sind Werkzeug, kein KPI.
Fazit
Migration ist kein Trend – sie löst echte Probleme: verschachtelte Layouts, weniger Client-JS, langfristige Wartbarkeit.
Strategie passend wählen: klein und schrittweise, groß Zero-Downtime. Parallel neue Features und Migration vermeiden.
Fallen sind normal – die sieben hier sind die Spitze des Eisbergs. GitHub-Issues durchsuchen; die meisten Probleme kennt jemand schon.
Lesbarkeit vor Bundle-Größe – Team und Codequalität zählen.
Pilot: 1–2 Seiten, Prozess verstehen, dann skalieren. Bei uns: erste Seite drei Tage, die nächsten zehn fünf Tage.
Der App Router hat Schwächen (Dev-Server!), die Richtung stimmt. Mit reiferem Ökosystem werden viele Fallen verschwinden.
Wenn Sie unterwegs hängen – melden Sie sich; vielleicht kenne ich die gleiche Falle.
Weiterführend:
- Offizielle Next.js-Migrationsanleitung
- WorkOS Zero-Downtime-Migration
- App Router FAQ (GitHub Discussions)
Viel Erfolg bei der Migration!
Vollständiger Migrationsablauf: Next.js Pages Router zu App Router
Vom Assessment bis zum Go-Live – inklusive Strategiewahl und Lösungen für häufige Probleme
⏱️ Estimated time: 80 hr
- 1
Step 1: Prüfen, ob sich die Migration lohnt
Kriterien:
• Verschachtelte Layouts: mehrere Layout-Ebenen, ohne bei Seitenwechsel neu zu rendern
• Performance-Potenzial: First Paint über 3 Sekunden, Optimierung möglich
• Langfristige Wartung: Projekt 3+ Jahre – früher migrieren, früher profitieren
Nicht migrieren, wenn:
• Projekt bald offline geht
• Kleine statische Site (≤5 Seiten)
• Team kennt React 18 noch nicht gut
• Viele veraltete Drittanbieter-Bibliotheken im Einsatz - 2
Step 2: Migrationsstrategie wählen
Je nach Projektgröße:
Kleines Projekt (<10 Seiten, unabhängig) → Schrittweise Migration:
• Seite für Seite migrieren
• Loading-Zustand beim Wechsel akzeptierbar
• Gut für Blogs mit geringer Seitenkopplung
Großes Projekt (>10 Seiten, hohe UX-Anforderungen) → Zero-Downtime:
• Alle Seiten unter /app/new neu aufbauen
• Versionswechsel per rewrites + Query-Parameter
• Nach internem Test produktiv schalten - 3
Step 3: getServerSideProps migrieren
Schritte:
1. Seitenkomponente zu async-Funktion machen
2. Daten direkt in der Komponente per fetch laden
3. Cache korrekt setzen:
• getServerSideProps → cache: 'no-store'
• getStaticProps → cache: 'force-cache'
• getStaticProps + revalidate → next: { revalidate: 60 }
4. Client-Interaktion in Client Component auslagern:
• Server Component lädt Daten, übergibt an Client Component
• Client Component für useState, useEffect usw. - 4
Step 4: Routing und Navigation anpassen
Routing-Code aktualisieren:
• next/router → next/navigation
• useRouter().pathname → usePathname()
• useRouter().query → useSearchParams()
• Link-Komponente statt router.push()
Hinweis: useRouter aus next/navigation verhält sich anders als im Pages Router – Link bevorzugen - 5
Step 5: Layout-System einrichten
Verschachtelte Layouts gegen Flackern beim Seitenwechsel:
• app/layout.tsx als Root-Layout (Header, Footer)
• Unterlayouts pro Bereich (z. B. app/dashboard/layout.tsx)
• Pro Layout-Ebene nur UI dieser Ebene
• Kind-Layouts erben vom Parent – beim Wechsel kein Neu-Render der Shell - 6
Step 6: Fehler und 404 behandeln
Fehler:
• error.tsx anlegen, freundliche Meldung
• error-Komponente mit 'use client'
404:
• /pages/404.js entfernen
• app/not-found.tsx anlegen
• notFound() in page.tsx zum Auslösen - 7
Step 7: Testen und optimieren
Tests:
• Alle Routen prüfen
• Datenabruf verifizieren
• Client-Interaktion testen
• Layout-Wechsel ohne Flackern
Performance:
• Unnötige "use client" reduzieren
• Cache-Strategie sinnvoll nutzen
• FCP, TTI, CLS messen
• Vorher/Nachher vergleichen
FAQ
Was ist der Unterschied zwischen schrittweiser und Zero-Downtime-Migration?
Zero-Downtime: alle Seiten unter /app/new neu bauen, Versionswechsel per Query-Parameter, nach Tests produktiv – für große Projekte mit hohen UX-Anforderungen.
Wie hole ich Daten nach der getServerSideProps-Migration?
Cache:
• getServerSideProps → cache: 'no-store'
• getStaticProps → cache: 'force-cache'
Bei Client-Interaktion: Server Component (Daten) + Client Component (Interaktion) trennen.
Warum flackert die Seite nach der Migration beim Wechsel?
Wie nutze ich useRouter im App Router?
Was tun bei inkompatiblen Drittanbieter-Bibliotheken?
Häufig betroffen:
• Framer Motion
• Chart-Bibliotheken (ECharts, Chart.js)
• Karussell-Bibliotheken
Vor der Migration GitHub-Issues der Bibliothek prüfen.
Dev-Server wird langsamer – was hilft?
Workarounds:
• Dev-Server regelmäßig neu starten (z. B. alle 15 Minuten)
• next dev --turbo (Turbopack)
• Unnötige Server Components reduzieren
Next.js 15 soll hier Verbesserungen bringen.
Wie lange dauert die Migration?
• Kleines Projekt (<10 Seiten): etwa 3–5 Tage
• Großes Projekt: 2–3 Wochen
Zuerst 1–2 Seiten als Pilot – danach skalieren. Unser Team: erste Seite 3 Tage, die nächsten 10 Seiten 5 Tage.
11 Min. Lesezeit · Veröffentlicht am: 18. Dez. 2025 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe
Praxisbeispiel mit Next.js 15, Server Actions und Prisma: Schritt für Schritt ein produktionsreifes Full-Stack-Blog-System an einem Wochenende – mit vollständigem Code, Fallstricken und Performance-Optimierung.
Teil 2 von 51
Nächster
Next.js Advanced Routing: Route Groups, Nested Layouts, Parallel Routes und Intercepting Routes
Route Groups für klare Verzeichnisse, Nested Layouts für wiederverwendbare UI, Parallel Routes für mehrere Seitenfragmente und Intercepting Routes für elegante Modals – mit Codebeispielen und typischen Fallstricken für große Next.js-Projekte.
Teil 4 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js App Router in der Praxis: Route Groups und Nested Layouts gegen Verzeichnis-Chaos in großen Projekten


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen