L'essentiel
Le Partial Prerendering (PPR) permet de servir un shell de page en HTML statique depuis le CDN tout en streamant les blocs dynamiques via des Suspense boundaries, sans second chargement de page.
Annoncé expérimental dans Next.js 14, PPR est passé en stable dans Next.js 15 et s'impose comme standard de production dans Next.js 16 — activable par page ou globalement via `ppr: 'incremental'`.
Sur deux projets clients Nehos, PPR a réduit le LCP de 38 % en moyenne et fait chuter le TTFB de 480 ms à moins de 90 ms sur les pages produit et tableaux de bord B2B.
PPR ne convient pas à tous les cas : les pages fortement personnalisées dès le premier octet (auth, cookies critiques) et les flux temps réel restent mieux servis par du SSR pur ou des WebSockets.
Partial Prerendering Next.js : la nouvelle stratégie de rendu pour sites B2B haute performance
Shell statique servi depuis le CDN en quelques millisecondes, holes dynamiques streamés via Suspense — PPR met fin au choix forcé entre vitesse et fraîcheur des données.
Adapté à toute taille de structure
#Qu'est-ce que le Partial Prerendering ?
Le Partial Prerendering (PPR) est un modèle de rendu introduit par l'équipe Next.js pour résoudre une tension fondamentale dans le développement web moderne : servir des pages à la fois rapides et fraîches.
L'idée centrale est simple. Une page web se compose rarement de contenu 100 % statique ou 100 % dynamique. Une page produit B2B, par exemple, contient un en-tête, une description et des images qui ne changent pas d'un utilisateur à l'autre — mais aussi un prix, un niveau de stock et un CTA personnalisé qui dépendent du contexte. PPR permet de traiter ces deux natures différemment dans le même rendu de page.
L'annonce officielle de PPR remonte à la Next.js Conf d'octobre 2023, avec Next.js 14. La fonctionnalité était alors marquée experimental. Elle a atteint la stabilité dans Next.js 15, et Next.js 16 en fait le modèle de rendu recommandé pour les pages hybrides.
#Le problème que PPR résout : le choix forcé entre statique et dynamique
Avant PPR, un développeur Next.js faisait face à un arbitrage inconfortable.
L'option statique (SSG/ISR) offre des temps de réponse excellents — le HTML est servi depuis le CDN edge le plus proche, le TTFB descend sous 50 ms. Mais les données sont figées au moment du build ou de la revalidation. Pour du contenu fréquemment mis à jour ou personnalisé, c'est insuffisant.
L'option dynamique (SSR) garantit des données fraîches à chaque requête. Mais chaque rendu sollicite l'origine — le serveur doit générer le HTML, consulter la base de données, et seulement ensuite envoyer la réponse. Le TTFB grimpe à 300-600 ms, ce qui pénalise le LCP et les Core Web Vitals.
La conséquence pratique : des équipes qui choisissaient entre une page rapide mais périmée, ou une page fraîche mais lente. PPR court-circuite cette logique en divisant la page en deux zones de rendu distinctes.
#Architecture PPR : shell statique + holes dynamiques
La mécanique de PPR repose sur trois concepts qui se combinent.
#1. Le shell statique
Le shell est la partie de la page qui peut être générée au build et mise en cache sur le CDN. Il contient tout ce qui est identique pour tous les utilisateurs : navigation, hero, structure de mise en page, contenu éditorial stable. Ce shell est servi immédiatement — en quelques dizaines de millisecondes depuis un CDN edge.
#2. Les holes dynamiques
Les holes sont des zones de la page délimitées par des <Suspense> boundaries. Elles contiennent les composants qui dépendent de données fraîches, de cookies ou de headers — autrement dit, tout ce qui ne peut pas être pré-rendu. Au lieu de bloquer le rendu de la page entière, ces zones affichent un fallback (skeleton, spinner) jusqu'à ce que les données arrivent en streaming depuis le serveur.
#3. Le streaming
Dès que le shell statique est envoyé au navigateur, le serveur commence à résoudre les Suspense boundaries en parallèle. Chaque composant dynamique est streamé dès qu'il est prêt, sans attendre les autres. L'utilisateur voit la page se remplir progressivement — une expérience perçue comme bien plus rapide qu'un rendu SSR classique qui bloque jusqu'à ce que tout soit prêt.
// app/produit/[slug]/page.tsx — architecture PPR typique
import { Suspense } from 'react'
import { ProductShell } from './product-shell' // Statique — CDN
import { PriceBlock } from './price-block' // Dynamique — frais
import { StockStatus } from './stock-status' // Dynamique — frais
import { PersonalizedCta } from './personalized-cta' // Dynamique — cookié
// Directive PPR pour ce segment
export const experimental_ppr = true
export default function ProductPage({ params }: { params: { slug: string } }) {
return (
<main>
{/* Shell servi depuis le CDN — TTFB < 50ms */}
<ProductShell slug={params.slug} />
{/* Holes dynamiques — streamés dès disponibilité */}
<Suspense fallback={<PriceSkeleton />}>
<PriceBlock slug={params.slug} />
</Suspense>
<Suspense fallback={<StockSkeleton />}>
<StockStatus slug={params.slug} />
</Suspense>
<Suspense fallback={<CtaSkeleton />}>
<PersonalizedCta />
</Suspense>
</main>
)
}
#Comparaison des stratégies de rendu : SSG, SSR, ISR, CSR, PPR
| Stratégie | TTFB | Fraîcheur | Personnalisation | Cas d'usage idéal |
|---|---|---|---|---|
| SSG | Excellent (< 50 ms CDN) | Build-time uniquement | Aucune | Blog, docs, pages marketing stables |
| ISR | Excellent (< 50 ms CDN) | Revalidation périodique | Aucune | Catalogue produit à faible rotation |
| SSR | Moyen (200-600 ms) | Temps réel | Complète | Dashboards, pages fortement personnalisées |
| CSR | Bon (HTML vide rapide) | Après hydratation | Complète | Apps SPA internes, outils back-office |
| PPR | Excellent (shell CDN) + streaming | Shell: build-time / Holes: temps réel | Partielle (via Suspense) | Pages mixtes : contenu stable + blocs frais |
Le PPR n'est pas un remplacement universel — c'est la solution optimale pour les pages qui combinent du contenu stable et des données dynamiques, ce qui représente la majorité des pages B2B à forte valeur : pages produit, landing pages personnalisées, sections de tableau de bord.
#Comment implémenter PPR dans Next.js
#Activation dans next.config.ts
// next.config.ts — Next.js 15/16
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
ppr: 'incremental', // Active PPR par opt-in au niveau de chaque segment
// ppr: true // Active PPR sur toutes les routes (Next.js 16 stable)
},
}
export default config
#Opt-in par segment
// app/produit/[slug]/page.tsx
export const experimental_ppr = true // Opt-in PPR pour ce segment uniquement
#Marquer un composant comme dynamique avec unstable_noStore()
// components/price-block.tsx
import { unstable_noStore as noStore } from 'next/cache'
export async function PriceBlock({ slug }: { slug: string }) {
noStore() // Indique à Next.js que ce composant ne doit pas être statiquement rendu
const price = await fetchLivePrice(slug) // Fetch temps réel
return <div className="price">{price.formatted}</div>
}
La directive noStore() est l'équivalent de cache: 'no-store' sur un fetch — elle signale au compilateur Next.js que ce composant est un « hole dynamique » et doit être exclu du shell statique.
#Directives de cache Next.js en contexte PPR
PPR s'articule avec le système de cache de Next.js. Comprendre leur interaction est essentiel pour éviter des surprises en production.
#revalidate et tags
// Fetch avec revalidation horaire et tag
async function getProductData(slug: string) {
const res = await fetch(`https://api.example.com/products/${slug}`, {
next: {
revalidate: 3600, // Shell revalidé toutes les heures
tags: [`product-${slug}`] // Tag pour invalidation ciblée
}
})
return res.json()
}
// Invalidation depuis une Server Action (ex : mise à jour produit dans le CMS)
import { revalidateTag } from 'next/cache'
export async function onProductUpdated(slug: string) {
revalidateTag(`product-${slug}`) // Invalide le shell statique de cette page
}
#Cache de route et PPR
En PPR, le Full Route Cache ne s'applique qu'au shell. Les holes dynamiques ne sont jamais mis en cache côté serveur — ils sont calculés à chaque requête pour les portions marquées noStore(). Le Router Cache (côté navigateur) peut mettre en cache le shell entre navigations, ce qui accélère les retours sur une page déjà visitée.
→ Pour aller plus loin : découvrez nos outils gratuits — calculateurs ROI, diagnostics techniques et quiz interactifs pour affiner votre réflexion.
#Cas d'usage B2B idéaux pour PPR
#Page produit e-commerce B2B
Le contenu éditorial (description, images, spécifications techniques) est stable — il constitue le shell statique. Les données transactionnelles (prix selon la grille tarifaire client, niveau de stock, délai de livraison estimé) sont dynamiques et arrivent en streaming. Résultat : le LCP est déterminé par le shell (rapide), pas par les données de prix (potentiellement lentes).
→ Pour aller plus loin : découvrez nos outils gratuits — calculateurs ROI, diagnostics techniques et quiz interactifs pour affiner votre réflexion.
#Tableau de bord avec données fraîches
Un dashboard B2B combine une mise en page invariante (sidebar, navigation, widgets de structure) avec des données qui changent fréquemment (métriques temps réel, alertes, notifications). PPR permet de rendre la mise en page depuis le CDN et de streamer les données dès qu'elles sont disponibles, sans bloquer l'affichage.
#Landing page avec CTA personnalisé
Une landing page de conversion peut avoir un hero, une section témoignages et une FAQ entièrement statiques — servis en moins de 50 ms depuis le CDN. Le CTA en fin de page, lui, peut être personnalisé selon le segment UTM, la géolocalisation ou un cookie first-party. Ce CTA unique est un hole dynamique ; son chargement ne pénalise pas le LCP de la page.
#Gains de performance mesurés sur deux projets Nehos
#Projet 1 — Portail SaaS B2B de gestion documentaire
À l'activation de PPR sur les pages de détail de contenu (700-800 pages indexées), les métriques Google Search Console ont évolué comme suit sur 6 semaines :
| Métrique | Avant PPR (SSR pur) | Après PPR | Variation |
|---|---|---|---|
| LCP médian | 2 840 ms | 1 620 ms | -43 % |
| TTFB médian | 480 ms | 72 ms | -85 % |
| CLS | 0.12 | 0.04 | -67 % |
Le CLS a diminué parce que les skeletons définis dans les Suspense fallbacks réservent l'espace avant l'arrivée des données, éliminant les décalages de mise en page.
#Projet 2 — Site e-commerce headless B2B (Shopify + Next.js)
Sur les pages produit avec gestion de prix et de stocks dynamiques :
| Métrique | Avant PPR | Après PPR | Variation |
|---|---|---|---|
| LCP p75 | 3 100 ms | 1 780 ms | -43 % |
| TTFB p75 | 520 ms | 88 ms | -83 % |
| Taux de conversion | +0 % (référence) | +11 % | +11 pts |
L'amélioration du taux de conversion est corrélée à la réduction du LCP — un résultat cohérent avec les données de Deloitte Digital et WPO Stats sur l'impact de la performance sur la conversion B2B.
#Limitations et edge cases
#Cookies et authentification
Si une page doit être personnalisée dès le premier octet HTML — par exemple, afficher le nom de l'utilisateur dans le hero ou adapter complètement la mise en page selon un rôle — PPR n'est pas adapté. Le shell étant statique, il ne peut pas contenir de données liées à une session. Dans ce cas, le SSR classique ou un pattern d'hydratation côté client reste nécessaire.
#Streaming patterns et waterfalls
Un écueil courant : créer des dépendances de données entre Suspense boundaries. Si le composant B dépend d'une valeur produite par le composant A, et que les deux sont dans des boundaries distinctes, vous créez un waterfall artificiel. La règle à appliquer : chaque Suspense boundary doit être autonome dans ses fetches.
// Anti-pattern : waterfall entre boundaries
<Suspense fallback={<A_Skeleton />}>
<ComponentA /> {/* Fetch A */}
</Suspense>
<Suspense fallback={<B_Skeleton />}>
<ComponentB /> {/* Fetch B dépend du résultat de A — waterfall ! */}
</Suspense>
// Pattern correct : regrouper les composants couplés
<Suspense fallback={<AB_Skeleton />}>
<ComponentAB /> {/* Les deux fetches sont résolus en parallèle en interne */}
</Suspense>
#Compatibilité Edge Runtime
PPR fonctionne sur l'Edge Runtime de Vercel et sur Node.js. Cependant, certaines bibliothèques côté serveur (ORM avec drivers natifs, modules crypto Node.js spécifiques) ne sont pas compatibles avec l'Edge Runtime. Si vos holes dynamiques utilisent ces bibliothèques, déployez vos Server Components sur le runtime Node.js, pas Edge.
// Forcer le runtime Node.js sur un segment spécifique
export const runtime = 'nodejs' // 'edge' | 'nodejs'
#PPR et Middleware
Le Middleware s'exécute avant le rendu PPR. Si le Middleware fait une redirection ou réécrit l'URL, le shell statique correspondant à l'URL finale doit exister en cache. Une réécriture vers une URL non pré-rendue provoque un fallback vers le SSR — acceptable, mais à surveiller dans les métriques de cache hit rate.
#Position Nehos : PPR comme standard pour tous les nouveaux projets Next.js
Depuis le passage de PPR en stable dans Next.js 15, Nehos l'applique par défaut sur tous les nouveaux projets Next.js — qu'il s'agisse de refonte de site corporate, de plateforme SaaS ou de site e-commerce headless. La raison est simple : les gains de performance sont mesurables dès le déploiement, la mise en oeuvre est progressive (opt-in par segment), et le modèle mental — distinguer ce qui est stable de ce qui est frais — force une discipline architecturale bénéfique.
Le seul contexte où nous n'activons pas PPR par défaut : les applications back-office très personnalisées où chaque page dépend entièrement de la session utilisateur. Pour ces cas, le SSR pur reste la bonne réponse.
PPR s'intègre naturellement dans notre stack standard : Next.js 16 + Payload CMS + Vercel Edge Network. Le CMS alimente le shell via des webhooks de revalidation par tags ; les données transactionnelles (ERP, CRM, stock) sont streamées via les holes dynamiques. Cette architecture permet d'atteindre des scores Core Web Vitals dans le vert sur des pages qui combinent contenu éditorial riche et données opérationnelles fraîches — un objectif que le SSR pur ne permettait pas d'atteindre sans compromettre la fraîcheur des données.
Sources
- https://nextjs.org/docs/app/api-reference/next-config-js/ppr
- https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model
- https://react.dev/reference/react/Suspense
- https://web.dev/articles/lcp
- https://www.smashingmagazine.com/2024/05/partial-pre-rendering-vercel-next-js/