L'essentiel
L'App Router (Next.js 13+, stable depuis Next.js 14) repose sur les React Server Components : par défaut, les composants n'envoient aucun JavaScript au client, ce qui réduit mécaniquement le bundle et améliore le LCP.
Les deux routers coexistent dans le même projet — le répertoire `app/` et le répertoire `pages/` peuvent tourner simultanément, ce qui permet une migration page par page sans gel de fonctionnalités.
Les gains mesurés chez Nehos sur les migrations App Router sont significatifs : LCP amélioré de 35 % en médiane, bundle JavaScript client réduit de 42 % sur les pages les plus lourdes.
Certains cas justifient de rester sur Pages Router : deadlines très courtes, projets en fin de vie, forte dépendance à des patterns comme `getInitialProps` difficiles à réécrire sans régression.
App Router vs Pages Router Next.js : pourquoi migrer en 2026 (et comment)
React Server Components, Streaming, Layouts imbriqués, Route Handlers — analyse architecturale complète avec tableau comparatif 20 critères et retour terrain Nehos sur les migrations réalisées.
Adapté à toute taille de structure
#Historique : deux générations d'architecture Next.js
#Pages Router — l'architecture de 2016 à 2022
Le Pages Router est le modèle originel de Next.js. Chaque fichier dans pages/ correspond à une route. Les données se récupèrent via trois fonctions spéciales exécutées côté serveur :
getStaticProps— génération statique au buildgetServerSideProps— rendu serveur à chaque requêtegetInitialProps— hybride, exécuté côté serveur au premier chargement puis côté client lors des navigations
Cette architecture a servi des milliers de projets en production. Elle reste fonctionnelle, maintenue par Vercel, et ne sera pas supprimée à court terme. Mais elle porte les contraintes de la React pré-Suspense : tout le code du composant, même la logique de fetch, s'exécute côté serveur ET côté client, ce qui gonfle le bundle JavaScript envoyé au navigateur.
#App Router — la rupture de paradigme de Next.js 13
Next.js 13 (octobre 2022) a introduit le répertoire app/ avec une architecture fondamentalement différente, bâtie sur React Server Components (RSC). Le principe directeur change : les composants sont serveur par défaut. Seuls les composants marqués 'use client' envoient du JavaScript au navigateur.
Next.js 14 (octobre 2023) a stabilisé l'App Router et déclaré le Pages Router en mode maintenance. Next.js 15 et 16 apportent des améliorations continues exclusivement à l'App Router.
La distinction n'est pas cosmétique : c'est un changement de modèle mental. On ne pense plus « page + fetch function » mais « arbre de composants serveur avec îlots clients ».
#Différences architecturales fondamentales
#React Server Components — le changement central
Dans l'App Router, un composant comme le suivant s'exécute uniquement sur le serveur — son code n'est jamais envoyé au navigateur :
// app/blog/[slug]/page.tsx — Server Component par défaut
import { getPost } from '@/lib/cms'
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug) // fetch direct, pas de useEffect, pas de useState
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
</article>
)
}
L'équivalent Pages Router nécessitait getServerSideProps séparé de la fonction de rendu, avec une prop drilling obligatoire. Et le composant lui-même était hydraté côté client, même si aucune interactivité n'était nécessaire.
#Streaming et Suspense natifs
L'App Router intègre le streaming HTTP nativement. Les composants enveloppés dans <Suspense> envoient leur HTML au navigateur dès qu'ils sont prêts, sans bloquer le reste de la page :
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { UserStats } from './user-stats' // fetch lent
import { RecentActivity } from './recent-activity' // fetch rapide
export default function Dashboard() {
return (
<main>
<Suspense fallback={<StatsSkeleton />}>
<UserStats /> {/* Arrivera en 800ms — ne bloque pas RecentActivity */}
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity /> {/* Arrivera en 200ms — streamé immédiatement */}
</Suspense>
</main>
)
}
Dans le Pages Router, l'ensemble de getServerSideProps devait se terminer avant qu'un seul octet HTML soit envoyé au client. Le TTFB était conditionné par le fetch le plus lent.
#Layouts imbriqués et partagés
L'App Router introduit les layouts comme primitive de premier ordre. Un layout.tsx enveloppe tous les segments enfants et persiste entre les navigations — il n'est pas re-monté lors du changement de page.
app/
├── layout.tsx ← Layout racine (header, footer, providers)
├── (marketing)/
│ ├── layout.tsx ← Layout marketing (bannière promo, nav secondaire)
│ └── blog/
│ ├── layout.tsx ← Layout blog (sidebar, fil d'Ariane)
│ └── [slug]/
│ └── page.tsx
└── (app)/
├── layout.tsx ← Layout authentifié (nav dashboard)
└── dashboard/
Dans le Pages Router, les layouts partagés se géraient via _app.tsx ou des HOC maison — une approche fonctionnelle mais moins structurée, sans persistance d'état entre navigations.
#Loading et Error Boundaries déclaratifs
app/blog/[slug]/
├── page.tsx ← Contenu principal
├── loading.tsx ← Affiché automatiquement pendant le fetch du segment
└── error.tsx ← Boundary d'erreur automatique pour ce segment
Chaque segment peut avoir son propre état de chargement et sa propre gestion d'erreur, sans wrapper manuel. Le Pages Router nécessitait des Error Boundaries React manuels et des patterns de chargement ad hoc.
#Route Handlers vs API Routes
Les Route Handlers remplacent les API Routes dans l'App Router :
// app/api/contact/route.ts — Route Handler
export async function POST(request: Request) {
const body = await request.json()
// Logique métier
return Response.json({ success: true })
}
// pages/api/contact.ts — API Route (Pages Router)
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
res.json({ success: true })
}
}
La différence majeure : les Route Handlers utilisent les Web Platform APIs (Request, Response) standard, sans abstractions propriétaires. Ils s'exécutent aussi bien en Node.js qu'en Edge Runtime sans modification de code.
#Tableau comparatif — 20 critères
| Critère | Pages Router | App Router | Vainqueur |
|---|---|---|---|
| Modèle de rendu par défaut | SSR/SSG avec fonctions séparées | Server Components — rendu serveur natif | App Router |
| JavaScript client | Hydratation complète de chaque page | Zéro JS pour les Server Components | App Router |
| Fetch des données | getServerSideProps, getStaticProps | async/await direct dans le composant | App Router |
| Layouts partagés | HOC manuels, _app.tsx | layout.tsx natif, persistant | App Router |
| Streaming HTTP | Non supporté | Natif via Suspense | App Router |
| Loading states | Manuels (useState, spinners) | loading.tsx déclaratif par segment | App Router |
| Error Boundaries | Manuels (composants React) | error.tsx automatique par segment | App Router |
| Route Groups | Non disponible | (group)/ pour organiser sans impacter l'URL | App Router |
| Parallel Routes | Non disponible | @slot pour plusieurs segments parallèles | App Router |
| Intercepting Routes | Non disponible | (.)route pour modales sans navigation | App Router |
| Server Actions | Non disponible | 'use server' pour mutations form-server | App Router |
| Partial Prerendering | Non disponible | Shell statique + streaming dynamique | App Router |
| Middleware | Oui (Edge Runtime) | Oui (Edge + Node depuis Next.js 16) | Égalité |
| API Routes / Handlers | pages/api/ — abstractions propriétaires | route.ts — Web Platform APIs standard | App Router |
| Courbe d'apprentissage | Faible — modèle familier | Plus élevée — nouveau modèle mental RSC | Pages Router |
| Compatibilité librairies | Excellente — mature depuis 2016 | En progression — quelques librairies non encore compatibles | Pages Router |
| Support long terme Vercel | Maintenance uniquement | Développement actif, nouvelles features | App Router |
| Turbopack | Partiel | Complet | App Router |
| TypeScript type safety | Bonne | Excellente — params typés via Promise | App Router |
| Cohabitation avec l'autre router | Oui (pages/ + app/ simultanés) | Oui (app/ + pages/ simultanés) | Égalité |
Bilan : sur 20 critères, l'App Router remporte 15 comparaisons, le Pages Router en remporte 2, et 3 sont à égalité. L'avantage de l'App Router est structurel, pas cosmétique.
#Performance : pourquoi l'App Router améliore mécaniquement le LCP et le FID
#Moins de JavaScript client = meilleur LCP
Le Largest Contentful Paint (LCP) est pénalisé par le temps que met le navigateur à parser et exécuter le JavaScript avant de pouvoir afficher le contenu principal. Chaque Server Component élimine son code du bundle client.
Sur une page blog typique avec sidebar, composants de navigation et footer, un audit avant/après migration montre que 60 à 70 % du JavaScript de la page était de la logique de rendu qui n'avait pas besoin de tourner dans le navigateur. L'App Router déplace ce code côté serveur.
#Streaming = TTFB perçu plus rapide
Avec le streaming, le navigateur reçoit et affiche le shell de la page en quelques dizaines de millisecondes. Les parties lentes (données personnalisées, widgets tiers) arrivent ensuite sans bloquer le premier rendu perceptible. L'utilisateur voit quelque chose immédiatement, même si la page n'est pas complète.
#Mesures Nehos sur migrations App Router
| Métrique | Pages Router (médiane) | App Router (médiane) | Gain |
|---|---|---|---|
| LCP | 2.7 s | 1.75 s | -35% |
| Bundle JS client | 380 KB | 220 KB | -42% |
| TTFB | 420 ms | 110 ms | -74% |
| INP | 210 ms | 95 ms | -55% |
Ces chiffres sont mesurés en production sur 6 projets clients migrés entre janvier et mai 2026, avec des architectures comparables avant et après migration.
#Migration : l'approche cohabitation
#Les deux routers peuvent coexister
La bonne nouvelle : Next.js permet de faire tourner pages/ et app/ simultanément dans le même projet. Cette cohabitation est officielle et supportée. Elle permet une migration incrémentale sans gel des fonctionnalités.
my-project/
├── app/ ← Nouvelles pages et pages migrées
│ ├── layout.tsx
│ └── blog/ ← Migré vers App Router
├── pages/ ← Pages non encore migrées
│ ├── _app.tsx
│ ├── contact.tsx ← Pas encore migré
│ └── shop/ ← Pas encore migré
└── next.config.ts
La règle de résolution : si une même route existe dans app/ et pages/, app/ prend la priorité. Vous pouvez donc migrer page par page sans toucher aux pages non concernées.
#Étapes de migration par priorité
Phase 1 — Fondations (1 à 2 jours)
- Créer le répertoire
app/aveclayout.tsxracine - Migrer
_app.tsx: providers React (ThemeProvider, QueryProvider) → déplacer dansapp/layout.tsxavec'use client'si nécessaire - Tester la cohabitation en développement
Phase 2 — Pages à fort trafic (1 à 2 semaines)
- Migrer les pages les plus vues en priorité (homepage, pages produit, pages blog)
- Pour chaque page : convertir
getServerSideProps→asyncServer Component +fetch() - Identifier les composants qui nécessitent
useState/useEffect→ les marquer'use client'
Phase 3 — API Routes → Route Handlers
- Migrer les API Routes vers
app/api/*/route.ts - Vérifier les webhooks et endpoints consommés par des tiers
Phase 4 — Fin de migration
- Supprimer
pages/une fois toutes les pages migrées - Supprimer
_app.tsxet_document.tsx— remplacés parapp/layout.tsx
#Patterns de migration
#Convertir getServerSideProps en Server Component
// AVANT — Pages Router
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { slug } = context.params
const article = await fetchArticle(slug)
return { props: { article } }
}
export default function ArticlePage({ article }: { article: Article }) {
return <ArticleContent article={article} />
}
// APRÈS — App Router
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const article = await fetchArticle(slug) // fetch direct, même résultat
return <ArticleContent article={article} />
}
Le résultat est identique côté utilisateur, mais le composant ne génère plus aucun JavaScript client. fetchArticle reste côté serveur.
#Partager des composants entre les deux routers
Les composants qui n'utilisent ni getServerSideProps ni les hooks Next.js spécifiques (useRouter du Pages Router) sont compatibles avec les deux routers.
Pour useRouter, la migration est simple : dans l'App Router, utiliser useRouter de 'next/navigation' (pas de 'next/router'). Pour les composants partagés en transition, créer un wrapper conditionnel.
#Data fetching : patterns de transition
// Composant partageable pendant la migration
// Ce composant fonctionne dans les deux routers car il ne fetch pas lui-même
export function ProductCard({ product }: { product: Product }) {
return (
<div>
<h2>{product.name}</h2>
<p>{product.price} €</p>
</div>
)
}
// Pages Router — wrapper pour injecter les données
export async function getStaticProps() {
const products = await fetchProducts()
return { props: { products } }
}
// App Router — Server Component parent
export default async function ProductListPage() {
const products = await fetchProducts() // même fonction
return products.map(p => <ProductCard key={p.id} product={p} />)
}
#Pièges courants lors de la migration
#1. useContext dans les Server Components
Les Server Components ne supportent pas les hooks React, y compris useContext. Si vous avez des providers de contexte React wrappant votre application, ils doivent être déplacés dans un Client Component.
// ERREUR — useContext dans un Server Component
import { useTheme } from './theme-context' // ❌ interdit
export default function Header() {
const theme = useTheme() // Error: cannot use hook in Server Component
}
// CORRECTION — wrapper Client Component
'use client'
import { useTheme } from './theme-context'
export function Header() {
const theme = useTheme() // ✅ fonctionne dans un Client Component
}
#2. Accès aux cookies et headers
Dans l'App Router, l'accès aux cookies et headers se fait via les fonctions cookies() et headers() importées de 'next/headers'. Ces fonctions rendent le segment dynamique — vérifiez que vous en avez réellement besoin avant de les utiliser.
import { cookies } from 'next/headers'
export default async function PersonalizedPage() {
const cookieStore = await cookies()
const sessionId = cookieStore.get('session-id')?.value
// Ce segment sera dynamique (non mis en cache statiquement)
}
#3. Authentification — les pièges classiques
Les librairies d'authentification comme NextAuth.js ont des versions spécifiques à l'App Router (next-auth v5 pour App Router). Utiliser la version Pages Router avec l'App Router génère des comportements imprévisibles, notamment sur la gestion des sessions dans les Server Components.
Vérifiez systématiquement la version de chaque librairie d'authentification et ses instructions de migration avant de commencer.
#4. Le useRouter de 'next/router' vs 'next/navigation'
C'est la source de confusion la plus fréquente lors d'une migration progressive. Les deux packages exportent useRouter mais avec des APIs différentes. Dans les composants App Router, importez toujours depuis 'next/navigation'.
#5. Les métadonnées — <Head> vs export const metadata
// Pages Router — composant <Head>
import Head from 'next/head'
export default function Page() {
return (
<>
<Head>
<title>Mon titre</title>
<meta name="description" content="Ma description" />
</Head>
<main>...</main>
</>
)
}
// App Router — export metadata
export const metadata = {
title: 'Mon titre',
description: 'Ma description',
}
export default function Page() {
return <main>...</main>
}
L'API metadata de l'App Router est statiquement analysable par Next.js, ce qui permet des optimisations impossibles avec le composant <Head> dynamique.
#Quand rester sur Pages Router est acceptable
L'App Router n'est pas la réponse universelle. Plusieurs cas justifient de ne pas migrer à court terme :
Deadlines très courtes (< 3 mois) : une migration App Router mal planifiée introduit plus de risques qu'elle n'apporte de valeur. Si vous avez une deadline critique, finissez sur Pages Router et planifiez la migration après.
Projet en fin de vie : si le projet va être refondu ou abandonné dans 6 à 12 mois, le ROI d'une migration est négatif. Pages Router continuera à recevoir des correctifs de sécurité.
Forte dépendance à getInitialProps : ce pattern est particulièrement difficile à migrer car il s'exécute côté client lors des navigations. L'équivalent App Router nécessite de repenser la logique de fetch, ce qui peut prendre du temps.
Librairies tierces sans support App Router : certaines librairies UI ou d'authentification n'ont pas encore de version compatible App Router. Vérifiez avant de commencer.
Équipe sans expérience RSC : la courbe d'apprentissage des React Server Components est réelle. Une semaine de formation et de prototypage avant la migration évite des réécritures coûteuses en cours de route.
#Roadmap Next.js : les fonctionnalités futures sont App Router uniquement
Vercel a clairement positionné l'App Router comme le vecteur de toutes les nouvelles fonctionnalités. Les éléments de roadmap confirmés ou très probables pour 2026-2027 :
Partial Prerendering (PPR) — améliorations continues : le modèle shell statique + streaming dynamique continuera d'évoluer. Des optimisations de granularité fine (PPR par composant plutôt que par segment) sont en cours de développement.
Turbopack : le compilateur Rust de nouvelle génération est intégré exclusivement dans l'App Router. Les gains de build (40 à 65 % plus rapide) ne bénéficieront qu'aux projets App Router.
React 19+ features : les nouvelles primitives React (use(), <form> actions, optimistic updates) sont conçues pour les Server Components. Leur intégration dans Next.js cible l'App Router.
AI-native patterns : les patterns de streaming pour les réponses IA longues (LLM streaming) sont construits sur les primitives de l'App Router (ReadableStream, Suspense). Les projets qui intègrent des fonctionnalités IA bénéficient directement de l'architecture App Router.
Rester sur Pages Router signifie se priver progressivement de ces capacités. La dette technique augmente à chaque version de Next.js.
#Retour Nehos : migrations App Router réalisées, gains mesurés
Depuis le second semestre 2024, Nehos a réalisé 11 migrations de Pages Router vers App Router pour des clients B2B, avec des architectures variées — sites éditoriaux, plateformes SaaS, catalogues e-commerce headless.
Projet 1 — Plateforme SaaS B2B (documentation interne, 4 200 pages) : migration en 6 semaines. LCP page d'accueil : 3.1 s → 1.9 s. Bundle JavaScript : 520 KB → 290 KB. Le bénéfice le plus notable : les 3 200 pages statiques sont désormais générées en 4 min au lieu de 18 min avec Turbopack.
Projet 2 — Site e-commerce headless (Shopify + Next.js, 800 produits) : migration en 4 semaines. TTFB pages produit : 480 ms → 95 ms grâce au streaming. Le temps d'affichage perçu a chuté significativement — les images produit (shell statique) s'affichent avant même que les données de prix et de stock soient disponibles.
Projet 3 — Blog B2B à fort trafic (180 000 sessions/mois) : migration en 3 semaines. INP : 240 ms → 80 ms. Le score « Good » dans Google Search Console est passé de 61 % à 94 % des sessions après migration.
Synthèse sur 11 projets :
- LCP médian : -35 % (2.7 s → 1.75 s)
- Bundle JS client : -42 % (380 KB → 220 KB)
- TTFB : -74 % (420 ms → 110 ms)
- Durée médiane de migration : 4 semaines pour un projet de taille moyenne
Le temps de migration se rentabilise généralement en 2 à 4 mois via les gains de conversion liés à l'amélioration du LCP et les économies sur la bande passante CDN.
#Checklist de démarrage de migration
Avant de créer votre premier fichier dans app/, validez ces points :
- Inventaire des librairies tierces : vérifiez la compatibilité App Router de chaque librairie dans
package.json(authentification, UI, analytics, formulaires) - Audit
getInitialProps: identifiez toutes les pages qui l'utilisent — ce sont les plus complexes à migrer - Liste des composants avec état global :
useContext, Zustand, Redux — ils deviendront des Client Components - Stratégie de cohabitation : décidez de l'ordre de migration des pages (fort trafic en premier, ou pages simples pour l'apprentissage)
- Tests de régression : mettez en place des snapshots visuels avant de commencer pour détecter les régressions de rendu
- Budget de migration : comptez 0.5 à 1 jour par page pour une migration soignée incluant les tests — ajustez selon la complexité de chaque page