Moteur de rendu V8
bext utilise V8 (via rusty_v8 ) pour tous les rendus côté serveur. Le pool de rendu est indépendant du moteur au niveau de l'API publique - historiquement, il prenait également en charge WebKit/JavaScriptCore, mais ce backend a été supprimé depuis les instantanés de tas de la V8 et TurboFan JIT offrait de meilleures performances de démarrage à froid et d'état stable pour chaque déploiement réel.
Bâtiment#
cargo build -p bext-server --release \
--no-default-features \
--features v8,tls,nginx-compat,php,route-css
La fonctionnalité v8 fait partie de l'ensemble de fonctionnalités par défaut. Le --no-default-features ci-dessus dépose simplement les bagages dont nous n'avons pas besoin ; voir Construire des drapeaux pour la liste complète.
Configuration#
[render]
workers = 4 # Dedicated V8 worker threads (default: 4)
bundle_path = "dist/ssr-bundle.js"
Il n'y a pas de champ engine — V8 est la seule option. Les configurations existantes qui définissent engine = "v8" se chargent toujours proprement (le champ inconnu est ignoré).
Sous bext run, les travailleurs de rendu forment un pool multiplexé par défaut ( BEXT_V8_POOL=1 ) : les requêtes sont démultiplexées par identifiant entre les travailleurs, de sorte qu'un travailleur ne reste jamais inactif lors de l'aller-retour IPC entre les rendus, et le corps de la réponse revient sur la mémoire partagée ( BEXT_V8_SHM_IPC=1 ). Un travailleur sature déjà le pipeline de requêtes pour les routes liées aux E/S/au maître ; augmentez workers (ou BEXT_V8_POOL_SIZE ) pour les SSR gourmands en CPU, où le pool évolue de manière presque linéaire. Voir Variables d'environnement pour les indicateurs individuels.
Pipeline de rendu à trois couches#
Le rendu à la demande V8 utilise une architecture en couches pour des performances maximales :
Layer 1: React Base (747 KB, built once per process)
→ React + ReactDOM + jsx-runtime as globalThis globals
→ Disk-cached at .bext/v8-react-base.js (survives restarts)
Layer 2: Site Shell (31 KB for simple sites, cached per-site)
→ Root layout + Nav + Footer + shared UI components
→ Disk-cached at .bext/ssr-cache/__shell.js
→ Rebuilt when layout files change
Layer 3: Page Delta (27-51 KB per route, cached per-route)
→ Just the page component (React externalized)
→ Disk-cached at .bext/ssr-cache/{route}.js
→ Rebuilt when page source changes
Performance#
| Métrique | Sans superposition | Avec superposition |
|---|---|---|
| Taille du paquet de routage | ~800 Ko | 27-51 Ko (96 % plus petit) |
| Temps de compilation de Bun | 30 ms | 9-14 ms |
| Évaluation V8 (premier contexte) | 100 ms | 32ms |
| Évaluation V8 (ultérieure) | 100 ms | 7ms |
| Appel du cache de réponse | 6 ms | 6ms |
Hiérarchie du cache#
Les bundles sont mis en cache à trois niveaux :
- Mémoire — DashMap, recherche instantanée, effacée lors du changement de source
- Disque —
.bext/ssr-cache/, survit aux redémarrages, validé par le fichier mtime - Compile — Sous-processus Bun, s'exécute uniquement en cas d'échec du cache complet
Après un redémarrage avec un cache disque chaud, les routes se chargent sans aucune compilation Bun.
Configuration#
Le pipeline à la demande est configurable via bext.config.toml :
[nextjs]
on_demand = true
cache_ttl = 60 # Response cache TTL in seconds (default: 60)
cache_swr = 300 # Stale-while-revalidate window in seconds (default: 300)
max_shell_size = 524288 # Max shell bundle size in bytes (default: 512KB)
[render]
streaming = true # Use renderToReadableStream + bext event loop
# streaming_timeout_ms = 5000 # Wall-clock cap for one render
# streaming_stall_ms = 250 # Bail out if no chunk for this long
# rsc = true # Expose /__bext/rsc/<path> Flight endpoint
Les sites avec des shells plus grands que max_shell_size (par exemple, les applications full-stack avec Prisma/tRPC) reviennent automatiquement à la compilation complète.
Si vous n'avez besoin que de pages entièrement synchrones, configurez streaming = false pour ignorer complètement le pilote de boucle d'événements et obtenir une latence légèrement inférieure. Les composants du serveur asynchrone et use(promise) cesseront de fonctionner, mais les pages de rendu pur ne seront pas affectées.
RSS en streaming#
Lorsque streaming = true, bext pilote le renderToReadableStream de React 19 à partir de react-dom/server directement à l'intérieur de l'isolat V8. La V8 n'a pas de boucle d'événements intégrée, donc bext en implémente une dans Rust :
collect_async_render_resultpompeperform_microtask_checkpoint()jusqu'à ce que React rapportedone- La progression est lue via l'accès brut aux objets V8, donc chaque tick ne coûte presque rien
- Un flux bloqué (pas de nouveau morceau pour
streaming_stall_ms) renfloue prématurément - Un plafond d'horloge murale (
streaming_timeout_ms, 5 000 ms par défaut) délimite le pire des cas
Le mode streaming se déverrouille :
- Composants du serveur
async function Page() - Limites
<Suspense>avec enfants asynchrones - Le crochet
use(promise) await fetch(...)et autres travaux asynchrones réservés au serveur
Lorsque streaming = false, bext supprime globalThis.__ReactDOMServer.renderToReadableStream du contexte de la page afin que le wrapper de bundle passe au renderToString synchrone de React. Ce chemin ne peut pas attendre, mais il ignore complètement la boucle d'événements, ce qui est utile pour les pages entièrement synchronisées.
Composants du serveur React (expérimental)#
Définir [render] rsc = true et bext expose /__bext/rsc/<path> pour tout itinéraire correspondant. Le point de terminaison renvoie une véritable charge utile de vol React via react-server-dom-parcel/server.edge.
Le pipeline RSC utilise un bundle distinct et un contexte de page V8 distinct du pipeline HTML SSR :
- Le travailleur bun warm compile une entrée RSC par route avec
--conditions=react-serverafin que React soit résolu en sa version serveur uniquement. - Le travailleur regroupe ce React en ligne sur serveur uniquement (en ignorant la règle externe habituelle react). L'externaliser laisserait dans la sortie des références
import.meta.require("react")que le mode de script classique de V8 ne peut pas analyser, et forcerait le moteur d'exécution à utiliser le React normal du bundle SSR, qui est incompatible avec les chemins de code réservés au serveur dereact-server-dom-parcel. - Le bundle est évalué dans un nouveau contexte V8, mis en cache séparément du contexte SSR (la clé de cache du contexte de page a l'espace de noms
page:rsc:...vspage:ssr:...) renderToReadableStreamdereact-server-dom-parcel/server.edgeproduit la charge utile Flight, quecollect_async_rsc_resultdraine via le même pilote de boucle d'événements que le streaming SSR.
La charge utile de vol d'une petite route atterrit en ~ 10 ms à chaud, ~ 50 ms à froid (compilation + évaluation combinées). Le point de terminaison est actuellement uniquement localhost et additif : il ne remplace pas encore la réponse HTML SSR.
En-têtes de réponse#
Les pages rendues en V8 incluent :
| En-tête | Valeur |
|---|---|
x-bext-mode |
on-demand |
x-bext-cache |
hit, stale ou miss |
cache-control |
public, max-age={ttl}, stale-while-revalidate={swr} |
etag |
Hachage FNV du corps de réponse |
x-content-type-options |
nosniff |
x-frame-options |
SAMEORIGIN |
referrer-policy |
strict-origin-when-cross-origin |
link |
Conseils de préchargement extraits de la page HTML |
content-encoding |
gzip (lorsque le client accepte) |
vary |
Accept-Encoding |
ETag permet les réponses 304 Not Modified — aucun transfert lors de visites répétées.
Architecture#
Fil d'évaluation dédié#
L'évaluation V8 s'exécute sur un seul thread de système d'exploitation dédié avec une pile de 32 Mo :
- Isolat unique à longue durée de vie — jamais abandonné (évite rusty_v8 v130 SEGV)
- Nouveau contexte par requête — chaque rendu obtient une portée propre
- Répartition basée sur les canaux — les appelants envoient leur travail via
mpsc::channel - Délai d'expiration de 30 secondes — empêche les rendus suspendus
- GC périodique —
low_memory_notification()tous les 50 rendus
Temps mis en cache#
Le répertoire source mtime est mis en cache avec une durée de vie d'une seconde pour éviter les déplacements du système de fichiers à chaque requête (plus de 53 fichiers pour un site typique).
Fonctions du pont#
La V8 expose les fonctions natives du pont IO au code groupé :
| Fonction | But |
|---|---|
__readFile(path) |
Lire des fichiers à partir du disque |
__readDir(path) |
Lister le contenu du répertoire |
__httpFetch(url, opts) |
Requêtes HTTP via ureq |
__dbQuery(db, sql, params) |
Requêtes SQLite |
__env(key) |
Accès aux variables d'environnement |
__log(level, msg) |
Journalisation structurée |
Polyremplissages#
Les isolats nus V8 n'ont pas d'API Node.js /Bun. bext fournit des polyfills pour :
process.env,process.versions.node,process.cwd()console.log/warn/error/infoTextEncoder/TextDecodersetTimeout/clearTimeoutqueueMicrotaskwindow,document,navigator,location(tronçons SSR minimaux)
Dépannage#
<head> vide dans le rendu HTML#
Le renderToString de React 19 peut supprimer les enfants <head> dans la V8. bext contourne ce problème en extrayant le contenu principal du côté serveur layout.tsx et en l'injectant dans le code HTML si <head></head> est vide.
Coquille trop grande#
Si vous voyez « Shell trop grand, utilisant le bundle complet » dans les journaux, la chaîne de mise en page du site comprend de lourdes dépendances (Prisma, tRPC, etc.). Le shell dépasse max_shell_size et revient à une compilation complète. Ceci est attendu pour les applications full-stack.
Pour augmenter le seuil :
[nextjs]
max_shell_size = 1048576 # 1MB
Utilisation de la mémoire#
Surveillez la mémoire V8 et les performances de rendu :
bext-server diagnose --pass <admin-password>
bext-server diagnose --pass <admin-password> --format json
Vérification du timing d'évaluation#
Répartition de la synchronisation des journaux V8 par requête :
V8 eval timing: polyfills_ms=0, base_shell_ms=1, bundle_ms=0, render_ms=5, total_ms=7
base_shell_ms: Temps d'évaluation de la base React + shell du site (1 ms après le premier contexte, 21 ms la première fois)bundle_ms: délai d'évaluation du delta de page (0 ms pour les lots de 27 à 51 Ko)render_ms: Temps de rendu React vers chaîne (5-10 ms)
En rapport#
- Indicateurs de construction et de fonctionnalités — activation de la fonctionnalité
v8au moment de la compilation - Architecture : pool de rendu V8 — topologie du pool, disjoncteur et indicateurs de résilience
- Architecture : Pipeline de rendu PRISM — comment la V8 s'intègre dans le chemin de requête complet
- Variables d'environnement —
BEXT_RENDER_WORKERS, drapeaux de piscineBEXT_V8_* - bext.config.toml — Référence des sections
[render]et[nextjs]