Nehos Groupe

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

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··next-headless

#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 build
  • getServerSideProps — rendu serveur à chaque requête
  • getInitialProps — 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èrePages RouterApp RouterVainqueur
Modèle de rendu par défautSSR/SSG avec fonctions séparéesServer Components — rendu serveur natifApp Router
JavaScript clientHydratation complète de chaque pageZéro JS pour les Server ComponentsApp Router
Fetch des donnéesgetServerSideProps, getStaticPropsasync/await direct dans le composantApp Router
Layouts partagésHOC manuels, _app.tsxlayout.tsx natif, persistantApp Router
Streaming HTTPNon supportéNatif via SuspenseApp Router
Loading statesManuels (useState, spinners)loading.tsx déclaratif par segmentApp Router
Error BoundariesManuels (composants React)error.tsx automatique par segmentApp Router
Route GroupsNon disponible(group)/ pour organiser sans impacter l'URLApp Router
Parallel RoutesNon disponible@slot pour plusieurs segments parallèlesApp Router
Intercepting RoutesNon disponible(.)route pour modales sans navigationApp Router
Server ActionsNon disponible'use server' pour mutations form-serverApp Router
Partial PrerenderingNon disponibleShell statique + streaming dynamiqueApp Router
MiddlewareOui (Edge Runtime)Oui (Edge + Node depuis Next.js 16)Égalité
API Routes / Handlerspages/api/ — abstractions propriétairesroute.ts — Web Platform APIs standardApp Router
Courbe d'apprentissageFaible — modèle familierPlus élevée — nouveau modèle mental RSCPages Router
Compatibilité librairiesExcellente — mature depuis 2016En progression — quelques librairies non encore compatiblesPages Router
Support long terme VercelMaintenance uniquementDéveloppement actif, nouvelles featuresApp Router
TurbopackPartielCompletApp Router
TypeScript type safetyBonneExcellente — params typés via PromiseApp Router
Cohabitation avec l'autre routerOui (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étriquePages Router (médiane)App Router (médiane)Gain
LCP2.7 s1.75 s-35%
Bundle JS client380 KB220 KB-42%
TTFB420 ms110 ms-74%
INP210 ms95 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)

  1. Créer le répertoire app/ avec layout.tsx racine
  2. Migrer _app.tsx : providers React (ThemeProvider, QueryProvider) → déplacer dans app/layout.tsx avec 'use client' si nécessaire
  3. Tester la cohabitation en développement

Phase 2 — Pages à fort trafic (1 à 2 semaines)

  1. Migrer les pages les plus vues en priorité (homepage, pages produit, pages blog)
  2. Pour chaque page : convertir getServerSidePropsasync Server Component + fetch()
  3. Identifier les composants qui nécessitent useState/useEffect → les marquer 'use client'

Phase 3 — API Routes → Route Handlers

  1. Migrer les API Routes vers app/api/*/route.ts
  2. Vérifier les webhooks et endpoints consommés par des tiers

Phase 4 — Fin de migration

  1. Supprimer pages/ une fois toutes les pages migrées
  2. Supprimer _app.tsx et _document.tsx — remplacés par app/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 :

  1. Inventaire des librairies tierces : vérifiez la compatibilité App Router de chaque librairie dans package.json (authentification, UI, analytics, formulaires)
  2. Audit getInitialProps : identifiez toutes les pages qui l'utilisent — ce sont les plus complexes à migrer
  3. Liste des composants avec état global : useContext, Zustand, Redux — ils deviendront des Client Components
  4. Stratégie de cohabitation : décidez de l'ordre de migration des pages (fort trafic en premier, ou pages simples pour l'apprentissage)
  5. Tests de régression : mettez en place des snapshots visuels avant de commencer pour détecter les régressions de rendu
  6. 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
Questions & Réponses

Questions fréquentes sur App Router vs Pages Router

Oui, c'est officiellement supporté par Next.js. Les répertoires `app/` et `pages/` peuvent coexister dans le même projet. En cas de conflit sur une même route, `app/` prend la priorité. Cette cohabitation est la stratégie recommandée pour une migration progressive — vous migrez page par page sans gel du projet.
La durée dépend de la taille et de la complexité du projet. Sur nos migrations clients, comptez 0.5 à 1 jour par page pour une migration soignée avec tests. Un site de 20 pages se migre en 2 à 3 semaines. Une plateforme de 200 pages nécessite 8 à 12 semaines. Les projets avec forte utilisation de `getInitialProps` ou de librairies d'authentification complexes prennent plus de temps.
Fonctionnellement oui : un Server Component asynchrone fait le même travail que `getServerSideProps` — il fetch des données côté serveur et les rend en HTML. La différence structurelle est que le composant n'est plus hydraté côté client, donc il ne génère aucun JavaScript dans le bundle. Pour les cas dynamiques (données personnalisées, cookies), les Server Components utilisent `cookies()` et `headers()` de `next/headers`.
Pas toutes, mais la grande majorité. Les librairies UI modernes (shadcn/ui, Radix, Headless UI, Tailwind) sont compatibles. Les librairies qui s'appuient sur des context providers globaux (Material UI, Chakra UI) fonctionnent dans des Client Components. Les librairies qui utilisent des patterns incompatibles avec RSC (accès à `window` au niveau module, hooks au niveau module) nécessitent des wrappers. Vérifiez la documentation de chaque librairie avant de démarrer.
L'App Router améliore mécaniquement les Core Web Vitals par deux mécanismes : réduction du bundle JavaScript client (les Server Components n'envoient pas de JS au navigateur) et streaming HTTP via Suspense (le contenu visible arrive plus vite). Sur nos mesures de production, le LCP s'améliore de 30 à 40 % en médiane après migration. Le SEO est préservé car Next.js génère du HTML côté serveur dans les deux routers — les robots d'indexation voient du contenu HTML complet.
L'approche recommandée est d'utiliser NextAuth.js v5 (Auth.js) qui supporte nativement l'App Router avec des helpers spécifiques pour les Server Components et les Route Handlers. La session se récupère avec `await auth()` dans les Server Components. Pour protéger des routes, utilisez le Middleware Next.js qui intercepte les requêtes avant le rendu. Évitez de mixer les APIs NextAuth v4 (Pages Router) avec l'App Router — les comportements de session sont différents.
La migration progressive (page par page avec cohabitation des deux routers) est presque toujours préférable pour les projets en production active. Elle permet de livrer en continu, de valider les gains sur les premières pages avant de généraliser, et de limiter le risque de régression. Une migration complète en une fois est envisageable uniquement pour les petits projets (moins de 15 pages) ou lors d'une refonte totale du design, où la réécriture complète est de toute façon nécessaire.
Réserver un audit