Rendu à la demande Next.js
Pointez bext vers un répertoire Next.js app/ et commencez à servir – pas de répertoire next build, pas de répertoire .next/. Les pages se compilent à la première demande et sont mises en cache. Comme PHP, mais avec React.
Démarrage rapide#
# In your Next.js project directory:
bext dev .
# bext.config.toml
[server]
app_dir = "."
[framework]
type = "nextjs"
[nextjs]
on_demand = true
C'est ça. Visitez http://localhost:3000/ et bext :
- Scanner
app/pour les fichierspage.tsx+layout.tsx - Compilez la page demandée à la demande (travailleur au pain chaud, ~15 ms)
- Rendu via V8 —
renderToStringpour les pages de synchronisation,renderToReadableStream+ pilote de boucle d'événements personnalisé de bext pour le streaming - Mettre en cache à la fois le bundle compilé et le contexte de page V8 chaud (le prochain appel à la même route ignore entièrement l'évaluation du bundle, ~ 5 ms)
- Servir les requêtes suivantes à partir du cache
Comment ça marche#
GET /blog/hello-world
│
▼
Route scanner matches: app/blog/[slug]/page.tsx
│
▼
ISR cache check → miss (first request)
│
▼
Bundle cache check → miss (never compiled)
│
▼
Generate wrapper entry:
import Layout from "app/layout.tsx"
import Page from "app/blog/[slug]/page.tsx"
renderToReadableStream(<Layout><Page params={{slug:"hello-world"}} /></Layout>)
│
▼
Warm bun worker → compiled JS (~15ms)
│
▼
V8 streaming render → HTML (~3ms warm, ~30ms cold)
│
▼
Cache: bundle (by route + mtime), warm V8 context (by bundle hash), HTML (ISR with TTL)
│
▼
Return HTML
Next request: GET /blog/hello-world → ISR cache hit → 0ms
Configuration#
[nextjs]
compat = "full" # Real layout.tsx, metadata, full Next.js semantics
on_demand = true # Enable on-demand compilation
bundler = "bun" # bun (warm worker pool) or turbopack (in-process Rust)
bundle_cache_max = 500 # Max cached compiled bundles
[render]
streaming = true # React 19 renderToReadableStream + bext event loop
# rsc = true # Experimental RSC Flight payload endpoint
# streaming_timeout_ms = 5000 # Wall-clock cap for a single render
# streaming_stall_ms = 250 # Bail out if no chunk for this long
[nextjs.aliases] # Import path aliases
"@/*" = "src/*"
compat = "full" signifie que bext comprend le vrai layout.tsx, les exportations de métadonnées, les composants du serveur asynchrone et le reste du contrat App Router. Les alias de chemin proviennent automatiquement de votre tsconfig.json.
Ce qui est pris en charge#
| Fonctionnalité | Statut | Remarques |
|---|---|---|
Pages statiques ( page.tsx ) |
✅ | Compilé + mis en cache à la première demande |
Mises en page ( layout.tsx ) |
✅ | Composition imbriquée (racine → feuille), cache shell par chaîne de mise en page |
Itinéraires dynamiques ( [slug] ) |
✅ | Paramètres extraits de l'URL |
Fourre-tout ( [...path] ) |
✅ | Tous les segments restants capturés |
Groupes de routages ( (group) ) |
✅ | Aucun impact sur l'URL, organisation uniquement |
generateStaticParams |
✅ | Utilisé pour les conseils de pré-rendu |
Métadonnées statiques ( export const metadata ) |
✅ | Détecté au moment de l'analyse |
Itinéraires API ( route.ts ) |
✅ | GET/POST/etc., paramètres dynamiques via le deuxième argument |
| Composants du serveur asynchrone | ✅ | Natif — streaming = true utilise la boucle d'événements V8 de renderToReadableStream + bext |
Limites <Suspense> |
✅ | Le streaming SSR pousse les enfants asynchrones jusqu'à leur achèvement |
Crochet use(promise) |
✅ | Piloté par la boucle de rendu en streaming |
redirect() / notFound() |
✅ | Traduit en HTTP 307/404 via la capture Error.prototype.digest |
next/headers / next/cookies |
✅ | Lire la demande en direct |
Composants 'use client' |
✅ | Rendu par le serveur au format HTML, le bundle client les hydrate |
| Charge utile du vol RSC (expérimental) | ⚙️ | [render] rsc = true expose /__bext/rsc/<path> renvoyant une charge utile de vol réelle via react-server-dom-parcel |
Actions du serveur ( 'use server' ) |
🔜 | Intégration via decodeReply/decodeAction de react-server-dom-parcel — en cours |
Adaptateurs des API Next.js#
L’adaptateur require de V8 dans bext prend directement en charge les API serveur courantes de Next.js, sans installation :
import Link from "next/link" // → <a>
import Image from "next/image" // → <img>
import { redirect, notFound } from "next/navigation" // → 307 / 404 via digest
import { headers, cookies } from "next/headers" // → live request
import { revalidateTag } from "next/cache" // → bext cache invalidation
import { NextResponse } from "next/server" // → response builder
redirect() et notFound() définissent leur champ digest, puis bext installe sur l’enveloppe SSR un accesseur d’écriture Error.prototype.digest qui capture l’URL ou le code et convertit l’erreur levée en réponse HTTP appropriée.
Composants serveur asynchrones, Suspense et use()#
Les composants serveur asynchrones, <Suspense> avec des enfants asynchrones et le hook use(promise) fonctionnent tous nativement lorsque [render] streaming = true, la valeur par défaut des nouveaux projets :
// app/blog/[slug]/page.tsx
export default async function Page({ params }) {
const { slug } = await params;
const post = await fetchPost(slug);
return <article>{post.title}</article>;
}
V8 n’a pas de boucle d’événements native ; bext en implémente donc une en Rust : collect_async_render_result fait progresser perform_microtask_checkpoint() jusqu’à ce que le renderToReadableStream de React indique done, avec détection des blocages (streaming_stall_ms) et limite de durée totale (streaming_timeout_ms). L’accès direct aux objets V8 évite le coût de compilation et d’évaluation JavaScript à chaque tour. Un rendu en streaming à chaud utilise ainsi environ 1 ms du thread d’évaluation.
Si vous définissez streaming = false, bext utilise le renderToString synchrone de React. Ce chemin ne prend pas en charge les composants serveur asynchrones, les limites Suspense ou use(promise), mais évite entièrement la boucle d’événements : utile pour les projets sans récupération asynchrone de données côté serveur.
Composants du serveur React (expérimental)#
Définissez [render] rsc = true pour que bext expose /__bext/rsc/<path> sur toute route correspondante. Ce point de terminaison renvoie une véritable charge Flight React produite par react-server-dom-parcel/server.edge et prête à être consommée par createFromFetch côté client.
En interne, bext compile un bundle RSC distinct pour chaque route via le processus bun maintenu à chaud :
- Bun est appelé avec
--conditions=react-server; React est donc résolu vers sa version réservée au serveur - Le bundle intègre ce React réservé au serveur au lieu de le laisser externe : cela permet d’exécuter le bundle Flight comme un script classique autonome dans V8 et évite que ses mécanismes internes React entrent en conflit avec le React habituel du bundle HTML SSR
- Le bundle compilé est évalué dans un contexte de page V8 distinct de celui du bundle HTML SSR. Le contexte SSR et le contexte RSC sont mis en cache séparément : la requête suivante sur la même route réutilise les deux
- Une charge utile de vol pour un petit itinéraire atterrit en ~ 10 ms à chaud, ~ 50 ms à froid (compilation + évaluation combinées)
Le point de terminaison Flight est actuellement uniquement localhost et additif : il ne remplace pas encore la réponse HTML SSR. Le manifeste client, le bootstrap d’hydratation et le câblage 'use server' sont les éléments restants avant que RSC ne soit entièrement de bout en bout.
Cache de paquet#
Les bundles compilés sont mis en cache par route :
| Clé | route:/blog/:slug |
|---|---|
| Annulation | Modifications de page.tsx mtime, ou toute modification de layout.tsx mtime |
| Nombre maximum d'entrées | 500 (configurable via bundle_cache_max) |
| Expulsion | LRU lorsqu'il est plein |
Lorsque vous modifiez un fichier :
- L'observateur de fichiers détecte le changement de mtime
- Entrée du cache du bundle invalidée pour les routes concernées
- Entrées du cache ISR effacées pour ces routes
- La prochaine requête est recompilée à la demande (~ 15 ms)
Performance#
Validé de bout en bout via le harnais SSR dans harnesses/ssr-dev/ (33 tests de base + 10 tests spécifiques au streaming par rapport à une véritable application de luminaire Next.js) :
| Scénario | Temps |
|---|---|
| Compilation de petits pains chauds (page unique + chaîne de mise en page) | ~13 à 15 ms |
| Rendu à froid V8 (évaluation du bundle + premier appel) | ~30 ms |
| Rendu chaud V8 (contexte de page mis en cache) | ~3 à 5 ms |
| Rendu en streaming – composant serveur asynchrone + Suspense | ~5 ms |
| Charge utile du vol RSC (froid) | ~50 ms |
| Charge utile du vol RSC (chaud) | ~10 ms |
| Accès au cache ISR | sous-milliseconde |
| Changement de fichier → invalider → recompiler | ~15 ms |
Voir bext.dev/benchmarks pour les chiffres complets et comment ils ont été mesurés.
Exemple : nextjs-blog#
Le répertoire examples/nextjs-blog contient une application Next.js 15 complète :
examples/nextjs-blog/
bext.config.toml
package.json
app/
layout.tsx # Root layout with nav + footer
page.tsx # Home page with post list
blog/
[slug]/
page.tsx # Dynamic blog post with async params
api/
revalidate/
route.ts # ISR revalidation endpoint
Exécutez-le :
cd examples/nextjs-blog
bun install
bext dev .
Visitez http://localhost:3060/ – la page d’accueil s’affiche instantanément. Cliquez sur un article de blog : compilé lors de la première visite, mis en cache ensuite.
Comparaison avec next build#
next build + next start |
bext à la demande | |
|---|---|---|
| Étape de construction | Obligatoire (~ 30-60 s) | Aucun |
| Chargement de la première page | Pré-rendu | ~ 20 ms de compilation |
| Chargements ultérieurs | Instantané | Instantané (cache ISR) |
| Changement de fichier | Reconstruction requise | Recompilation automatique (~ 15 ms) |
| Mémoire | Répertoire.next/ complet | Cache par itinéraire |
| Démarrage à froid | Charger tous les itinéraires | Ne chargez rien, compilez à la demande |
Le bext à la demande est idéal pour les sites de développement et de contenu où le retour instantané compte plus que la première peinture de zéro ms.
streaming = false désactive les composants du serveur asynchrone, les limites Suspense et use(promise). Définissez ceci uniquement si vos pages n'ont pas de récupérations asynchrones côté serveur et que vous souhaitez ignorer complètement la boucle d'événements V8.
En rapport#
- Intégration Next.js — le package SDK
@bext/nextjspour le cache, le KV, les files d'attente et les tâches - Présentation des frameworks — comparaison de tous les choix de framework
- PRISM — Le framework TSX natif de bext pour les nouveaux projets
- Pool de rendu V8 — comment fonctionne le pool d'isolation V8 de bext
- Mise en cache — TTL, périmé pendant la revalidation, mécanique ISR
Links#
- prochainjs.org — Documentation officielle de Next.js