ENbext.dev
DocumentationArchitecture

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Pipeline de rendu PRISM

Cette page retrace une seule requête de page PRISM tout au long de l'espace de travail Rust : comment l'itinéraire est mis en correspondance, comment le .tsx correspondant est transformé en un bundle JS évaluable V8 (via le interface tsc-rs et le Passe de compilation PRISM ), comment ce bundle est évalué dans le Pool de rendu V8 et comment le HTML résultant est assemblé et mis en cache.

Il s'agit de l'histoire de l'intégration de trois sous-systèmes qui ont leurs propres pages de référence : lisez ceci d'abord pour voir comment ils s'emboîtent, puis suivez les liens croisés pour plus de profondeur.

Le chemin en un coup d'œil#

code
HTTP request (GET /pricing)
   │
   ▼
[1] Dispatch            bext-server ssr_pipeline::pipeline::handle
   │                    → on_demand::handle_bext_app  (framework = prism)
   ▼
[2] Route match         prism::handle_prism_request → handle_prism_request_inner
   │                    ROUTE_TABLE_CACHE  +  bext_turbopack::prism::discover_routes
   ▼
[3] Compile (cache miss)
   │   ┌─ pre_transform_source ──────────────────────────────────────┐
   │   │  PRISM compile-fold pass  bext_core::transform::prism_compile │  ← tsc-rs AST
   │   │     (static JSX → string concat)                             │
   │   └──────────────────────────────────────────────────────────────┘
   │   tsc-rs --pipe worker  (TscRsPipeWorker.transform_full)          ← tsc-rs emit
   │   transitive-import BFS  +  emit_route_bundle  (topo-sorted IIFEs)
   ▼
[4] Evaluate            V8 render pool (SubprocessPool / PrismPool)
   │                    install_prism_context → __bextPrismRender(props)
   ▼
[5] Collect             collect_async_prism_result  (drive microtasks/timers)
   │                    RouteResult { kind: "html" | "response" }
   ▼
[6] Assemble + cache    HTML transforms → ISR / L0 / compressed-body cache
   │
   ▼
HTTP response

Chaque étape ci-dessous nomme la caisse/fichier et les symboles clés afin que vous puissiez accéder à la source. Les chemins sont relatifs à crates/ dans le espace de travail.

1. Répartition : s'agit-il d'un site PRISM ?#

PRISM est un et non un détecté automatiquement. Un site s'inscrit explicitement avec [framework] type = "prism" dans bext.config.toml, analysé en FrameworkConfig::framework_type ( bext-server/src/config.rs ). Il existe deux points d'entrée de répartition en fonction du mode de déploiement ( modes de déploiement ) :

Mode Entrée Remarques
Autonome ( bext run ) bext-server/src/handler.rs Vérifie framework_type == FrameworkType::Prism, construit un PrismCacheCtx soutenu par le L1/L2 IsrCache, appelle ssr_pipeline::prism::handle_prism_request.
Mascarade ( nginx -compat vhost) ssr_pipeline/on_demand.rs::handle_bext_app Lorsque [build] engine = "turbopack", la passerelle transmet à super::prism::handle_prism_request. C'est ainsi que sont desservis docs.bext.dev et les autres sites en cours.

Les deux convergent vers le pipeline partagé :

code
pipeline::handle()                 ssr_pipeline/pipeline.rs
  → gate_static_public()           static / public asset fast-path
  → on_demand::handle_bext_app()   framework dispatch
    → prism::handle_prism_request()        public, single-flight wrapper
      → prism::handle_prism_request_inner()  the real work

handle_prism_request enveloppe l'appel interne dans fusionnant un seul vol ( crate::single_flight ) pour les SSR GET sans cookie, de sorte qu'un troupeau tonitruant sur une route froide s'effondre en un seul rendu. Basculer avec BEXT_SINGLEFLIGHT=0.

Un détail à connaître : FrameworkType::Prism correspond en interne à DetectedFramework::Fetch pour le scanner existant — PRISM est intercepté avant, le scanner s'exécute, donc un outil qui lit DetectedFramework voit Fetch, et non Prism.

2. Correspondance d'itinéraire#

Dans handle_prism_request_inner, les chemins spéciaux sont d'abord mis en correspondance ( /islands/<name>.js, /_bext/action/<name>, /_bext/image, fichiers de métadonnées, /api/* ), puis intergiciel est consulté, puis la table de routage des pages :

  • ROUTE_TABLE_CACHE — un Mutex<HashMap<…>> global de processus dans prism.rs, saisi par (app_root, invisible_segments) et contenant la liste de routes par application (un Vec de RouteInfo ). En cas d'échec, il appelle bext_turbopack::prism::discover_routes, qui parcourt src/app/ pour page.tsx / page.jsx.
  • RouteInfo ( bext-turbopack/src/prism.rs ) porte url_path, page_path, is_client, is_dynamic, is_catch_all.
  • match_route effectue un scan linéaire : itinéraires statiques avant itinéraires dynamiques, fourre-tout ( [...slug] ) en dernier.
  • route_signals_cache mémorise l'analyse par itinéraire ( has_loader, has_action, revalidate, éligibilité au streaming) utilisé pour brancher l'ISR et le comportement de streaming. Voir Données PRISM.

Le cache est invalidé par clear_route_table_cache_for_app(), appelé depuis l'observateur de fichiers bundler.rs lorsqu'un fichier sous src/app/ est modifié. Voir rechargement en direct et invalidation et redémarrages.

Conseil

Si vous devez déboguer ce que le bundler émet, définissez BEXT_DUMP_PRISM_BUNDLE=1 et redémarrez bext. Le bundle CommonJS complet pour la route correspondante est écrit dans stderr afin que vous puissiez inspecter la disposition IIFE et toutes les concaténations de chaînes repliées.

3. Compiler — Ensemble .tsx → V8#

En cas d'échec du cache, la route correspondante est compilée en un seul bundle CommonJS. L'orchestration réside dans bext-turbopack/src/direct.rs ; l'entrée spécifique à PRISM est compile_route_server dans bext-turbopack/src/prism.rs, qui définit jsx_import_source = Some("@bext-stack/framework") et appels compile_entry_to_js.

3a. Cache de compilation à quatre couches#

compile_entry_to_js vérifie quatre caches avant d'effectuer tout travail :

  1. PATH_MEMO — clé de chemin, basée sur TTL (par défaut 30 s, BEXT_TURBOPACK_PATH_MEMO_TTL_MS). Ignore même la statistique mtime.
  2. Cache de compilation de hachage de contenu — en cours, saisi sur un hachage u64 de l'entrée plus les mtimes de toutes les importations transitives. Survit à l’expiration du TTL.
  3. Cache disque persistant ( compile_disk_cache ) — clé de hachage de contenu bundles sous /var/cache/bext/compile, avec vérification transitive-mtime du temps de lecture (auto-correction ; un service modifié rejette l'entrée). Il s'agit du niveau auquel survit à un redémarrage/redéploiement : une route inchangée est servie à partir du disque en 80 µs au lieu de réexécuter tsc-rs (8 ms) — éliminant ainsi la tempête de recompilation après le déploiement. Se désinscrire avec BEXT_TURBOPACK_DISK_CACHE=0.
  4. Porte à vol unique — COMPILE_KEY_LOCKS, un hachage par contenu Arc<Mutex<()>> donc N requêtes simultanées pour le même module invalidé ne sont pas chacune compilées à froid. Coupe-circuit : BEXT_TURBOPACK_COMPILE_SINGLEFLIGHT=0.

Ce n'est plus un piège (2026-06-12) : les caches en cours (1, 2) sont toujours mourir avec le processus, mais le niveau de disque (3) signifie qu'un redémarrage n'est plus compile à froid les itinéraires inchangés. Les modifications de la source sont récupérées par le observateur de rechargement en direct (mode travailleur) ou observateur d'itinéraire de mascarade (mode en cours) dans un délai d'environ 2 s — l'entrée sur le disque s'auto-invalide sur le l'heure mtime du fichier a été modifiée, vous n'avez donc plus besoin de rm -rf .bext + redémarrage. Voir invalidation et redémarrages.

3b. Le pli de pré-transformation (avant tsc-rs)#

pre_transform_source exécute avant, l'interface TypeScript voit le fichier. Lorsque jsx_import_source == "@bext-stack/framework" et que le fichier est .tsx / .jsx :

  • Les fichiers "use signals" sont acheminés vers bext_core::transform::prism_signals::optimize (voir Signaux ).
  • sinon le Passe de compilation PRISM ( bext_core::transform::prism_compile::optimize ) plie les sous-arbres JSX statiques en concaténations de chaînes directement dans le texte source, à l'aide de l'analyseur/AST tsc-rs (types de nœuds tsc_rs_parser::parse + tsc_rs_ast). Il effectue des épissures de plages d'octets sur la source d'origine - c'est et non qui émet JS lui-même. Désactivez-vous avec BEXT_PRISM_COMPILE=0.

La définition de jsx_import_source force également le mode JSX tsc-rs à react-jsx (exécution automatique), de sorte que l'émetteur produit require("@bext-stack/framework/jsx-runtime") appelle tout JSX laissé par la passe de pliage. Voir le Exécution JSX.

3c. La transformation tsc-rs (le véritable TS→JS)#

La source repliée est transmise à un sous-processus tsc-rs --pipe persistant — TscRsPipeWorker dans direct.rs :

  • transform_full envoie { filename, source, options } sous forme d'un seul JSON ligne et relit { output, imports, dynamic_imports, exports, source_map }. C’est là que TSX devient CommonJS JS.
  • PipeOptions porte target: es2022, module: commonjs, jsx, jsx_import_source.
  • Le travailleur suit le mtime binaire sur le disque et réapparaît automatiquement lorsque vous reconstruisez tsc-rs, afin que les modifications du frontend prennent effet sans redémarrer bext.
  • Un pool de ces travailleurs est dimensionné par BEXT_TURBOPACK_PIPE_POOL_SIZE (par défaut 1) afin que des modules distincts puissent se transformer en parallèle.

La division complète du travail – qui analyse, qui émet – se trouve sur la page Frontal TypeScript.

3d. Fermeture transitive + émission de bundle#

compile_closure effectue un BFS sur les importations de l'entrée, transformant chaque module une fois (résultats mémorisés dans le fichier à clé Blake3). shared_registry() / ModuleRegistry ), puis emit_route_bundle assemble le tout dans un seul fichier :

  • écrit les shim IIFE et les solutions de secours globalThis.__Layout{0,1,2},
  • émet chaque module comme IIFE avec //# sourceURL=<abs>,
  • exécute un topological_sort afin que les modules soient évalués dans l'ordre des dépendances (avec objets __modules[abs] vides pré-enregistrés pour la sécurité des cycles).

BEXT_PRISM_COMPILE_OFFTHREAD=1 déplace toute cette étape vers spawn_blocking afin qu'une compilation à froid ne bloque pas le thread de travail actix (une atténuation des tempêtes de recompilation — voir le Post-mortem du pool de rendu V8 ). Videz le paquet émis pour inspection avec BEXT_DUMP_PRISM_BUNDLE=1.

4. Évaluer dans V8#

Le bundle compilé (une chaîne partagée Arc ) est évalué par Pool de rendu V8. Il existe deux backends ; la décision d'expédition dans handle_prism_request_inner est à peu près use_pool = !subprocess_mode_configured() && backend().is_none():

Back-end Caisse / fichier Quand
SubprocessPool bext-v8/src/subprocess.rs Production. BEXT_V8_POOL=1. N travailleurs hors processus, chacun isolé par V8, restituent les images envoyées sur des sockets Unix encadrés.
En cours de processus PrismPool bext-v8/src/prism_pool.rs Threads du système d’exploitation default_pool(), BEXT_PRISM_WORKERS (par défaut 2), chacun étant un isolat de longue durée.
Filetage d'évaluation unique bext-v8/src/eval.rs Retomber; un thread bext-v8-eval-worker sérialise tous les rendus.

Quel que soit le backend, un contexte de rendu est défini par install_prism_context ( eval.rs ) : un nouveau contexte V8 avec les fonctions de pont enregistrées ( bridge::register_bridge_functions — __httpFetch, __bextReadChunk, __env, __readFile, …)), les polyfills évalués et le bundle JS évalué. Les contextes chauds sont mis en cache dans prism_contexts (saisi par prism_cache_key(shell, bundle) ), de sorte que les rendus suivants sur le même isolat ignorent la réévaluation du bundle.

Construire un contexte pour la première fois évalue le bundle JS. Cette évaluation est soutenue par un cache de bytecode V8 persistant ( bytecode_disk_cache, /var/cache/bext/bytecode, saisi par le hachage du bundle + la balise de version V8) : sur une construction de contexte froid, le travailleur restaure le bytecode compilé via ConsumeCodeCache au lieu de ré-analyser + compiler le JS, donc une construction de contexte après redémarrage est également plus rapide. Se désinscrire avec BEXT_V8_BYTECODE_DISK_CACHE=0. Avec le disque compilez le cache (§3a), un redémarrage ignore à la fois, la compilation tsc-rs et l'analyse V8.

5. Rendu → RouteResult#

Le rendu est piloté en appelant globalThis.__bextPrismRender(propsJson) :

  • tamponné ( render_prism_in_context_buffered ) — collect_async_prism_result interroge globalThis.__bextPrismResult.done en boucle, pompant des microtâches/minuteries ( pump_timers_and_microtasks ) jusqu'à ce que le rendu se stabilise, puis renvoie le résultat.
  • Diffusion ( render_prism_in_context_streaming, inscription facultative BEXT_PRISM_STREAMING=1 ) — collect_async_prism_result_progressive pilote la boucle d'événements V8 et pousse chaque nouveau morceau sur un canal Tokio au fur et à mesure qu'il apparaît, de sorte que la sortie renderToReadableStream soit progressivement envoyée au fil. Les itinéraires avec un loader / action (qui peuvent rediriger ou définir un statut) reviennent à la mise en mémoire tampon.

Le rendu renvoie un RouteResult sur un cadrage IPC binaire ( \x01v1\n{envelope}\n{body} ) qui évite la surcharge d'échappement JSON.stringify :

  • kind: "html" — un corps de page normal.
  • kind: "response" — le chargeur/l'action a renvoyé un Response (redirection, notFound(), statut personnalisé). Extraits de rouille status, headers et body et construit directement le SiteResponse.

6. Assembler, transformer, mettre en cache#

Le HTML est finalisé via le temps de réponse pipeline de transformation (remplacement de l'îlot de serveur, extraction des indices de préchargement pour HTTP 103 Early Hints) et l'étape de compression du cycle de vie de la demande, puis mis en cache sur plusieurs couches :

Couche Où But
L0 bext-server/src/l0_cache.rs Cache en cours de processus haut de gamme à la limite d'actix. Seuls les nouveaux hits ISR sont stockés (hôte + chemin + clé de requête).
ISR (L1/L2) bext-core/src/cache/isr.rs (IsrCache) Le cache mode = "isr" avec TTL revalidate, invalidation de balise et bouclage périmé pendant la revalidation. L2 est soutenu par Redis pour le multi-instance. Voir mise en cache.
Cache latéral à corps comprimé DashMapPrismStore dans prism.rs Les corps pré-compressés (brotli/gzip) afin que les hits chauds évitent la recompression. Budgétisé par BEXT_COMPRESSED_BODIES_MAX_BYTES (par défaut 512 Mio).
Table de routage / cache de compilation §2, §3a Caches structurels gérés par hachage d'application et de contenu.

Le site docs.bext.dev que vous lisez exécute mode = "isr" avec revalidate = 300, donc la plupart des rendus de fragments sont des accès au cache.

7. Le chemin de l'erreur#

Lorsqu'un rendu échoue, prism_error_response ( prism.rs ) renvoie un 500 avec l'erreur apparue dans les en-têtes de la superposition de développement :

  • x-bext-error-kind, x-bext-error-message, x-bext-error-route, chacun est nettoyé en ASCII visible par to_ascii_header (les tirets, les guillemets bouclés et les ellipses sont pliés ; tout ce qui se trouve en dehors de 0x20..=0x7E devient ? ). Ceci est important car une valeur d'en-tête non-ASCII rend HeaderValue::TryFrom échoue silencieusement et l'en-tête disparaît.
  • le corps est une page d'erreur stylisée de render_prism_error_page avec des indices heuristiques sur la « cause probable ».

Gotcha : compile les échecs de prendre une branche différente qui rend la page d'erreur directement sans réglage x-bext-error-message, donc la superposition de développement revient à lire le (éventuellement compressé) corps. Seuls les échecs de rendu ** définissent l'en-tête. Voir codes d'erreur et dépannage.

Sous contre-pression de piscine, la réponse est plutôt un 503 avec Retry-After: 1 et x-bext-pool-shed: queue-full — voir hangar précoce.

Où aller ensuite#

Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.