ENbext.dev
DocumentationPRISM (natif bext)

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

Sur cette page

Passe de compilation PRISM

Une passe de style SWC au moment de la construction qui plie les sous-arbres JSX statiques/pilotés par des accessoires en concaténations de chaînes directes avant, le bundler émet des appels d'exécution jsx(). Équivalent dans l'esprit aux modèles compilés de Marko, mais fonctionnant sur le TSX existant sans aucune modification de création.

par défaut pour tout site avec [framework] type = "prism". Désactivez-vous par build avec BEXT_PRISM_COMPILE=0. Équivalent en octets vérifié à l'exécution à chaque couche (réécriture source → bundle JS → rendu V8 → HTTP TTFB).

Ce qu'il plie (six niveaux)#

La passe est un additif  : tout ce qui ne peut pas prouver statiquement sa sécurité passe par le chemin d'exécution h() existant. Chaque niveau s’ajoute au précédent ; vous obtenez le syndicat par défaut.

Niveau 1 : sous-arbres entièrement statiques#

La balise d'élément est une balise hôte, chaque attribut est une chaîne littérale, chaque enfant est du texte ou un autre élément JSX pliable.

tsx
// source
<head>
  <meta charSet="utf-8" />
  <title>my-page</title>
</head>

// emitted (literal body, branded as rendered HTML)
__bextSafe("<head><meta charset=\"utf-8\"><title>my-page</title></head>")

Niveau 2 : forme statique avec enfants d'expression JSX dynamiques#

Élément statique + attributs statiques + au moins une position enfant {value}. La valeur dynamique traverse la même limite renderChild que le moteur d'exécution : les chaînes simples sont échappées et les sous-arbres SafeHtml la traversent.

tsx
// source
<h1>Hello, {name}!</h1>

// emitted
__bextSafe("<h1>Hello, " + __bextChild(name) + "!</h1>")

Niveau 3a — valeurs d'attributs dynamiques#

Y compris le nom de classe conditionnel canonique :

tsx
// source
<button className={active ? "on" : "off"}>{label}</button>

// emitted
__bextSafe('<button class="' + __bextEsc(active ? "on" : "off") + '">' + __bextChild(label) + "</button>")

__bextSafe, __bextEsc et __bextChild sont importés une fois en haut du fichier depuis @bext-stack/framework/jsx-runtime — ce sont les mêmes implémentations de safe, escapeHtml et renderChild utilisées par le runtime, donc les attributs et les valeurs enfants sont gérés de manière identique en octets avec ou sans passe.

Niveau 4 — Déroulement du array.map()#

tsx
// source
<ul>{items.map(i => <li>{i}</li>)}</ul>

// emitted shape (formatted for readability)
__bextSafe((() => {
  let __bextOut = "<ul>";
  for (const i of items) {
    __bextOut += "<li>";
    __bextOut += __bextChild(i);
    __bextOut += "</li>";
  }
  __bextOut += "</ul>";
  return __bextOut;
})())

L'accumulateur unique évite le tableau intermédiaire et le .join("") utilisés par l'ancienne forme de sortie. Le niveau 4+ gère également les flèches (item, idx) => multi-arguments et les flèches i => { return <li/>; } à corps de bloc.

Niveau 5 – intégration de composants sans hélice#

tsx
// source
function Brand() { return <span class="b">bext</span>; }
function Header() { return <header><Brand/></header>; }
function Page() { return <main><Header/></main>; }

// Page's emitted body
return __bextSafe("<main><header><span class=\"b\">bext</span></header></main>");

Récursif : <Page><Header><Brand/></Header></Page> → un concat. Capture les modèles canoniques de shell de mise en page <Header/>, <Footer/>, <Logo/>.

Niveau 5.5 – Inlining des composants d'hélices#

Les composants avec un seul paramètre d'objet déstructuré se replient lorsque le site d'appel fournit tous les accessoires requis :

tsx
// source
function Card({ title, body }: { title: string; body: string }) {
  return <div class="card"><h2>{title}</h2><p>{body}</p></div>;
}
function Page({ user }: { user: { name: string; bio: string } }) {
  return <Card title={user.name} body={user.bio} />;
}

// Page's emitted body
return __bextSafe("<div class=\"card\"><h2>" + __bextChild(user.name) + "</h2><p>" + __bextChild(user.bio) + "</p></div>");

La substitution concerne uniquement l'identifiant : {title} dans le corps est remplacé par la valeur du site d'appel. Les accessoires littéraux de chaîne sont remplacés par des littéraux et reçoivent un échappement normal au moment de la compilation dans leur position finale ; les enfants dynamiques passent par __bextChild, tandis que les attributs dynamiques deviennent une étendue dynamique pointant vers la source du site d'appel et utilisent __bextEsc.

Ce qu'il ne plie PAS (bails intentionnels)#

Modèle Pourquoi
<div {...rest}> Forme de propagation inconnue au moment de la construction
function Card(props) (paramètre d'identification) L’accès aux membres props.X nécessite une analyse de portée ; seul le { a, b } déstructuré est pris en charge
<Card>{slot}</Card> (composant avec enfants) Le filetage des emplacements nécessite une substitution adaptée à la portée - différé
<main>{props.children}</main> props.children peut être AsyncIterable<string> ; la contrainte de chaîne rendrait [object AsyncGenerator]
style={{...}}, dangerouslySetInnerHTML={{...}} Sémantique des attributs de forme d'objet - le runtime les applique, le pli ne peut pas
i => { console.log(i); return <li/>; } Bloquer les corps avec caution pour effets secondaires ; seuls les blocs à retour unique se plient
async function Page(), function* Page() Composants asynchrones/générateurs gérés par le runtime de streaming, ne se plient pas

La règle props.children était autrefois plus stricte (toute référence à props provoquait la libération de l'ensemble du sous-arbre). Il s'agit désormais de children spécifique à : props.title, props.amount, props.user.name etc. se plient tous.

Résultats empiriques#

Niveau source ( bext-core::transform::prism_compile )#

La source actuelle contient 52 cas #[test] couvrant la réécriture sur des luminaires synthétiques + le sites/prism-demo/src/app/page.tsx réel. Sur une page représentative (11 appels jsx() dans la sortie dépliée) :

Niveau 1 uniquement Niveau 1+2 Niveau 1+2+3a+4+5+5,5
8 sur 11 éliminés (73%) 11 sur 11 (100%) 11 sur 11

Au niveau du bundle ( bext-turbopack::prism::tests )#

Le pass est intégré dans le pipeline de compilation de bext - turbopack à deux endroits : compile_closure (source d'entrée) et transform_with_analysis_opts (importations transitives). Les tests E2E compilent le même appareil avec/sans le pass, comptent les appels jsx(), vérifient que l'importation d'assistance est émise correctement.

Équivalence d'octets vérifiée à l'exécution#

html_byte_equivalence_with_and_without_pass compile un appareil deux fois (env-on / env-off), évalue les deux bundles en V8 via bext_v8::eval::render_prism et affirme que le code HTML rendu est identique en octets. Actuellement, 237 octets correspondent aux six niveaux.

Microbanc SSR actuel#

Le luminaire canonique est désormais le catalogue Tailwind de 38,3 Ko et 100 lignes dans harnesses/jsx-shootout. Il rejette les différences d'octets avant le timing et échappe à chaque valeur de texte dynamique dans la sortie compilée et dans le plafond écrit à la main. Sur Bun 1.3.13, épinglé sur un noyau :

Chemin moyenne p50 Débit
Plafond JS sécurisé et écrit à la main 15,8 µs 14,3 µs 63 000 rendus/s
Passe de compilation 15,7µs 14,0 µs 64K rendus/s
Interprété, direct h() 69,3 µs 62,1 µs 14,4 000 rendus/s
Exécution JSX automatique 75,3 µs 67,9 µs 13,3K rendus/s

La passe de compilation est 4,4 fois plus rapide que le h() interprété directement et 4,8 fois plus rapide que le chemin JSX automatique réel. Il correspond désormais au plafond écrit à la main dans le bruit de mesure, de sorte que les changements de codegen inférieurs à la microseconde doivent être évalués avec des analyses par lots alternées plutôt qu'avec un seul échantillon de minuterie.

Historique HTTP TTFB sur sites/status (mesure séparée)#

Cet ancien résultat de site de statut n'a pas été réexécuté dans le cadre de la fusillade actuelle de 38,3 Ko et ne doit pas être mélangé avec le tableau de rendu uniquement ci-dessus. Il a utilisé un équivalent ApacheBench sur le répartiteur bext du serveur PRISM desservant la page d'état.dev bext :

Mode moyenne p95
BEXT_PRISM_COMPILE=0 17,59 ms 23,69 ms
par défaut 6,50 ms 8,39 ms
Accélération 2,71 × 2,82 ×

Dans cette exécution, la surcharge HTTP était d'environ 4 à 5 ms et constante entre les modes ; le delta SSR dominait le TTFB mesuré.

Règle d'activation#

code
options.jsx_import_source == Some("@bext-stack/framework")
  && filename ends with .tsx | .jsx
  && BEXT_PRISM_COMPILE != "0" | "false" | "off"

Donc aucune configuration requise — chaque site avec [framework] type = "prism" le récupère automatiquement lors de la prochaine compilation. Il s'agit du chemin de production normal pour les sites PRISM, et non d'une optimisation de démonstration uniquement.

Ajouter le pass sur un nouveau site : rien d'obligatoire au-delà [framework] type = "prism" dans bext.config.toml et jsxImportSource = "@bext-stack/framework" dans tsconfig.json.

Vérification sur votre machine#

bash
# Source-level unit tests (fast, ~1s after warm build)
cd ~/bext && rustup run nightly-2026-04-02 cargo test \
  -p bext-core --lib transform::prism_compile

# End-to-end compile-diff + V8 byte-equivalence (~2 min cold rebuild)
cd ~/bext && rustup run nightly-2026-04-02 cargo test \
  -p bext-turbopack --lib prism::tests

# Release-mode SSR perf microbench
cd ~/bext && rustup run nightly-2026-04-02 cargo test --release \
  -p bext-turbopack --lib prism::tests::bench_render_speedup \
  -- --ignored --nocapture

# Eyeball what the rewrite does to sites/prism-demo/src/app/page.tsx
cd ~/bext && rustup run nightly-2026-04-02 cargo test \
  -p bext-core --lib transform::prism_compile::tests::dump_rewrite \
  -- --nocapture --ignored

Comment il se compare aux autres moteurs#

Même appareil de liste de produits de 38,3 Ko validé par octets (Bun 1.3.13, noyau épinglé) :

Moteur moyenne p50
Constructeur de chaînes Rust 3,9 µs 3,9 µs
Plafond JS sécurisé et écrit à la main 15,8 µs 14,3 µs
bext - Passe de compilation PRISM ¹ 15,7µs 14,0 µs
Marc 5 38 µs 22 µs
PRISM interprété, direct h()² 69,3 µs 62,1 µs
PRISM JSX automatique 75,3 µs 67,9 µs
Solide 1.9 ( generate: "ssr" ) 78 µs 61 µs
React 19 1 823 µs 1 646 µs

bext - PRISM - les rendus compilés bien en avance sur Marko 5 et Solid 1.9 sur le même appareil. Marko et Solid sont eux-mêmes des moteurs de création de modèles au moment de la compilation (Marko émet des out.push(…) ; Le babel-preset-solid de Solid avec generate: "ssr" émet _$ssr(template, ...args) ) — la passe de compilation PRISM bat les deux car elle émet un seul IIFE à accumulateur de croissance ( (() => { let __bextOut = ""; for (...) __bextOut += "..."; return __bextOut; })() ) avec des boucles for sur les modèles .map / Array.from au lieu du tableau intermédiaire Forme .map().join("") Solide émet.

¹ La passe de compilation est passée de la forme d'expression .map().join("") à une boucle for IIFE + impérative et regroupe désormais les segments de sortie adjacents. La sortie actuelle compilée correspond au plafond JavaScript écrit à la main, équivalent à la sécurité, dans le bruit de mesure. La ligne Rust de 3,9 µs est une limite inférieure native et n'est pas la cible d'équivalence.

² Le runtime interprété utilise un renflouement d'expression régulière dans escapeHtml, des chemins rapides primitifs dans h(), une réutilisation directe des accessoires intrinsèques créés par le compilateur, une normalisation des composants gardés et un cache limité à deux entrées de résultats d'échappement de classe exacts. Les enfants à cordes simples restent échappés ; les sous-arbres rendus transitent via la marque SafeHtml.

Le faisceau de banc complet se trouve sur harnesses/jsx-shootout/ dans le référentiel bext – exécute les cinq moteurs sur trois appareils DOM identiques avec une seule commande. Consultez son COMPILE-PASS.md pour la discussion sur la conception qui a motivé chaque niveau.

Niveau Signaux – expressions JSX réactives à encapsulage automatique#

Un pass compagnon distinct à crates/bext-core/src/transform/prism_signals.rs fonctionne sur Fichiers "use signals" (signaux jsxImportSource) plutôt que la source de base PRISM. Là où la passe principale plie les sous-arbres statiques* en chaînes littérales, la transmission des signaux fait l'inverse : elle enveloppe les expressions dynamiques *dans des fonctions de flèche afin que le durée d'exécution des signaux voie une fonction réévaluable plutôt qu'une valeur matérialisée une fois au moment de l'appel JSX.

Sans le pass :

tsx
"use signals";
<p>Count: {count.value}</p>
//                ^^^^^^^^^^^^ already materialized — not reactive

Avec le pass (transparent pour le développeur) :

tsx
<p>Count: {(() => count.value)}</p>
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^ thunk; runtime re-evaluates on signal change

Ce qu'il enveloppe#

Conteneurs d'expression JSX dans les fichiers "use signals" où l'expression contient n'importe où à l'intérieur une lecture de <knownSig>.value. « Connu » désigne un identifiant que le laissez-passer a identifié précédemment dans sa portée comme étant lié par signal(…) ou computed(…). Exemples qui résument :

tsx
{count.value}                          // bare read
{count.value > 0 ? "yes" : "no"}       // conditional
{Math.max(a.value, b.value)}           // call expression
{items.value.length}                   // chained member
{`hello, ${name.value}!`}              // template literal
{count.value as number}                // type assertion (preserved verbatim)

Ce qu'il n'enveloppe PAS#

Pourquoi
count.value = 5 (affectation) LHS de l'affectation est une écriture, pas une lecture suivie
<p>{1 + 2}</p> aucun signal dans l'expression — la passe est précise
<p>{user.name}</p> (pas de .value sur l'utilisateur) user n'est pas un signal connu ; passe, laisse-le tranquille
Organes fonctionnels ( onClick={() => count.value++} ) les rappels sont différés - envelopper l'extérieur ne les capturerait pas de toute façon
Méthodes objets, corps getter idem – étendues de fonctions imbriquées

Règle d'activation#

Le laissez-passer se déclenche uniquement lorsque tous les sont :

  1. La première directive non-espace du fichier est "use signals" ou 'use signals'.
  2. Le fichier contient au moins un caractère < (ignore les fichiers .ts purs sans JSX).
  3. Le jsxImportSource du bundler est @bext-stack/framework/signals.
  4. La variable d'environnement BEXT_PRISM_COMPILE n'est pas 0 / false / off.

Mise en œuvre#

text
crates/bext-core/src/transform/prism_signals.rs       ~370 LOC

Même modèle de marcheur tsc_rs_ast que le pass principal PRISM : collecte les identifiants liés au signal de var/let/const = signal(…) / Les déclarations = computed(…), parcourent tous les conteneurs d'expressions JSX, demandent à expr_contains_signal_read s'il faut les envelopper, les files d'attente s'étendent sur les remplacements, les applique de bout en bout afin que les décalages d'octets restent valides.

Ce qui est différé#

Raison
Niveau 6 – atomisation de chaînes statiques La V8 intègre déjà des littéraux de chaîne analysés, donc l'atomisation économise des octets de paquet (modeste) mais ne coûte pas d'exécution. Le coût de mise en œuvre n’a pas été calculé uniquement pour le gain de taille du bundle. Documenté comme différé dans la source.
Threading enfants du composant <Card>{slot}</Card> — le filetage de la fente dans la référence {children} du corps nécessite une substitution adaptée à la portée.
Paramètre d'accessoires de style d'identification function Card(props) { return <div>{props.title}</div>; } — la substitution d'accès aux membres est plus fragile que la correspondance d'identifiant. Utilisez le function Card({ title }) déstructuré pour opter pour l'inlining de niveau 5.5.
Vérification de régression CI HTML-diff Une vérification qui diffère la sortie HTML de chaque page PRISM avec/sans la transmission de chaque PR. Une assurance bon marché contre les régressions futures.
Conseil

Pour désactiver la passe de compilation pour une seule build (par exemple, pour comparer les sorties compilées et non compilées), définissez BEXT_PRISM_COMPILE=0 dans l'environnement. Une utilisation normale n’en a jamais besoin.

Référence d'implémentation#

Déposer Rôle LOC
crates/bext-core/src/transform/prism_compile.rs La logique visiteur + repli ~3,840
crates/bext-turbopack/src/direct.rs::compile_closure Crochet pour la source d'entrée ~17
crates/bext-turbopack/src/direct.rs::transform_with_analysis_opts Hook pour les importations transitives ~17
crates/bext-server/src/handler.rs::handle_ssr Achemine bext run de manière autonome via la répartition PRISM ~25
crates/bext-turbopack/Cargo.toml bext-core = { path = "../bext-core" } 1
harnesses/jsx-shootout/ Faisceau de banquette 5 moteurs —

Le pass utilise tsc_rs_ast (le propre analyseur TypeScript de bext) — pas de dépôt SWC. Fonctionne dans le cadre du pipeline de transformation existant de bext - turbopack, ce qui signifie qu'il n'y a pas de nouvelle infrastructure pour le rechargement à chaud, la surveillance des fichiers ou la compilation incrémentielle - celles-ci sont déjà là.

En rapport#

  • Cadre PRISM - le framework que les cibles de la passe de compilation
  • Exécution JSX — la fonction h() que la passe remplace au moment de la construction
  • Signaux — transmission de signaux compagnon qui encapsule automatiquement les expressions réactives
  • Piscine de rendu V8 — le pool d'isolation qui exécute les bundles compilés
  • Transformer le pipeline — comment la passe de compilation s'intègre dans le bundler plus large
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.