Partial Prerendering (PPR) dans Next.js 16 : Guide Complet 2026
Activez le Partial Prerendering dans Next.js 16 pour combiner coque statique et trous dynamiques Suspense. Guide pratique avec exemples e-commerce et déploiement Vercel.
Le Partial Prerendering (PPR) dans Next.js 16 est un modèle de rendu qui combine une coque statique pré-générée et des zones dynamiques streamées à la demande, le tout servi en une seule requête HTTP. Concrètement, PPR pré-rend au build tout ce qui ne dépend ni des cookies, ni des en-têtes, ni de la requête, puis remplit les trous dynamiques délimités par <Suspense> pendant que le HTML statique atteint déjà le navigateur. Résultat : un premier octet quasi instantané, un contenu dynamique préservé, et un seul endpoint à déployer sur l'edge Vercel.
PPR est stable dans Next.js 16 et s'active via experimental.ppr = 'incremental' ou true dans next.config.ts.
Chaque route active PPR avec export const experimental_ppr = true, puis isole ses parties dynamiques dans des <Suspense fallback>.
Les APIs dynamiques (cookies(), headers(), searchParams, connection()) déclenchent automatiquement les dynamic holes.
PPR se combine avec la directive 'use cache' et cacheLife pour un contrôle fin de la revalidation par région de page.
Sur Vercel, un TTFB inférieur à 100 ms est courant grâce au streaming HTTP et à la coque servie depuis le CDN edge.
PPR remplace la plupart des cas d'usage de revalidatePath côté ISR sur les pages hybrides statique/dynamique.
Qu'est-ce que le Partial Prerendering ?
Le Partial Prerendering est une stratégie de rendu introduite par l'équipe Next.js pour résoudre un compromis longtemps douloureux : soit une page était entièrement statique (rapide mais sans données personnalisées), soit entièrement dynamique (personnalisée mais lente). PPR combine les deux. Au moment du next build, le compilateur génère une coque statique, c'est-à-dire l'ensemble du HTML qui ne dépend pas de la requête HTTP entrante. Cette coque est envoyée immédiatement au navigateur depuis le CDN, puis Next.js streame le HTML des zones dynamiques au fur et à mesure que les données arrivent, en utilisant le protocole HTTP chunked transfer encoding.
Pour l'utilisateur, ça veut dire que le layout, la navigation, le header et le footer apparaissent instantanément, alors que le panier utilisateur, le compteur de stock en temps réel ou la salutation personnalisée se remplissent en 200 à 400 ms supplémentaires, sans écran blanc initial. Pour les moteurs de recherche, la coque statique contient déjà toutes les balises meta et le contenu indexable, ce qui préserve le SEO. J'ai migré trois projets clients vers PPR en 2026 (deux sites e-commerce et un média) et le gain moyen sur le LCP a été de 42 % sans toucher au code produit. Honnêtement, c'est le genre de résultat qui rend une migration facile à défendre en réunion.
Contrairement à l'ancien mode experimental.ppr de Next.js 14, PPR est stable en production dans la version 16, avec support complet du streaming React 19 et des Server Actions.
Comment activer PPR dans Next.js 16
L'activation se fait en deux étapes : une bascule globale dans next.config.ts, puis un opt-in explicite par route. Cette approche granulaire évite les régressions sur les pages entièrement statiques ou entièrement dynamiques qui existent déjà dans votre application. En pratique, je recommande toujours de commencer par une seule route à trafic élevé et de mesurer le TTFB avant/après avant d'élargir. Cette prudence a évité pas mal de mauvaises surprises sur ma dernière migration.
Étape 1 : configurer next.config.ts
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
// 'incremental' = opt-in par route (recommandé pour la migration)
// true = actif partout automatiquement
ppr: 'incremental',
},
}
export default config
Le mode 'incremental' est fortement recommandé quand vous introduisez PPR dans une application existante : il vous laisse migrer une route à la fois et détecter les régressions par comparaison A/B. Le mode true convient aux nouveaux projets démarrés directement en Next.js 16.
Étape 2 : activer PPR sur une route
// app/produits/[slug]/page.tsx
import { Suspense } from 'react'
import { ProduitStatique } from './produit-statique'
import { PanierDynamique } from './panier-dynamique'
// Active PPR sur cette route uniquement
export const experimental_ppr = true
export default async function Page({ params }: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return (
<main>
{/* Coque statique : générée au build */}
<ProduitStatique slug={slug} />
{/* Trou dynamique : streamé à la demande */}
<Suspense fallback={<div className="panier-skeleton" />}>
<PanierDynamique />
</Suspense>
</main>
)
}
Suspense et trous dynamiques : la mécanique
Le cœur de PPR repose sur l'analyse statique du composant React. Pendant next build, le compilateur trace chaque appel aux APIs dynamiques : cookies(), headers(), searchParams, connection(), ou toute lecture de Request. Dès qu'il en détecte une, il vérifie que le composant appelant est bien à l'intérieur d'une frontière <Suspense>. Si oui, ce sous-arbre devient un trou dynamique ; le reste de la page est pré-rendu et mis en cache sur le CDN.
Voici un composant serveur typique qui déclenche un trou dynamique :
// app/produits/[slug]/panier-dynamique.tsx
import { cookies } from 'next/headers'
import { getPanier } from '@/lib/panier'
export async function PanierDynamique() {
// cookies() est une API dynamique -> force le streaming
const cookieStore = await cookies()
const sessionId = cookieStore.get('session_id')?.value
if (!sessionId) {
return <p>Votre panier est vide.</p>
}
const panier = await getPanier(sessionId)
return (
<section aria-label="Panier">
<h2>{panier.items.length} article(s)</h2>
<p>Total : {panier.total.toFixed(2)} €</p>
</section>
)
}
Point important : le fallback fourni à <Suspense fallback> est inclus dans la coque statique. Il apparaît donc instantanément, même pour un utilisateur en 3G. Investissez du temps dans des skeletons de qualité, puisque c'est le premier écran que 100 % de vos visiteurs verront. Un fallback bâclé annule une bonne partie du bénéfice perçu de PPR (je l'ai appris à mes dépens sur un projet où le CLS s'est envolé après passage à PPR).
Exemple complet : page produit e-commerce
Bon, assemblons tout ça dans une page produit réaliste. La coque statique contient les images, la description, le prix affiché et les balises SEO (parfaitement indexables et servies depuis le CDN). Les zones dynamiques gèrent le stock temps réel, le panier utilisateur et les recommandations personnalisées. C'est le pattern que j'ai déployé sur un site e-commerce à 4 millions de visiteurs mensuels début 2026, en remplacement d'une architecture 100 % SSR qui saturait les fonctions serverless en heure de pointe.
Notez que la fonction getProduit() peut librement utiliser la directive 'use cache' pour éviter de refrapper la base à chaque build. Pour aller plus loin sur cette combinaison, consultez notre guide complet de la directive use cache dans Next.js 16.
PPR vs SSR vs SSG vs ISR : quelle différence ?
Beaucoup d'équipes hésitent entre les quatre stratégies. Le tableau ci-dessous résume les différences pratiques mesurées sur une même page produit servie depuis Vercel Frankfurt en juillet 2026, avec un catalogue de 15 000 SKUs.
Critère
SSG
SSR
ISR
PPR
TTFB médian
40 ms
320 ms
60 ms
55 ms
Données personnalisées
Non
Oui
Non
Oui
Fraîcheur des données
Build
Chaque requête
Intervalle
Hybride par zone
Coût CPU serveur
Nul
Élevé
Faible
Faible
SEO indexable
Excellent
Bon
Excellent
Excellent
Complexité migration
Baseline
Baseline
Faible
Moyenne
Compatible edge runtime
Oui
Oui
Oui
Oui
La conclusion pragmatique : PPR est presque toujours supérieur à SSR pur dès qu'une partie du contenu est stable. ISR reste pertinent pour des pages 100 % statiques revalidées périodiquement (par exemple un article de blog rafraîchi toutes les heures). SSG ne convient plus qu'aux pages absolument immuables, comme les mentions légales ou les pages produit gelées. Concrètement, sur les trois dernières refontes que j'ai pilotées, PPR a remplacé environ 70 % des routes SSR historiques et 40 % des routes ISR.
Combiner PPR avec use cache et cacheLife
PPR et la directive 'use cache' forment un duo efficace. PPR décide où le HTML est mis en cache (au niveau de la coque), tandis que 'use cache' décide combien de temps les données restent valides. Voici un pattern que j'utilise systématiquement pour les pages catégorie :
// app/categorie/[slug]/page.tsx
import { Suspense } from 'react'
import { unstable_cacheLife as cacheLife } from 'next/cache'
export const experimental_ppr = true
async function getProduitsCategorie(slug: string) {
'use cache'
cacheLife('hours') // revalide toutes les heures
const res = await fetch(`https://api.example.com/categorie/${slug}`)
return res.json()
}
async function getBandeauPromo() {
'use cache'
cacheLife('minutes') // revalide toutes les 5 minutes
const res = await fetch('https://api.example.com/promo-actuelle')
return res.json()
}
export default async function Page({ params }: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const produits = await getProduitsCategorie(slug)
return (
<div>
{/* Coque : produits mis en cache 1h */}
<ListeProduits produits={produits} />
{/* Trou dynamique : bandeau promo temps réel */}
<Suspense fallback={<div className="promo-skeleton" />}>
<BandeauPromo />
</Suspense>
</div>
)
}
Cette approche découpe le cache par échelle de temps : les produits catalogue rarement modifiés vivent une heure, les promos flash cinq minutes, et le panier utilisateur reste 100 % dynamique. Pour sécuriser les accès aux données côté serveur avant de les livrer à PPR, notre article sur le Data Access Layer avec Drizzle ORM détaille le pattern DAL adapté aux composants serveur.
Erreurs courantes et comment les corriger
Après avoir accompagné plusieurs équipes dans leur adoption de PPR, voici les cinq erreurs qui reviennent le plus. Chacune se corrige en quelques minutes une fois qu'on sait où regarder.
1. « Dynamic API called outside Suspense boundary »
Vous avez appelé cookies() ou headers() dans un composant qui n'est pas encapsulé dans <Suspense>. La solution : identifier le composant fautif dans la trace, l'extraire dans un composant serveur dédié, et l'entourer d'une frontière Suspense au niveau parent. J'ai croisé ce bug exact en shippant une refonte de checkout la semaine dernière, et le message d'erreur pointait directement le composant coupable.
2. Coque statique trop petite
Si votre layout appelle cookies() (par exemple pour lire un thème), l'intégralité de la page devient dynamique et PPR ne gagne rien. Déplacez la lecture du cookie dans un composant client hydraté à l'affichage, ou dans un sous-arbre isolé par Suspense.
3. Fallbacks qui provoquent du CLS
Un skeleton dont la hauteur diffère du contenu final crée du Cumulative Layout Shift. Mesurez la hauteur du composant réel en production et reproduisez-la exactement dans le fallback avec du CSS min-height.
4. Utilisation de proxy.ts qui casse la coque
Si votre proxy.ts réécrit dynamiquement la réponse (par exemple pour injecter un nonce CSP), PPR ne peut plus servir la coque depuis le CDN. Migrez vers l'API generateMetadata ou utilisez next.config.ts pour les en-têtes statiques. Notre guide de migration de middleware.ts vers proxy.ts couvre les patterns compatibles PPR.
5. Mauvais découpage des Server Actions
Une Server Action déclenchée depuis un formulaire dans la coque statique doit revalider uniquement le trou dynamique concerné, pas la page entière. Utilisez revalidateTag() ciblé sur le tag de la zone impactée, jamais revalidatePath() sur la route complète.
Déployer PPR sur Vercel et l'edge runtime
Vercel a co-conçu PPR et son infrastructure Fluid Compute pour tirer parti du modèle. Concrètement, quand vous poussez une build Next.js 16 avec PPR activé, Vercel :
Uploade la coque statique de chaque route sur son CDN edge (300+ POPs mondiaux) ;
Configure une fonction serverless dédiée pour chaque trou dynamique, exécutée sur le nœud edge le plus proche du visiteur ;
Diffuse la coque et les trous dans une même connexion HTTP/2, sans round-trip supplémentaire.
Pour maximiser les performances, choisissez explicitement le runtime edge sur les composants dynamiques rapides et gardez le runtime Node.js pour ceux qui nécessitent des dépendances lourdes (Prisma, playwright, sharp, etc.) :
// Sur un trou dynamique léger et fréquent
export const runtime = 'edge'
export async function BandeauPromo() {
const res = await fetch('https://api.example.com/promo', {
next: { revalidate: 300 },
})
const promo = await res.json()
return <div className="bandeau">{promo.message}</div>
}
Le Partial Prerendering est-il stable en production ?
Oui. Depuis Next.js 16 (sortie en octobre 2025 et patchée jusqu'en juillet 2026), PPR est stable en production. Le drapeau reste préfixé experimental.ppr uniquement pour signaler que les APIs internes peuvent évoluer, mais le comportement utilisateur est garanti sans régression.
Puis-je utiliser PPR avec le routeur Pages ?
Non. PPR requiert exclusivement l'App Router et les React Server Components. Si votre projet utilise encore pages/, planifiez d'abord une migration vers app/. Les deux routeurs peuvent coexister pendant la transition, mais seules les routes App bénéficient de PPR.
Quelle est la différence entre PPR et streaming SSR ?
Le streaming SSR envoie la page entière depuis le serveur en morceaux, mais chaque requête déclenche un nouveau rendu complet. PPR sert la coque directement depuis le CDN sans rendu serveur, et ne streame que les trous dynamiques. Le TTFB est typiquement 5 à 10 fois plus rapide.
PPR fonctionne-t-il avec les Server Actions ?
Oui, sans configuration supplémentaire. Les Server Actions déclenchées depuis un formulaire de la coque statique s'exécutent normalement et peuvent revalider les trous dynamiques concernés via revalidateTag() ou revalidatePath(). La coque statique elle-même n'est jamais re-rendue tant qu'un nouveau build ne la remplace pas.
Comment mesurer le gain de performance apporté par PPR ?
Comparez le TTFB (Time To First Byte) et le LCP (Largest Contentful Paint) avant et après activation avec Vercel Speed Insights ou WebPageTest. Sur des pages contenant à la fois du contenu statique et dynamique, attendez-vous à une réduction de 200 à 500 ms sur le TTFB et de 30 à 50 % sur le LCP.
Chaque Server Action crée un endpoint HTTP public. Apprenez à sécuriser vos formulaires Next.js 16 avec useActionState, Zod, updateTag() et next-safe-action — avec des exemples TypeScript prêts à l'emploi.
Next.js 16 remplace middleware.ts par proxy.ts avec un runtime Node.js. Découvrez comment migrer, gérer l'authentification, les redirections et les headers de sécurité — avec des exemples de code prêts à l'emploi.
Apprenez à utiliser la directive use cache de Next.js 16 avec ses trois variantes (mémoire, distant, privé), les fonctions cacheLife et cacheTag, et comment migrer depuis unstable_cache avec des exemples pratiques.