Next.js 16 App Router : 7 pièges à connaître avant
J'ai migré patricehuetz.fr vers Next.js 16 App Router en décembre 2025. Depuis, j'ai rencontré 7 pièges qui m'ont coûté du temps et qui ne sont pas dans la doc officielle — ou sont mentionnés en note de bas de page. Voici chacun avec le symptôme exact, le fix, et pourquoi je suis sûr que tu vas tous les rencontrer aussi si tu fais un vrai projet Next.js 16.
Piège 1 — Le cache qui ne s'invalide jamais
Next.js 16 cache par défaut les Server Components pendant 1 an. Oui, 1 an. Si tu fais un fetch qui dépend de tes données DB et que tu ne précises rien, ton composant est figé.
Symptôme : j'ai publié un nouvel article blog, il n'apparaissait pas sur la page d'accueil. J'ai clear le cache navigateur, rien. J'ai redéployé, rien. Il a fallu que je revalide explicitement.
Fix : en Next.js 16, active cacheComponents: true dans next.config.ts et utilise la directive 'use cache' avec cacheLife au lieu de l'ancien unstable_noStore (déprécié) :
// app/page.tsx
import { cacheLife, cacheTag } from 'next/cache';
async function getPosts() {
'use cache';
cacheLife('minutes'); // ou cacheLife({ revalidate: 60 })
cacheTag('posts');
return db.select().from(posts);
}
export default async function Page() {
const items = await getPosts();
return <PostList posts={items} />;
}Pour invalider explicitement depuis une Server Action :
import { revalidateTag } from 'next/cache';
await db.insert(posts).values(...);
revalidateTag('posts'); // prochaine requête voit la fraîcheurPiège 2 — Server Actions qui échouent silencieusement
Les Server Actions sont géniales sur le papier. En pratique, quand elles plantent, elles ne te disent pas toujours pourquoi. Mon cas : une action qui écrivait en base. En dev, parfaite. En prod sur Vercel, elle « réussissait » mais rien n'arrivait en DB.
Cause : l'action avait une erreur non-catchée dans un helper, et le runtime Vercel Functions avait rollback la transaction sans logger. La UI affichait un succès parce que action() retournait sans throw.
Fix : toujours wrap les Server Actions dans un try/catch avec logging explicite, et retourner un objet { success, error } à la place de void.
'use server';
export async function createPost(data: FormData) {
try {
const result = await db.insert(posts).values(...);
return { success: true, id: result.id };
} catch (e) {
console.error('createPost failed:', e);
return { success: false, error: String(e) };
}
}Piège 3 — `useSearchParams` qui force le client rendering
Si tu utilises useSearchParams dans un composant, Next.js force tout l'arbre parent à devenir client-rendered. Symptôme : ta page entière perd son streaming SSR, devient un SPA, et le premier load passe de 800ms à 2400ms.
Fix : wrap le composant qui utilise useSearchParams dans un <Suspense> dédié, qui isole le client rendering au lieu de le propager.
<Suspense fallback={<Skeleton />}>
<SearchParamsConsumer />
</Suspense>Piège 4 — Les types TypeScript de Route Handlers sont menteurs
En Next.js 16, les Route Handlers (app/api/route.ts) ont des types TypeScript qui ne correspondent pas au runtime. Exemple : params est typé comme un objet sync mais c'est en fait une Promise.
Symptôme : mon code TypeScript compilait. Le runtime crashait avec « params is a Promise, await it first ».
Fix :
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
...
}C'est documenté mais en bas de la page « breaking changes 15→16 ». Il faut vraiment chercher.
Piège 5 — Middleware qui modifie les headers est ignoré en prod
Mon middleware ajoutait un header X-Custom-User-Region en dev. Il marchait. En prod, le header disparaissait.
Cause : Vercel Edge Middleware + certaines optimisations de cache ignorent les headers custom qui ne sont pas dans une allowlist. Pas de warning, pas d'erreur.
Fix : utiliser NextResponse.next({ request: { headers: newHeaders } }) et vérifier que tes headers sont bien listés dans la config Vercel si tu veux qu'ils traversent le CDN.
export function middleware(request: NextRequest) {
const headers = new Headers(request.headers);
headers.set('x-user-region', getUserRegion(request));
return NextResponse.next({ request: { headers } });
}Piège 6 — `notFound()` dans un Server Component bloque tout le stream
Si tu appelles notFound() dans un Server Component au milieu d'un stream, la page entière est rerenderée depuis le début avec la 404. Les parts déjà streamées sont jetées.
Symptôme : une page avec un layout et 3 sections. La section 2 appelle notFound(). Résultat : layout déjà envoyé au client, puis re-render full de la 404, écran qui flash.
Fix : vérifier l'existence avant de rendre, et retourner null ou un composant <NotFound /> inline. Garder notFound() pour le top-level page component uniquement.
Piège 7 — La taille du build Vercel explose silencieusement
Mon build Vercel est passé de 12 MB à 47 MB entre deux déploiements sans que je touche à grand-chose. J'ai mis 2h à comprendre : un import type-only que j'avais refactoré était devenu runtime à cause d'un side-effect.
Symptôme : vercel build qui prend 4× plus longtemps, cold starts 3× plus lents en prod.
Fix : utiliser import type strictement pour les types, et vérifier avec npx @next/bundle-analyzer après chaque gros PR.
// ❌ import { User } from '@/lib/types'; // peut emporter du runtime
// ✅
import type { User } from '@/lib/types';Récap tableau
| Piège | Symptôme | Fix |
|---|---|---|
| 1. Cache 1 an | Données figées | revalidate = 60 ou noStore() |
| 2. Server Action silencieuse | Succès apparent, DB vide | try/catch + logging |
| 3. useSearchParams SSR cassé | Perf dégradée | Suspense wrapper |
| 4. Types menteurs params | Runtime crash | await params |
| 5. Middleware headers ignorés | Prod ≠ dev | NextResponse.next(...) |
| 6. notFound() bloque stream | Re-render full | check avant de call |
| 7. Bundle qui explose | Build lent, cold starts | import type + analyzer |
Ce qu'il faut retenir
- 1.Le cache par défaut à 1 an est le piège numéro 1 — révalide explicitement.
- 2.Les Server Actions échouent silencieusement sans try/catch.
- 3.useSearchParams, notFound(), middleware headers ont des comportements piégeux.
- 4.Les types TypeScript mentent sur les Route Handlers —
paramsest une Promise. - 5.Le bundle size peut exploser sans raison évidente — monitor-le.
Pour aller plus loin sur la pile technique derrière patricehuetz.fr et les choix d'architecture pour un blog technique moderne, je raconte tout ça dans mon livre sur les agents autonomes :
Soutenez mon travail sur Patreon
Accès anticipé aux articles, contenu exclusif, et la satisfaction de soutenir un auteur indépendant.
Rejoindre — à partir de 3€/mois