Pendant des années, choisir entre statique et dynamique revenait à choisir entre vitesse et fraîcheur. Le partial prerendering fait sauter ce compromis : une seule page peut être servie instantanément depuis le CDN et contenir des morceaux rendus à la demande, personnalisés par requête.
Concrètement, Next.js prérend un squelette HTML au build, y laisse des « trous » là où vivent les données dynamiques, puis streame ces trous à l'exécution. Le visiteur voit la structure de la page tout de suite, le contenu dynamique arrive derrière, sans blocage. Depuis Next.js 16 (octobre 2025), ce modèle est stable et intégré aux Cache Components — plus besoin des flags expérimentaux.
Reste à comprendre le mécanisme réel : comment le shell statique est construit, pourquoi Suspense découpe le rendu, ce que la directive use cache change, et les pièges qui font qu'une page « dynamique par accident » perd tout le bénéfice. C'est ce qu'on déroule ici, du build jusqu'à la production.
Le faux dilemme entre SSG et SSR
Jusqu'ici, une route Next.js était soit statique (générée au build, ultra-rapide, mais figée), soit dynamique (rendue à chaque requête, fraîche, mais dépendante de la latence de votre serveur et de votre base). Dès qu'une page contenait un seul élément personnalisé — un panier, un nom d'utilisateur, un prix à jour — la route entière basculait en dynamique. Le header, le footer, le contenu marketing identique pour tout le monde : tout repayait le coût du rendu serveur à chaque visite.
Le partial prerendering supprime ce basculement binaire. La granularité n'est plus la route, mais le composant. Ce qui est identique pour tous les visiteurs reste statique. Ce qui dépend de la requête devient un trou dynamique. Une page produit e-commerce illustre bien l'idée : la fiche (titre, description, images) est statique, tandis que le stock temps réel et les recommandations personnalisées sont des trous. Vous ne payez le rendu serveur que pour les 10 % de la page qui en ont réellement besoin.
Comment fonctionne le partial prerendering
Au build, Next.js exécute un rendu du composant serveur. Tout ce qui peut être résolu sans données de requête est figé dans un shell HTML : layout, navigation, contenu statique, et les fallbacks des zones dynamiques. Ce shell est un fichier statique, déployable sur un CDN comme n'importe quel HTML.
À l'exécution, la requête arrive sur le edge le plus proche du visiteur. Le CDN renvoie le shell immédiatement — il était déjà prérendu, aucune base n'est interrogée pour l'obtenir. En parallèle, l'origine rend les trous dynamiques et streame leur HTML dans la même réponse, en une seule connexion HTTP. Le navigateur assemble le tout au fur et à mesure.
// La page est statique par défaut. Seul ce qui est
// enveloppé dans un Suspense et lit des données de
// requête devient un trou rendu à l'exécution.
export default function ProductPage() {
return (
<main>
<ProductHeader /> {/* statique, dans le shell */}
<Suspense fallback={<StockSkeleton />}>
<LiveStock /> {/* dynamique, streamé */}
</Suspense>
</main>
)
}
Le rôle de Suspense : définir les trous dynamiques
La frontière entre statique et dynamique, c'est Suspense. Tout ce qui se trouve au-dessus d'une limite Suspense fait partie du shell et est prérendu. Tout ce qui est à l'intérieur d'une limite Suspense et qui lit des données de requête — cookies(), headers(), un fetch non caché — devient un trou. Next.js prérend alors uniquement le fallback au build, et rend le vrai composant à la demande.
Cette mécanique donne un contrôle explicite et lisible. Vous ne configurez pas le rendu dans un fichier à part : vous le dessinez directement dans le JSX, en décidant où placer vos limites Suspense. C'est la même API que celle utilisée pour le streaming classique, ce qui rend l'adoption progressive. Si vous avez déjà structuré vos pages avec des limites Suspense propres — comme détaillé dans notre article sur le streaming et Suspense dans l'App Router —, le partial prerendering s'active presque sans refonte.
Cache Components et la directive use cache
Dans Next.js 16, le partial prerendering ne s'active plus via experimental.ppr. Il fait partie des Cache Components, pilotés par la directive use cache. Le modèle mental s'inverse : au lieu de marquer ce qui est dynamique, vous marquez explicitement ce qui est mis en cache. Ce qui ne l'est pas est traité comme dynamique et rendu à la requête.
// 'use cache' rend ce segment cachable et donc
// intégrable au shell statique. Sans elle, tout accès
// à des données de requête le pousse dans un trou.
async function CategoryNav() {
'use cache'
const categories = await getCategories()
return <Nav items={categories} />
}
Cette approche clarifie une confusion fréquente : une page devenait « dynamique par accident » dès qu'un composant profond appelait cookies(). Avec use cache, la limite est déclarative et locale. Pour bien orchestrer revalidation et invalidation autour de cette directive, on renvoie vers notre guide dédié au caching dans l'App Router, qui couvre revalidateTag et les durées de cache.
Ce que le partial prerendering change sur le TTFB
Le gain principal est mesurable : le TTFB n'est plus déterminé par la vitesse de votre base, mais par la proximité du visiteur avec le edge le plus proche. Le shell part du CDN en quelques dizaines de millisecondes, quel que soit l'état de votre origine. Les requêtes lentes — un appel à un service de recommandation, une agrégation SQL — ne bloquent plus le premier octet : elles sont streamées après, dans leur propre trou.
Sur une page où 90 % du contenu est identique entre visiteurs, cela transforme le profil de performance. Le First Contentful Paint tombe au niveau d'un site statique, tandis que les zones dynamiques restent fraîches à chaque requête. Vous cessez d'arbitrer entre un site rapide mais figé et un site frais mais lent : vous obtenez les deux sur la même route, ce qui pèse directement sur les Core Web Vitals et donc le référencement.
Les pièges à éviter en production
Le premier piège est l'accès dynamique non maîtrisé. Un cookies() ou un headers() appelé en dehors d'une limite Suspense fait basculer toute la page hors du shell statique. Auditez vos composants partagés — un provider d'analytics ou de thème qui lit un cookie au niveau racine annule le bénéfice sur l'ensemble du site.
Le deuxième est le sur-découpage. Multiplier les limites Suspense sur des micro-composants crée trop de trous à streamer, avec un coût de coordination qui grignote le gain. Regroupez les zones dynamiques cohérentes sous une même limite plutôt que d'en semer partout.
En pratique
Le partial prerendering n'est pas une optimisation cosmétique : c'est un changement de modèle par défaut. On raisonne désormais en « statique sauf preuve du contraire », en isolant explicitement le dynamique derrière des limites Suspense et la directive use cache. La bonne démarche d'adoption est incrémentale : identifiez vos pages à fort trafic et à contenu majoritairement partagé — listings, fiches produit, pages de contenu —, placez-y des limites Suspense propres, puis mesurez le TTFB avant/après.
En production, la discipline compte plus que la configuration. La plupart des régressions viennent d'un accès dynamique caché dans un composant transverse, pas du framework lui-même. Un audit des providers racine et une lecture attentive de la sortie de build suffisent le plus souvent à récupérer un shell pleinement statique. Combiné à une stratégie de cache maîtrisée, le partial prerendering donne des pages qui se comportent comme du statique tout en restant personnalisées.
Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit ou d'un renfort tech ? Parlons-en.