Next.js Pages Router vers App Router : guide pratique et pièges à éviter

Le directeur technique pose la question en réunion : « Notre projet Next.js 12, on peut passer à la 14 ? »
Je fixe l’écran — ce vieux projet tourne depuis deux ans. La dernière montée vers React 17 nous avait coûté une semaine de bugs, le support client débordé.
Cette fois, c’est un peu différent. Le soir, je feuillette la doc officielle : Server Components, layouts imbriqués, meilleures perfs… ça donne envie. Puis la page migration : tableaux d’API, getServerSideProps devient quoi, _app.js se découpe comment… la migraine.
Pire : la « migration progressive » recommandée sonne bien, mais en pratique, basculer entre /pages et /app affiche un spinner — l’expérience se dégrade.
Deux semaines de galère, de threads communautaires et d’essais. Voici ce qui en est sorti :
- Comment juger si votre projet vaut le coup
- Deux stratégies, avantages et inconvénients réels (pas la théorie officielle)
- getServerSideProps : étapes et exemples
- 7 pièges que j’ai creusés moi-même
Si vous hésitez à upgrader ou si la migration coince, ce guide devrait vous faire gagner du temps.
Pourquoi migrer ? Faites le calcul
Un rappel froid : toutes les migrations ne valent pas le coup.
Un ami m’a demandé s’il fallait migrer une landing promo bientôt supprimée. Réponse : non — code jetable dans six mois, inutile d’y passer du temps.
Quand migrer ? Quelques critères :
Besoin de layouts imbriqués
C’était notre déclencheur. Notre back-office SaaS : sidebar + header + contenu sur trois niveaux. En Pages Router, chaque navigation re-rendait toute la sidebar.
L’utilisateur sentait l’interface clignoter — pas le réseau, le layout.
Les layouts imbriqués d’App Router règlent ça. WorkOS rapporte une « connexion nettement améliorée, sans loading ni jitter » — nos tests confirment : seul le contenu bouge, la nav reste fixe.
Marge d’optimisation perf
Premier affichage > 3 s ? App Router peut aider.
Notre liste produits utilisait getServerSideProps : chaque refresh attendait le HTML serveur. En Server Components, données streamées côté serveur — 3,2 s → 1,8 s.
Attention : pas toutes les pages gagnent. Un éditeur canvas côté client ? Quasi aucun gain, parfois un peu plus lent.
Projet sur le long terme
Maintenance 3 ans ou plus : migrer tôt. Vercel oriente les nouveautés vers App Router ; Pages Router est en mode maintenance.
Pas envie de refaire ça dans deux ans avec des API encore différentes.
Quand ne pas migrer
- Projet bientôt arrêté — inutile
- Petit site statique (≤ 5 pages) — ROI faible
- Équipe peu à l’aise React 18 — Suspense et Server Components d’abord
- Beaucoup de vieilles libs — incompatibilités en cascade
En bref : ne migrez pas pour la mode. Quel problème concret résolvez-vous ? Si la réponse est « aucun, curiosité », laissez tomber.
Notre calcul : deux semaines-homme pour une UX meilleure et moins de dette sur trois ans — rentable. Et vous ?
Choisir entre deux stratégies
La doc recommande la migration progressive — page par page, en douceur.
En pratique, un piège majeur.
Le piège de la migration progressive
Scénario : l’accueil est dans /app, la fiche produit encore dans /pages. Clic depuis l’accueil → écran blanc, spinner… puis le contenu.
Pourquoi ? Saut App Router → Pages Router = deux apps distinctes, rechargement complet du bundle JS. Retour en 2010.
WorkOS le dit aussi : « naviguer entre routeurs, c’est comme changer d’application ». Ils voulaient du progressif, ils ont abandonné.
Pas totalement impossible pourtant.
Cas adaptés :
- Pages peu couplées (blog)
- Migration par module entier (tout le compte utilisateur, puis le catalogue)
- Loading acceptable entre versions
Un blog tech l’a fait — correct. SaaS ou e-commerce : oubliez.
Le schéma zéro downtime de WorkOS
Grands projets ? WorkOS propose une astuce.
Recréer toutes les pages sous /app/new, bascule par paramètre de requête :
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/:path*',
destination: '/new/:path*',
has: [
{
type: 'query',
key: 'new',
value: 'true',
},
],
},
]
},
}
/dashboard reste l’ancienne version ; ?new=true montre la nouvelle.
Test, produit, design en prod sans impact utilisateur. Une fois validé : /app/new → /app, suppression de /pages et des rewrites.
C’est notre approche. Zéro bug utilisateur en prod — une semaine de tests sur données réelles avant bascule.
Étapes :
- Monter Next.js 14 — /pages intact pour l’instant
- Hooks de routing — next/router → next/navigation, compatibilité des deux routeurs
- Créer /app/new — structure des pages
- Réutiliser les composants — import depuis /pages
- Configurer rewrites — bascule
?new=true - Tests internes + canary — équipe sur la nouvelle version
- Mise en prod — /app/new → /app, nettoyage
De l’étape 1 à 7 : 10 jours ouvrés. 6 jours de pages, 3 de bugs, 1 de déploiement.
Mon conseil
- < 10 pages, indépendantes → progressive
-
10 pages, UX exigeante → zéro downtime
- Nouveau projet → App Router direct
Évitez de migrer en parallèle du développement feature — deux styles de code, c’est pénible. Deux semaines dédiées, ou attendez.
getServerSideProps en pratique
La question la plus fréquente : « getServerSideProps n’existe plus, comment fetcher ? »
App Router simplifie — il faut surtout changer de mentalité.
De la séparation à la fusion
Pages Router : données (getServerSideProps) et UI séparées ; Next.js appelle la fonction serveur et passe les props.
App Router : la page est une fonction async qui fetch directement :
// ❌ Ancien : 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>
}
// ✅ Nouveau : 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' // crucial — équivalent getServerSideProps
})
const project = await res.json()
return <h1>{project.title}</h1>
}
Plus simple ? Deux gros pièges derrière.
Piège 1 : mauvaise config cache
Par défaut, fetch en App Router est mis en cache (comme getStaticProps), pas à chaque requête.
J’ai migré une page prix sans y penser — prix figés, plaintes clients. Une demi-journée pour trouver le cache.
Table de correspondance :
getServerSideProps→cache: 'no-store'getStaticProps→cache: 'force-cache'(défaut)getStaticProps + revalidate→next: { revalidate: 60 }
Piège 2 : état client
Pages avec getServerSideProps + filtres, tri… En App Router, un async Server Component ne peut pas utiliser useState, useEffect.
Solution : découper.
// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
const products = await fetchProducts()
return <ProductList initialData={products} />
}
// components/ProductList.tsx (Client Component)
'use client'
import { useState } from 'react'
export function ProductList({ initialData }) {
const [products, setProducts] = useState(initialData)
const [filter, setFilter] = useState('')
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>
)
}
Serveur pour les données, client pour l’interaction.
Ne surchargez pas “use client” — toute la page en client, c’est perdre l’intérêt des Server Components.
Étapes de migration
Étape 1 : découper
Dans /pages, séparer affichage pur et composants avec état. Tester.
Étape 2 : déplacer vers /app
- Affichage pur → app/[route]/page.tsx, async, fetch dedans
- État → fichier séparé avec “use client”
- Supprimer getServerSideProps
Rollback facile si problème.
Détail : cookies et headers
Ancien context.req.cookies :
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = cookies()
const token = cookieStore.get('auth-token')
// token → requête utilisateur...
}
Idem pour headers(), redirect() depuis next/headers ou next/navigation.
7 pièges courants et solutions
Les sept suivants, je les ai tous creusés — au moins une heure de debug chacun.
Piège 1 : erreurs serveur avalées
Symptôme : page blanche ou skeleton, pas d’erreur visible.
Mon cas : API cassée, écran vide, console vide. Pas de error.tsx → Next.js avale l’exception, affiche le fallback Suspense.
Solution : error.tsx par segment :
// app/dashboard/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Erreur : {error.message}</h2>
<button onClick={reset}>Réessayer</button>
</div>
)
}
Dev : stack détaillée. Prod : message utilisateur.
Piège 2 : useRouter ne fonctionne plus
Symptôme : useRouter().push() ne navigue pas ou méthode absente.
Cause : next/router ≠ next/navigation.
// ❌ Erreur
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard') // push absent !
En fait push existe parfois, mais le comportement diffère — préférez Link :
// ✅ Mieux
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
const router = useRouter()
router.push('/dashboard')
import Link from 'next/link'
<Link href="/dashboard">Aller au dashboard</Link>
Table (collée sur mon écran pendant la migration) :
| Pages Router | App Router |
|---|---|
useRouter().push(url) | useRouter().push(url) (existe, déconseillé) |
useRouter().pathname | usePathname() |
useRouter().query | useSearchParams() |
useRouter().asPath | usePathname() + useSearchParams() |
Piège 3 : import dynamique cassé
Symptôme : next/dynamic ne rend rien — « You’re importing a component that needs useState… »
Cause : Server Component côté serveur ; libs client-only (graphiques) plantent.
Mon page ECharts :
// ❌ Erreur
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export default function Page() {
return <Chart data={data} />
}
Chart veut window, absent côté serveur.
Solution : “use client” sur la page, ou composant client dédié :
// 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} />
}
Piège 4 : scintillement à la navigation
Symptôme : header et sidebar clignotent à chaque lien.
Cause : pas de layout, ou mal configuré. J’avais la nav dans chaque page.tsx — évidemment ça flash.
Correct :
// app/layout.tsx (racine)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
</body>
</html>
)
}
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
)
}
Entre /dashboard/analytics et /dashboard/settings, seul <main> change.
Piège 5 : 404 personnalisée ignorée
Symptôme : 404 par défaut Next.js au lieu de la vôtre.
Cause : /pages/404.js encore présent — bloque app/not-found.tsx.
Solution : supprimer /pages/404.js et /pages/500.js :
// app/not-found.tsx
export default function NotFound() {
return <h1>Page introuvable</h1>
}
Déclencher depuis une page :
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>
}
Piège 6 : dev server de plus en plus lent
Symptôme : HMR à 10 s, puis crash.
Pas de solution parfaite de mon côté.
Problème connu Next.js 14. FlightControl : « perf dev server si mauvaise qu’on renoncerait aux nouveautés » — redémarrage toutes les 20 minutes.
Contournements :
- Redémarrer le dev server (~15 min, rappel)
next dev --turbo(Turbopack, parfois instable)- Moins de Server Components là où le client suffit
Next.js 15 aurait amélioré — pas encore testé chez nous.
Piège 7 : libs tierces incompatibles
Symptôme : Framer Motion, Lottie — window ou document introuvable.
Cause : libs purement client dans un Server Component.
Mes animations Framer Motion : tout cassé après migration.
Solutions :
- “use client” sur le composant concerné
- Vérifier une version React 18 compatible
- Changer de lib si nécessaire
Libs souvent client-only :
- Framer Motion (animations de sortie — issues ouvertes)
- swiper, slick-carousel
- ECharts, Chart.js
- react-dnd, dnd-kit
Projets lourds en animations : lire les issues GitHub avant de migrer.
Optimisations post-migration
La migration n’est pas la ligne d’arrivée.
Réduire le JavaScript client
Atout des Server Components.
Notre liste produits : 120 Ko gzip en React pur. Après migration, affichage en Server Component, filtres en Client — 45 Ko.
Vérifier :
npm run build
Pages (Static)/(SSR) vs ○ (Client Component). Tout en ○ = trop de “use client”.
Conseils :
- Texte et images → Server Component
- Formulaires, boutons → Client Component
- “use client” sur les sous-composants, pas toute la page
Cache raisonné
Plus fin qu’en Pages Router :
// Temps réel
fetch(url, { cache: 'no-store' })
// Revalidation 60 s
fetch(url, { next: { revalidate: 60 } })
// Statique
fetch(url, { cache: 'force-cache' })
Liste produits : revalidate 60 s — données fraîches, −60 % d’appels API.
Monitoring perf
Comparer :
- FCP — premier contenu visible
- TTI — page interactive
- CLS — stabilité visuelle
Vercel Analytics : FCP 3,2 s → 1,8 s, TTI 5,1 s → 3,3 s. L’éditeur canvas : quasi identique.
Éviter la sur-optimisation
Ne découpez pas un formulaire en 20 micro-composants pour le KPI Server Component.
Règle : besoin de useState/useEffect → “use client”, point. Les Server Components sont un outil, pas un objectif.
Conclusion
Migrer n’est pas une mode — c’est résoudre un problème : layouts imbriqués, moins de JS client, maintenance longue.
Stratégie : petit projet progressif, grand projet zéro downtime. Pas de migration en parallèle du feature dev.
Les pièges sont normaux — les sept ci-dessus ne sont que la surface. GitHub issues d’abord.
Pas de sur-optimisation — lisibilité et vélocité d’équipe > taille de bundle.
Conseil : pilotez 1–2 pages, documentez, puis généralisez. Chez nous : 3 jours la première, 5 jours pour les dix suivantes.
App Router a des défauts (surtout le dev server), mais la direction est bonne. L’écosystème comblera les trous.
Un blocage en migration ? Échangez en commentaire — j’ai peut-être le même sous les pieds.
Ressources :
Bonne migration !
Processus complet de migration Pages Router vers App Router
Étapes de l'évaluation au déploiement, avec choix de stratégie et résolution des problèmes courants
⏱️ Estimated time: 80 hr
- 1
Step 1: Évaluer si la migration en vaut la peine
Critères :
• Layouts imbriqués : plusieurs niveaux sans re-render à chaque navigation
• Optimisation perf : premier affichage > 3 s avec marge d'amélioration
• Maintenance long terme : projet maintenu 3 ans ou plus — migrer tôt pour en profiter
Ne pas migrer si :
• Projet bientôt arrêté
• Petit site statique (≤ 5 pages)
• Équipe peu à l'aise avec React 18
• Forte dépendance à des libs tierces anciennes - 2
Step 2: Choisir la stratégie de migration
Selon la taille du projet :
Petit projet (< 10 pages, pages indépendantes) → migration progressive :
• Page par page
• Loading acceptable entre /pages et /app
• Adapté aux blogs, faible couplage
Grand projet (> 10 pages, exigences UX) → zéro downtime :
• Reconstruire toutes les pages sous /app/new
• rewrites + paramètre de requête pour basculer
• Mise en prod après tests internes - 3
Step 3: Migrer getServerSideProps
Étapes :
1. Transformer la page en fonction async
2. fetch des données directement dans le composant
3. Options cache :
• getServerSideProps → cache: 'no-store'
• getStaticProps → cache: 'force-cache'
• getStaticProps + revalidate → next: { revalidate: 60 }
4. Interactions client dans un Client Component :
• Server Component pour les données
• Client Component pour useState, useEffect, etc. - 4
Step 4: Routes et navigation
Mettre à jour le code routing :
• next/router → next/navigation
• useRouter().pathname → usePathname()
• useRouter().query → useSearchParams()
• Préférer Link à router.push()
Note : useRouter de next/navigation se comporte différemment — privilégiez Link - 5
Step 5: Configurer les layouts
Layouts imbriqués pour éviter le scintillement :
• app/layout.tsx racine (Header, Footer)
• Sous-layouts par zone (ex. app/dashboard/layout.tsx)
• Chaque layout n'ajoute que l'UI propre à son niveau
• Héritage automatique, pas de re-render parent au changement de page - 6
Step 6: Erreurs et 404
Erreurs :
• error.tsx pour capturer et afficher un message clair
• Marquer error avec 'use client'
404 :
• Supprimer /pages/404.js
• Créer app/not-found.tsx
• Appeler notFound() dans page.tsx si besoin - 7
Step 7: Tests et optimisation
Tests :
• Toutes les routes
• Récupération des données
• Interactions client
• Pas de scintillement entre layouts
Perf :
• Réduire les 'use client' inutiles
• Stratégie de cache adaptée
• Surveiller FCP, TTI, CLS
• Comparer avant/après migration
FAQ
Quelle différence entre migration progressive et zéro downtime ?
Zéro downtime : tout reconstruit sous /app/new, bascule par paramètre de requête, prod après validation — grands projets et UX exigeante.
Comment récupérer les données après migration de getServerSideProps ?
Cache :
• getServerSideProps → cache: 'no-store'
• getStaticProps → cache: 'force-cache'
Interactions client : Server Component (données) + Client Component (UI interactive).
Pourquoi scintille-t-on au changement de page après migration ?
Comment utiliser useRouter dans App Router ?
Libs tierces incompatibles après migration ?
Exemples : Framer Motion, ECharts, Chart.js, carrousels.
Vérifiez les issues GitHub avant de migrer.
Dev server de plus en plus lent ?
Contournements :
• Redémarrer le dev server régulièrement (~15 min)
• next dev --turbo (Turbopack)
• Moins de Server Components inutiles
Next.js 15 aurait amélioré ce point.
Combien de temps dure une migration ?
• Petit projet (< 10 pages) : 3 à 5 jours
• Grand projet : 2 à 3 semaines
Pilotez 1–2 pages d'abord. Chez nous : 3 jours pour la première, 5 jours pour les 10 suivantes.
10 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Next.js 15 en pratique : comment j'ai construit un blog de niveau production en un week-end
Cas pratique Next.js 15 + Server Actions + Prisma : guide pas à pas pour construire un blog full-stack de niveau production en un week-end. Code complet, pièges rencontrés et stratégies d'optimisation des performances.
Partie 2 sur 51
Suivant
Routage avancé Next.js : groupes de routes, layouts imbriqués, routes parallèles et routes interceptées
Guide complet des quatre fonctionnalités de routage avancé Next.js : groupes de routes pour une arborescence claire, layouts imbriqués réutilisables, routes parallèles pour afficher plusieurs pages simultanément, routes interceptées pour des modales élégantes. Exemples de code complets et pièges à éviter.
Partie 4 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire