Nehos Groupe

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

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··next-headless

#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égieTTFBFraîcheurPersonnalisationCas d'usage idéal
SSGExcellent (< 50 ms CDN)Build-time uniquementAucuneBlog, docs, pages marketing stables
ISRExcellent (< 50 ms CDN)Revalidation périodiqueAucuneCatalogue produit à faible rotation
SSRMoyen (200-600 ms)Temps réelComplèteDashboards, pages fortement personnalisées
CSRBon (HTML vide rapide)Après hydratationComplèteApps SPA internes, outils back-office
PPRExcellent (shell CDN) + streamingShell: build-time / Holes: temps réelPartielle (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étriqueAvant PPR (SSR pur)Après PPRVariation
LCP médian2 840 ms1 620 ms-43 %
TTFB médian480 ms72 ms-85 %
CLS0.120.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étriqueAvant PPRAprès PPRVariation
LCP p753 100 ms1 780 ms-43 %
TTFB p75520 ms88 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.

Questions & Réponses

Questions fréquentes sur le Partial Prerendering Next.js

L'ISR (Incremental Static Regeneration) régénère l'intégralité d'une page à intervalles définis — toute la page est soit en cache, soit en cours de régénération. Le PPR divise une même page en zones : le shell statique (permanent en CDN) et les holes dynamiques (calculés à chaque requête). PPR offre donc des données plus fraîches sans sacrifier les performances du shell, là où l'ISR implique toujours un délai de revalidation même pour les données les plus critiques.
PPR convient aux pages dont le shell est identique pour tous les utilisateurs, même non authentifiés. Pour les sections personnalisées (profil, prix client, rôle utilisateur), ces données arrivent via des holes dynamiques après lecture du cookie de session côté serveur. En revanche, si toute la page doit être différente selon l'utilisateur dès le premier octet, le SSR pur est plus adapté — le shell PPR ne peut pas contenir de données de session.
Ajoutez `experimental: { ppr: 'incremental' }` dans votre `next.config.ts`, puis ajoutez `export const experimental_ppr = true` dans les segments (fichiers page.tsx ou layout.tsx) où vous voulez activer PPR. Entourez ensuite les composants dynamiques avec des `<Suspense>` boundaries et marquez-les avec `unstable_noStore()`. La migration est progressive — vous pouvez activer PPR page par page sans toucher au reste de l'application.
Le principal risque est de manquer des composants qui lisent des cookies ou des headers sans être dans une Suspense boundary — Next.js lèvera une erreur en build pour signaler ces cas. Un second risque est le CLS (Cumulative Layout Shift) si les fallbacks Suspense ne réservent pas l'espace correct pour les données entrantes. Testez toujours sur un environnement de staging avec des outils de mesure CWV (Lighthouse, Web Vitals extension) avant de déployer en production.
Oui, PPR s'intègre naturellement avec les CMS headless. Le contenu éditorial (textes, images, structure de page) alimente le shell statique via des fetches taggués et revalidés par webhook à chaque publication. Les données transactionnelles ou personnalisées (stock, prix, recommandations) restent dans des holes dynamiques. Cette combinaison est précisément l'architecture que Nehos utilise avec Next.js 16 + Payload CMS sur les projets B2B.
En Next.js 14, PPR requiert `experimental: { ppr: true }` dans la config et reste instable. En Next.js 15, PPR est stabilisé avec le mode `incremental` qui permet l'opt-in par segment. En Next.js 16, le mode `incremental` devient le défaut recommandé et la directive de segment passe de `experimental_ppr` à simplement `ppr` dans certaines configurations — vérifiez les notes de version pour le flag exact selon votre version cible.
Les deux sont liés directement. Google utilise les Core Web Vitals (LCP, INP, CLS) comme signal de ranking depuis le Page Experience Update. Un LCP réduit de 2,8 s à 1,6 s passe d'une note « à améliorer » à « bon » dans Search Console, ce qui influence positivement le positionnement. Par ailleurs, PPR livre un HTML sémantique complet dans le shell — le contenu éditorial structuré est donc crawlable immédiatement par Googlebot, sans attendre le JavaScript côté client.
Réserver un audit