Aller au contenu principal
Next.js 16 App Router : 7 pièges à connaître avant
Retour au blog
Technique

Next.js 16 App Router : 7 pièges à connaître avant

Patrice Huetz11 avril 20266 min

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é) :

typescript
// 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 :

typescript
import { revalidateTag } from 'next/cache';
await db.insert(posts).values(...);
revalidateTag('posts');  // prochaine requête voit la fraîcheur
⚠️
Cache par défaut à **1 an** = la surprise du siècle quand tu découvres ça en prod. Lis les release notes de 16 avec attention.

Piè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.

typescript
'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.

tsx
<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 :

typescript
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.

typescript
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.

typescript
// ❌ import { User } from '@/lib/types'; // peut emporter du runtime
// ✅
import type { User } from '@/lib/types';

Récap tableau

PiègeSymptômeFix
1. Cache 1 anDonnées figéesrevalidate = 60 ou noStore()
2. Server Action silencieuseSuccès apparent, DB videtry/catch + logging
3. useSearchParams SSR casséPerf dégradéeSuspense wrapper
4. Types menteurs paramsRuntime crashawait params
5. Middleware headers ignorésProd ≠ devNextResponse.next(...)
6. notFound() bloque streamRe-render fullcheck avant de call
7. Bundle qui exploseBuild lent, cold startsimport type + analyzer
💡
Avant de passer en prod sur Next.js 16, lance **systématiquement** `next build` en local, inspecte le bundle avec `@next/bundle-analyzer`, et teste au moins 3 routes critiques en mode production.
⚠️
Next.js 16 est stable mais plein de breaking changes par rapport à 14/15. Budgétise au moins 2 jours de debug par projet complexe lors d'une migration.

Ce qu'il faut retenir

  1. 1.Le cache par défaut à 1 an est le piège numéro 1 — révalide explicitement.
  2. 2.Les Server Actions échouent silencieusement sans try/catch.
  3. 3.useSearchParams, notFound(), middleware headers ont des comportements piégeux.
  4. 4.Les types TypeScript mentent sur les Route Handlers — params est une Promise.
  5. 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 :

La Boucle Ralph
La Boucle Ralph

Guide Pratique du Coding Autonome par IA.

Découvrir →
🔒

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

Commentaires

Chargement des commentaires...

Laisser un commentaire