Navigation zwischen Next.js App Router und Pages Router – Eine Evolution
Grace Collins
Solutions Engineer · Leapcell

Einleitung
Die Landschaft der Webentwicklung verändert sich ständig, mit neuen Paradigmen und Werkzeugen, die auf die wachsenden Anforderungen an Leistung, Wartbarkeit und Entwicklererfahrung reagieren. Zu den bedeutendsten Fortschritten der letzten Jahre gehört die Weiterentwicklung des Next.js-Frameworks, insbesondere die Einführung des App Routers als Nachfolger des etablierten Pages Routers. Dieser entscheidende architektonische Wandel stellt einen bedeutenden Schritt vorwärts in der Art und Weise dar, wie wir React-Anwendungen erstellen, und bietet verbesserte Möglichkeiten für Data Fetching, Rendering und Routing. Das Verständnis der Nuancen zwischen diesen beiden Routing-Mechanismen ist nicht mehr nur von rein akademischem Interesse, sondern eine praktische Notwendigkeit für Entwickler, die robuste, skalierbare und effiziente Web-Erlebnisse schaffen wollen. Dieser Artikel wird den App Router und den Pages Router gründlich untersuchen, ihre zugrundeliegenden Architekturen zerlegen, ihre jeweiligen Stärken und Schwächen abwägen und einen klaren Weg für die Migration bestehender Anwendungen oder den Beginn neuer Projekte aufzeigen.
Verständnis der Next.js-Routing-Paradigmen
Im Herzen jeder Webanwendung liegt ihr Routing-System, das bestimmt, wie Benutzer zwischen verschiedenen Teilen der Anwendung navigieren und wie Inhalte geliefert werden. Next.js bietet zwei primäre Ansätze dafür: den Pages Router und den App Router. Um ihre Unterschiede zu verstehen, ist es entscheidend, ihre Grundprinzipien zu begreifen und wie sie Entwicklungsmuster beeinflussen.
Der Pages Router: Ein Dateisystem-basiertes Fundament
Der Pages Router, das ursprüngliche Routing-System in Next.js, basiert auf dem Prinzip des Dateisystem-basierten Routings. Jede JavaScript-, TypeScript- oder JSX-Datei im pages
-Verzeichnis wird automatisch zu einer Route. Zum Beispiel entspricht pages/index.js
dem Root-Pfad (/
) und pages/about.js
wird /about
zugeordnet.
Schlüsselkonzepte des Pages Routers:
- Dateisystem-basiertes Routing: Routen werden durch die Dateistruktur im
pages
-Verzeichnis definiert. - Data Fetching: Data Fetching wird hauptsächlich auf Seitenebene mit
getServerSideProps
,getStaticProps
undgetStaticPaths
gehandhabt. Diese Funktionen werden auf dem Server ausgeführt, bevor die Komponente gerendert wird, was Pre-Rendering-Strategien wie Server-Side Rendering (SSR) und Static Site Generation (SSG) ermöglicht.// pages/posts/[id].js export async function getServerSideProps(context) { const { id } = context.params; const res = await fetch(`https://api.example.com/posts/${id}`); const post = await res.json(); return { props: { post } }; } function Post({ post }) { return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); } export default Post;
- Client-seitige Navigation:
next/link
wird für client-seitige Übergänge zwischen Seiten verwendet und sorgt für eine schnelle, Single-Page Application (SPA)-ähnliche Erfahrung.// pages/index.js import Link from 'next/link'; function HomePage() { return ( <div> <Link href="/about"> <a>Über uns</a> </Link> </div> ); } export default HomePage;
Vorteile des Pages Routers:
- Einfachheit und Vertrautheit: Leicht verständlich für Entwickler, die von traditionellen dateibasierten Routing-Systemen kommen.
- Reifes Ökosystem: Langjährig und gut unterstützt, mit einer riesigen Menge an Dokumentation und Community-Ressourcen.
- Klare Trennung von Belangen: Jede Seite repräsentiert typischerweise eine eigene Route und ihre zugehörigen Data Fetching-Logik.
Nachteile des Pages Routers:
- Kopplung von Data Fetching und UI: Data Fetching-Logik ist oft direkt an die Seitenkomponente gebunden, was die gemeinsame Nutzung oder Wiederverwendung von Data Fetching über verschachtelte Komponenten erschwert.
- Begrenzte Layout-Verschachtelung: Komplexe Layouts können umständlich zu verwalten sein, oft erfordern sie Higher-Order Components oder Prop-Drilling.
- Leistungsherausforderungen bei tiefen Bäumen: Die Optimierung der Leistung für Anwendungen mit tief verschachtelten Routen und komplexen Datenabhängigkeiten kann herausfordernd sein, da die gesamte Seite bei Routenänderungen oft neu gerendert wird.
Der App Router: Ein React Server Components Paradigma
Der App Router, der auf React Server Components (RSC) aufbaut, stellt einen Paradigmenwechsel dar. Anstatt Seiten als primäre Einheit für Routing und Data Fetching zu betrachten, führt der App Router einen granulareren, komponenten-basierten Ansatz ein. Dateien und Ordner im app
-Verzeichnis definieren Routen, aber mit zusätzlichen Konventionen für Layouts, Ladezustände, Fehlergrenzen und API-Routen.
Schlüsselkonzepte des App Routers:
- Ordner-basiertes Routing: Ähnlich wie beim Pages Router definieren Ordner nun Routen, und
page.js
-Dateien definieren die UI für dieses Routensegment.app/ ├── layout.js // Root-Layout, gilt für alle Routen ├── page.js // Startseite ├── dashboard/ │ ├── layout.js // Dashboard-Layout, gilt für Dashboard-Routen │ ├── page.js // Dashboard Startseite │ ├── analytics/ │ │ └── page.js // Dashboard-Analysen │ └── settings/ │ └── page.js // Dashboard-Einstellungen
- React Server Components (RSCs): Dies ist der grundlegende Baustein. RSCs ermöglichen es, Komponenten auf dem Server zu rendern, ohne sie zu bündeln und zum Client zu senden. Dies reduziert die Größe des clientseitigen JavaScript-Bundles erheblich und verbessert die anfängliche Seitenladeleistung. Komponenten sind im
app
-Verzeichnis standardmäßig Server Components. Um eine Komponente zu einer Client Component zu machen, verwenden Sie die Direktive"use client"
.// app/dashboard/page.js (Standardmäßig Server Component) import { Suspense } from 'react'; import DashboardCharts from './DashboardCharts'; // Dies könnte eine Client Component sein async function getAnalyticsData() { // Serverseitiges Data Fetching const res = await fetch('https://api.example.com/analytics'); if (!res.ok) throw new Error('Failed to fetch analytics'); return res.json(); } export default async function DashboardPage() { const data = await getAnalyticsData(); // Data Fetching direkt in Server Component return ( <div> <h1>Dashboard Übersicht</h1> {/* DashboardCharts könnte eine Client Component sein, wenn sie Interaktivität benötigt */} <Suspense fallback={<p>Lade Diagramme...</p berlanggt> <DashboardCharts analyticsData={data} /> </Suspense> </div> ); } // app/dashboard/DashboardCharts.js (Client Component) // Benötigt Interaktivität, z.B. mit useState, useEffect oder Event-Listenern "use client"; import { useState, useEffect } from 'react'; export default function DashboardCharts({ analyticsData }) { // Client-seitige Logik für interaktive Diagramme const [chartData, setChartData] = useState(analyticsData); useEffect(() => { // Vielleicht client-seitige Daten-Updates oder Initialisierung der Diagrammbibliothek }, [analyticsData]); return ( <div> <h2>Diagramme</h2> {/* Diagramme mit einer client-seitigen Bibliothek rendern */} <pre>{JSON.stringify(chartData, null, 2)}</pre> </div> ); }
- Verschachtelte Layouts: Der App Router unterstützt nativ tief verschachtelte Layouts, die es UI-Komponenten ermöglichen, Kindrouten zu umschließen. Eine
layout.js
-Datei auf jeder Orderebene umschließt ihre Kindrouten. Dies verbessert die Code-Wiederverwendbarkeit und Wartbarkeit für gängige UI-Muster erheblich.// app/dashboard/layout.js export default function DashboardLayout({ children }) { return ( <section> <nav> <ul> <li>Dashboard Nav Link 1</li> <li>Dashboard Nav Link 2</li> </ul> </nav> {children} {/* Hier werden Kindrouten/Seiten gerendert */} </section> ); }
- Kollokation von Logik:
loading.js
(für Ladezustände),error.js
(für Fehlergrenzen) undroute.js
(für API-Routen) können neben ihren jeweiligen Routensegmenten platziert werden, was die Modularität verbessert.// app/dashboard/loading.js export default function Loading() { return <p>Lade Dashboard-Daten...</p>; } // app/dashboard/error.js "use client"; // Fehlergrenzen müssen Client Components sein export default function ErrorBoundary({ error, reset }) { return ( <div> <h2>Etwas ist schiefgelaufen!</h2> <button onClick={() => reset()}>Erneut versuchen</button> </div> ); } // app/api/users/route.js (API-Route für GET-Anfragen an /api/users) import { NextResponse } from 'next/server'; export async function GET() { const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; return NextResponse.json(users); }
- Fortgeschrittenes Data Fetching: Data Fetching kann direkt in Server Components mit
async
/await
erfolgen und bietet eine direktere und effizientere Methode zum Abrufen von Daten, ohne dassgetServerSideProps
odergetStaticProps
erforderlich sind. Next.js dedupliziert und cachtfetch
-Anfragen automatisch. - Streaming & Suspense: Nutzt Reacts Suspense, um Teile Ihrer UI an den Client zu streamen, sobald sie bereit sind, was zu schnelleren wahrgenommenen Ladezeiten führt.
Vorteile des App Routers:
- Verbesserte Leistung (kleinere Bundles, schnellere Ladezeiten): Durch das Rendern von Komponenten auf dem Server und das Senden nur notwendigen Client-JavaScript wird die anfängliche Ladezeit erheblich reduziert.
- Verbesserte Data Fetching-Strategie: Ermöglicht Data Fetching direkt in jeder Server Component und fördert die Kollokation und reduziert Boilerplate-Code. Automatische Anfrage-Memoisation und Caching optimieren Netzwerkanfragen.
- Verschachtelte Layouts und Kollokation: Vereinfacht komplexe UI-Strukturen und verbessert die Wartbarkeit, indem Layouts und zugehörige Logik (Laden, Fehler) neben ihren jeweiligen Routen platziert werden können.
- Streaming UI: Nutzt React Suspense, um UI inkrementell an den Client zu streamen und die wahrgenommene Leistung zu verbessern.
- Zukunftssicher: Steht im Einklang mit der Ausrichtung von React Server Components und modernen Webentwicklungs-Best-Practices.
Nachteile des App Routers:
- Steilere Lernkurve: Das Mentalitätsmodell von Server Components vs. Client Components und neue Data Fetching-Regeln erfordern ein tieferes Verständnis.
- Reife: Obwohl produktionsreif, ist es neuer als der Pages Router, was bedeutet, dass einige Randfälle oder Community-Ressourcen möglicherweise noch entwickelt werden.
- Debugging-Komplexität: Die Unterscheidung zwischen serverseitiger und clientseitiger Ausführung kann das Debugging manchmal erschweren.
- Integration mit älteren Bibliotheken: Einige reine Client-Side-React-Bibliotheken erfordern möglicherweise spezifische Anpassungen, um nahtlos im App Router-Paradigma zu funktionieren.
Architektonische Evolution und hybride Ansätze
Der Wechsel vom Pages Router zum App Router stellt eine Evolution hin zu granularerer Kontrolle, besserer Leistungsoptimierung und einer stärkeren Ausrichtung auf die Zukunft von React dar. Der Pages Router bot einen guten Ausgangspunkt für serverseitig gerendertes React, aber seine Einschränkungen bei komplexen UIs und Data Fetching-Optimierungen wurden offensichtlich.
Next.js ist darauf ausgelegt, einen hybriden Ansatz zu ermöglichen, bei dem sowohl pages
- als auch app
-Verzeichnisse im selben Projekt koexistieren können. Dies ist für graduelle Migrationsstrategien unglaublich nützlich.
- Routen im
pages
-Verzeichnis haben Vorrang vor Routen imapp
-Verzeichnis für widersprüchliche Pfade. - Sie können neue Funktionen oder komplett neue Teile Ihrer Anwendung mit dem App Router einführen, während bestehende Teile mit dem Pages Router beibehalten werden.
your-next-app/
├── app/ // Neue Funktionen, App Router
│ ├── dashboard/
│ │ └── page.js
│ └── products/
│ └── [id]/
│ └── page.js
├── pages/ // Bestehende Funktionen, Pages Router
│ ├── index.js
│ ├── about.js
│ └── api/
│ └── hello.js
├── components/
├── public/
└── next.config.js
Dieses hybride Modell ermöglicht es Entwicklern, die Vorteile des App Routers für neue Entwicklungen zu nutzen, ohne eine disruptive "Big Bang"-Neuschreibung ihrer gesamten Anwendung durchführen zu müssen.
Migrationsanleitung für bestehende Anwendungen
Die Migration einer bestehenden Next.js-Anwendung vom Pages Router zum App Router kann entmutigend wirken, aber ein systematischer Ansatz und das Verständnis der wichtigsten Unterschiede können sie überschaubar machen.
1. Stufenweise Migrationsstrategie (Empfohlen)
Angesichts der signifikanten architektonischen Unterschiede ist eine "Big Bang"-Migration für große Anwendungen selten ratsam. Setzen Sie stattdessen auf einen gestuften Ansatz:
- App Router für neue Features einführen: Beginnen Sie damit, neue Features oder komplett neue Abschnitte Ihrer Anwendung mit dem App Router zu erstellen. Dies ermöglicht Ihrem Team, Erfahrungen mit dem neuen Paradigma zu sammeln, ohne die bestehende Funktionalität zu beeinträchtigen.
- Leaf Routes zuerst migrieren: Identifizieren Sie isolierte, weniger komplexe Routen in Ihrem
pages
-Verzeichnis, die unabhängig migriert werden können. Beispiele hierfür sind statische Seiten (/about
,/contact
) oder einfache dynamische Routen (/blog/[slug]
), die keine tief verschachtelten Layouts haben. - Gemeinsame Komponenten und Layouts migrieren: Sobald Sie sich mit einzelnen Routen wohlfühlen, erwägen Sie, wie Sie gemeinsame Komponenten und Layouts migrieren. Im App Router sind Layouts Dateien innerhalb des
app
-Verzeichnisses, die Kindrouten umschließen. - Data Fetching adressieren: Dies ist oft der bedeutendste Teil der Migration. Wechseln Sie von
getServerSideProps
/getStaticProps
zu direktenfetch
-Aufrufen innerhalb von Server Components.
2. Wichtige Migrationsschritte und Überlegungen
-
Erstellen Sie das
app
-Verzeichnis: Erstellen Sie einfach ein neuesapp
-Verzeichnis im Stammverzeichnis Ihres Projekts. Next.js wird es automatisch erkennen. -
pages
nachapp
verschieben (selektiv):- Für eine einfache Seite wie
pages/about.js
: Erstellen Sieapp/about/page.js
. - Für eine dynamische Seite wie
pages/blog/[slug].js
: Erstellen Sieapp/blog/[slug]/page.js
.
- Für eine einfache Seite wie
-
layout.js
refaktorisieren: Die Rootpages/_app.js
undpages/_document.js
werden durchapp/layout.js
ersetzt.app/layout.js
: Diese Datei definiert das Root-Layout Ihrer Anwendung. Sie muss<html>
- und<body>
-Tags definieren. Server Components sind Standard. Wenn Sie clientseitige Provider haben (z.B. Redux, Context API, Chakra UI Provider), müssen diese in eine Client Component mit"use client"
gekapselt werden.
// app/layout.js import './globals.css'; // Globale Stile // Sie können Provider hier verschieben, gekapselt in eine Client Component import Providers from './providers'; // Stellen Sie sicher, dass providers.js eine Client Component ist export const metadata = { title: 'Next.js App Router', description: 'Migrationsbeispiel', }; export default function RootLayout({ children }) { return ( <html lang="en"> <body> <Providers> {/* Wenn Providers client-seitige Hooks verwendet, benötigt es "use client" */} {children} </Providers> </body> </html> ); }
- **
app/providers.js
(Beispiel für Client-seitige Provider):
// app/providers.js "use client"; // Diese Komponente muss eine Client Component sein import { ThemeProvider } from 'next-themes'; // import { ReduxProvider } from '../redux/provider'; // Beispiel export default function Providers({ children }) { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {/* <ReduxProvider> */} {children} {/* </ReduxProvider> */} </ThemeProvider> ); }
-
Data Fetching aktualisieren:
getServerSideProps
/getStaticProps
zuasync
Server Components:- Entfernen Sie
export async function getServerSideProps(...)
odergetStaticProps(...)
. - Machen Sie Ihre
page.js
-Komponente zu einerasync
-Funktion. - Verwenden Sie direkt
await fetch()
-Aufrufe innerhalb der Komponente. - Next.js cacht
fetch
-Anfragen automatisch und behandeltrevalidate
ähnlich wiegetStaticProps
.
// Vorher (Pages Router Beispiel) // pages/blog/[slug].js export async function getServerSideProps(context) { const { slug } = context.params; const res = await fetch(`https://api.example.com/blog/${slug}`); const post = await res.json(); return { props: { post } }; } function BlogDetails({ post }) { ... } // Nachher (App Router Beispiel) // app/blog/[slug]/page.js async function getPost(slug) { const res = await fetch(`https://api.example.com/blog/${slug}`, { next: { revalidate: 3600 } }); // Alle Stunde neu validieren if (!res.ok) throw new Error('Fehler beim Abrufen des Beitrags'); return res.json(); } export default async function BlogDetailsPage({ params }) { const post = await getPost(params.slug); return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); }
- Entfernen Sie
getStaticPaths
Ersatz: Im App Router ersetztgenerateStaticParams
die FunktiongetStaticPaths
.// app/products/[id]/page.js export async function generateStaticParams() { const products = await fetch('https://api.example.com/products').then((res) => res.json()); return products.map((product) => ({ id: product.id.toString(), })); } export default async function ProductPage({ params }) { const product = await fetch(`https://api.example.com/products/${params.id}`).then((res) => res.json()); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); }
-
Client vs. Server Components verwalten:
- Identifizieren Sie Komponenten, die client-seitige Interaktivität erfordern (Zustand, Effekte, Event-Listener). Präfixieren Sie diese Dateien mit
"use client";
. - Stellen Sie sicher, dass jede Komponente, die eine
"use client"
-Komponente importiert, ebenfalls auf dem Client ausgeführt wird, falls sie client-spezifische Hooks oder Funktionen verwendet. - Übergeben Sie Daten von Server Components an Client Components.
- Identifizieren Sie Komponenten, die client-seitige Interaktivität erfordern (Zustand, Effekte, Event-Listener). Präfixieren Sie diese Dateien mit
-
loading.js
underror.js
implementieren: Ersetzen Sie benutzerdefinierte Ladeanzeigen und Fehlerbehandlung durch die integrierten Konventionen von Next.js.- Erstellen Sie
loading.js
(Server Component) in einem Routensegment, um eine Lade-UI anzuzeigen. - Erstellen Sie
error.js
(Client Component mit"use client"
) in einem Routensegment für Fehlergrenzen.
- Erstellen Sie
-
API-Routen anpassen:
pages/api
-Routen werden jetzt durchroute.js
-Dateien imapp
-Verzeichnis für API-Routen ersetzt.// app/api/my-endpoint/route.js import { NextResponse } from 'next/server'; export async function GET(request) { // Sie können auf Request-Header, Query-Parameter usw. zugreifen. return NextResponse.json({ message: 'Hallo von der App Router API!' }); } export async function POST(request) { const body = await request.json(); return NextResponse.json({ received: body }, { status: 200 }); }
-
Testen: Testen Sie jede migrierte Route und Komponente gründlich, um Funktionalität, Leistung und korrekte Client-/Server-Abgrenzung sicherzustellen.
3. Häufige Migrationsfallen
- Versehentliche Client Components: Das Vergessen von
"use client"
für Komponenten, die client-seitiges Verhalten benötigen, oder das unnötige Hinzufügen für Komponenten, die es nicht tun, kann zu Problemen führen. - Nur-Server-Funktionen in Client Components: Der Versuch, Node.js-spezifische APIs (z.B.
fs
,process.env
direkt ohne NEXT_PUBLIC_ Präfix) in einer Client Component zu verwenden. - Daten-Revalidierung: Das Verständnis des neuen Daten-Caching- und Revalidierungsmodells für
fetch
-Anfragen im App Router ist entscheidend, um frische Daten zu gewährleisten. - Zustandsverwaltung: Wenn Sie globale Zustandsverwaltung (z.B. Redux, Zustand) oder Context APIs verwenden, stellen Sie sicher, dass Ihre Provider korrekt als Client Components in
app/layout.js
oder einer spezifischen Client-Grenze eingerichtet sind. - CSS-Bundling: Globale CSS sollte in
app/layout.js
importiert werden. Modul-CSS (z.B.component.module.css
) und Tailwind CSS funktionieren ähnlich.
Durch die Befolgung eines gestuften Ansatzes und das Verständnis dieser wichtigen Änderungen kann die Migration zum App Router ein reibungsloser und lohnender Prozess sein, der erhebliche Leistungs- und Entwicklererfahrungs-Vorteile freischaltet.
Fazit
Die Evolution vom Next.js Pages Router zum App Router markiert einen bedeutenden Sprung in der Art und Weise, wie wir Webanwendungen entwickeln, insbesondere mit der Akzeptanz von React Server Components. Während der Pages Router eine unkomplizierte, dateisystem-basierte Routing- und Pre-Rendering-Lösung bot, führt der App Router ein granulareres, komponenten-basiertes Paradigma ein, das die Leistung durch reduziertes client-seitiges JavaScript, verbessertes Data Fetching und natives Streaming optimiert. Das Verständnis der unterschiedlichen Architekturmodelle, sei es die Wahl der Einfachheit des Pages Routers für kleinere Projekte oder die Nutzung der fortgeschrittenen Fähigkeiten des App Routers für skalierbare, hochleistungsfähige Anwendungen, ist von größter Bedeutung. Für bestehende Projekte ermöglicht eine strategische, gestufte Migration Entwicklern, die Vorteile des App Routers schrittweise und ohne Unterbrechungen zu übernehmen und festigt damit die Position von Next.js als zukunftsweisendes Framework für das moderne Web. Die Zukunft der Next.js-Entwicklung zeigt fest auf den App Router, der eine robuste Grundlage für den Aufbau schneller und hochgradig wartbarer React-Anwendungen bietet.