PRISM
PRISM est le framework natif de bext. JSX se compile pour diriger les chaînes HTML via le runtime h() — il n'y a pas de React sur le serveur, pas de DOM virtuel, pas de différence. Une passe de compilation au moment de la construction fournie dans bext-core plie des arborescences de composants entières en concaténations de chaînes uniques avant que tout travail d'exécution ne se produise.
Sur le catalogue Tailwind actuel de 38,3 Ko, la passe de compilation s'affiche en 15,7 µs en moyenne et le véritable chemin d'exécution automatique JSX en 75,3 µs. React 19
renderToString prend 1 823 µs pour le même DOM. Voir Performance pour le luminaire, l'exécution et la méthodologie.
Le sigle est décoratif. Lisez-le comme "la façon dont bext restitue TSX sur le serveur" et continuez.
Démarrage rapide#
Un site PRISM minimal :
mkdir my-site && cd my-site
mkdir -p src/app
src/app/page.tsx:
export default function HomePage(props: {
searchParams?: { name?: string };
}) {
const name = props.searchParams?.name ?? "world";
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>my-site</title>
</head>
<body>
<h1>Hello, {name}!</h1>
<p>Rendered by PRISM (no React on the server).</p>
</body>
</html>
);
}
bext.config.toml:
[server]
port = 3088
app_dir = "."
[framework]
type = "prism"
tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@bext-stack/framework"
}
}
package.json:
{
"dependencies": {
"@bext-stack/framework": "workspace:*"
}
}
Courir:
bext run .
C'est tout le cadre. Aucun script de build package.json, non
vite.config.ts, pas de next.config.mjs. La première requête déclenche une compilation par route (~ 30-200 ms à froid) ; chaque requête suivante est un isolat V8 chaleureux avec des contextes de page mis en cache (rendu d'environ 1 à 5 ms).
Comment ça marche#
Le moteur d'exécution h()#
Le runtime JSX de PRISM se trouve sur @bext-stack/framework/jsx-runtime. Il exporte jsx, jsxs, Fragment et (depuis le 29/04/2026) escapeHtml. Le mode react-jsx de TypeScript importe automatiquement jsx / jsxs sur chaque site d'appel JSX.
// What you write:
<div className="card">
<h2>{title}</h2>
</div>
// What tsc-rs emits (when no compile pass runs):
jsx("div", { className: "card", children: jsx("h2", { children: title }) })
// What h() does (simplified):
function h(tag, props, ...children) {
if (typeof tag === "function") return tag(mergeChildren(props, children));
const attrs = formatAttrs(props); // " class=\"card\""
if (isVoidElement(tag)) return safe(`<${tag}${attrs}>`);
return safe(`<${tag}${attrs}>${renderChildren(children)}</${tag}>`);
}
Trois propriétés rendent cela réalisable :
Les chaînes simples sont échappées ; Le code HTML rendu porte la marque —
h()s'enroule sous-arbres rendus dansSafeHtml. Un parent transmet cette marque, tandis qu'une expression de chaîne simple est échappée. Cela empêche à la fois l’injection et la double évasion sans marqueur intra-bande dans la réponse.Le chemin asynchrone est
AsyncIterable<string>— lorsqu'un enfant est unPromiseouAsyncIterable,h()produit des morceaux : ouvrez la balise, puis chaque enfant résolu, puis fermez la balise. Le protocole de streaming dans@bext-stack/framework/streamingconstruit Suspense au-dessus de cette primitive.Les attributs et expressions enfants sont des limites d'échappement — dynamique les attributs passent par
formatAttrs; les enfants à cordes simples passent parescapeHtml. Voir HTML s'échappant pour savoir ce que cela signifie dans la pratique.
La passe de compilation#
Lorsque [framework] type = "prism" est défini, le pipeline de transformation bext - turbopack exécute prism_compile.rs sur chaque fichier .tsx / .jsx dans la fermeture transitive de l'itinéraire. Le pass est implémenté en tant que visiteur de style SWC sur tsc_rs_ast (pas de dépôt swc - utilise le propre analyseur TypeScript de bext).
Ce qu'il plie :
| Modèle | Sortir |
|---|---|
<head><meta charSet="utf-8"/></head> |
__bextSafe("<head><meta charset=\"utf-8\"></head>") |
<h1>Hello, {name}!</h1> |
__bextSafe("<h1>Hello, " + __bextChild(name) + "!</h1>") |
<div className={cond ? "a" : "b"}> |
'<div class="' + __bextEsc(cond ? "a" : "b") + '">' |
<ul>{items.map(i => <li>{i}</li>)}</ul> |
un accumulateur IIFE avec une boucle for et un texte dynamique échappé |
<Header/> (zéro-hélice) |
insère récursivement le corps de Header |
<Card title="x" body={user.bio}/> (paramètre déstructuré) |
corps en ligne avec substitution d'accessoires |
Récursif : un parent contenant un composant enfant contenant un
.map() contenant une classe ternaire — tous se réduisent en une seule expression concaténée. Sur l'appareil de pliage documenté, 0 sur 11 appels jsx() restent dans le bundle compilé.
Le laissez-passer est par défaut et sécurisé par construction : tout ce qu'il ne peut pas prouver statiquement en toute sécurité passe au runtime jsx(). Cautions sur :
- Attributs de diffusion (
<div {...rest}>) - Composants avec le paramètre de style Ident
function Card(props)— uniquement lefunction Card({ a, b })déstructuré est pris en charge - Références
props.children— pourrait êtreAsyncIterable<string>, la contrainte de chaîne rendrait[object AsyncGenerator] style={{...}}etdangerouslySetInnerHTML={{...}}— exécution applique la sémantique des objets- Appels de composants avec des enfants (
<Card>{slot}</Card>) - Flèches au corps de bloc avec effets secondaires dans les rappels
.map() - Fonctions asynchrones/générateurs
Désactivez-vous avec BEXT_PRISM_COMPILE=0 dans l'environnement. Utile pour les versions différentes ; inutile en utilisation normale.
Voir le document de compilation pour plus de détails.
Routage du système de fichiers#
src/app/
page.tsx → /
about/page.tsx → /about
blog/[slug]/page.tsx → /blog/:slug (dynamic param)
shop/[...rest]/page.tsx → /shop/* (catch-all)
api/users/route.ts → /api/users (HTTP API)
islands/Counter.tsx → /islands/Counter.js (browser bundle)
components/MyComp.tsx → not routed (server-only library)
Les mises en page enveloppent les pages par répertoire :
src/app/
layout.tsx ← root layout (renders for every route)
blog/
layout.tsx ← nested, wraps every blog/*
page.tsx ← /blog
[slug]/
page.tsx ← /blog/:slug
Le répartiteur compose les mises en page à l'envers :
Layout0(Layout1(Layout2(Page(props)))). Chaque mise en page reçoit
children comme premier accessoire, ainsi que le contexte complet de la demande.
Actions du serveur#
PRISM expose un modèle de mutation de style Remix : publiez un <form> sur
/_bext/action/<exportName>, expédition via le actions.ts colocalisé :
// src/actions/auth.ts
"use server";
export async function login(req: Request): Promise<Response> {
const body = await req.formData();
// ... auth logic ...
return new Response(null, {
status: 302,
headers: { Location: "/dashboard", "Set-Cookie": "session=..." },
});
}
// somewhere in JSX
<form method="post" action="/_bext/action/login">
<input name="email" />
<input name="password" type="password" />
<button>Sign in</button>
</form>
Le répartiteur parcourt src/actions/*.ts et trouve les fichiers commençant par
"use server", analyse leurs exportations. Tout POST /_bext/action/<exportName> est envoyé à la fonction correspondante. Voir Conventions de données PRISM pour les conventions de chargeur/action colocalisées avec les fichiers de route.
Îles#
Pour l'interactivité, marquez un composant avec "use client" :
// src/islands/Counter.tsx
"use client";
import { useState } from "preact/hooks";
export default function Counter({ start = 0 }: { start?: number }) {
const [n, setN] = useState(start);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Utilisez-le depuis un composant serveur :
import Counter from "../islands/Counter";
export default function Page() {
return (
<main>
<Counter start={42} />
</main>
);
}
Le serveur affiche un espace réservé <bext-island> ; le chargeur client récupère /islands/Counter.js, analyse les accessoires de data-props et appelle le mount(el, props) du composant. Les îles peuvent utiliser n'importe quel framework côté client ( React, Preact, Solid, Vanilla DOM) - le serveur s'en fiche, il expédie simplement le bundle.
Performance#
Numéros du harnais jsx-shootout sur une page de liste de produits de 38,3 Ko validée en octets. Les mesures utilisent Bun 1.3.13 sur un noyau épinglé ; les valeurs PRISM /Solid sont des médianes d'exécutions alternées :
| Moteur | moyenne | p50 | Débit |
|---|---|---|---|
| Constructeur de chaînes Rust | 3,9 µs | 3,9 µs | 255 000 rendus/s |
| Plafond JS sécurisé roulé à la main | 15,8 µs | 14,3 µs | 63 000 rendus/s |
| PRISM passe de compilation | 15,7µs | 14,0 µs | 64K rendus/s |
| Marc 5 | 38 µs | 22 µs | 26 000 rendus/s |
PRISM interprété, direct h() |
69,3 µs | 62,1 µs | 14,4K rendus/s |
| PRISM automatique JSX | 75,3 µs | 67,9 µs | 13,3K rendus/s |
Solide 1.9 ( generate: "ssr" ) |
78 µs | 61 µs | 12,8 000 rendus/s |
| React 19 | 1 823 µs | 1 646 µs | 549 rendus/s |
Le PRISM compilé est environ 2,4 fois plus rapide que Marko et 116 fois plus rapide que le React sur cet appareil, tout en correspondant au plafond JavaScript écrit à la main, équivalent en matière de sécurité, dans le bruit de mesure. La ligne Rust est une limite inférieure native, et non une implémentation JavaScript de PRISM.
PRISM et Solid interprétés sont désormais dans la même bande : Solid a le p50 inférieur, tandis que PRISM a la moyenne légèrement inférieure dans les dernières exécutions alternées. Le runtime automatique réutilise les accessoires intrinsèques créés par le compilateur au lieu de les cloner, contourne la normalisation des composants redondants lorsque les enfants sont déjà normalisés et conserve deux résultats exacts d'échappement de classe pour les lignes répétées du tableau. Le cache est limité à deux entrées et stocke la valeur d'échappement, de sorte que les classes dynamiques répétées n'affaiblissent pas le contrat d'échappement HTML.
Pour le contexte, une mesure historique distincte de bout en bout HTTP TTFB sur
sites/status (la page d'état bext.dev, un site de production PRISM) a enregistré :
| Mode | moyenne | p95 |
|---|---|---|
BEXT_PRISM_COMPILE=0 |
17,59 ms | 23,69 ms |
| par défaut (compilation activée) | 6,50 ms | 8,39 ms |
Cette exécution a enregistré 2,71 fois plus rapide TTFB. Il n'a pas été réexécuté dans le cadre de la fusillade actuelle de rendu de 38,3 Ko. Sa différence de 4 à 5 ms entre le microbench SSR et HTTP TTFB était la surcharge du framework HTTP (TCP, routage actix, écriture de réponse), non affectée par la passe.
Quand utiliser PRISM#
| Cas d'utilisation | Choix |
|---|---|
| Marketing public / documents / blog | PRISM — pages zéro JS, rendus en microsecondes |
| Statut / tableau de bord / administrateur en lecture principale | PRISM + quelques îlots pour les bits interactifs |
| Contenu basé sur les magazines/CMS | PRISM + cache ISR (TTL par route dans bext.config.toml) |
| Application avec soumissions de formulaires, redirections | PRISM + actions serveur |
| Application interactive en temps réel (chat, éditeur) | Next.js / React (les îles PRISM ne sont pas un SPA complet) |
| Base de code React / Next.js existante | Restez sur Next.js, utilisez bext comme interface HTTP ( type = "nextjs" ) |
PRISM est votre propre framework, dans votre dépôt. Il n'y a pas
Surface de l'API @bext-stack/framework au-delà de ce qu'elle contient
sites/shared/framework/ — lancez le runtime, ajoutez des composants, expédiez. L'équipe bext utilise PRISM pour bext.dev, status.bext.dev,
docs.bext.dev et une poignée de sites internes ; la passe de compilation a été développée contre ceux-ci.
Les enfants de l'expression PRISM ( {value} ) sont automatiquement échappés lorsque value est une chaîne simple. Les sous-arbres rendus portent la marque SafeHtml hors bande et passent sans double échappement. Utilisez dangerouslySetInnerHTML uniquement pour du HTML riche fiable ou déjà nettoyé. Voir HTML s'échappant.
Si vous souhaitez un routage de système de fichiers imbriqué sans écrire manuellement un routeur, consultez Routage et mises en page — defineRoutes() gère la composition de la disposition.
Quelle est la prochaine étape#
- Détails de l'exécution JSX —
h(), attributs, règles d'échappement - La passe de compilation – qu'est-ce qui se plie, qu'est-ce qui ne se plie pas, données de performances
- Conventions de données PRISM — Chargeur/action de style Remix
- Îles — React - composants interactifs hydratés
- Signaux — réactivité à grain fin, pas de React, ~ 3 Ko d'hydrate
- Actions du serveur — Exportations
"use server",<Form>PE, téléchargements en plusieurs parties - Vent arrière — Prise en charge intégrée de Tailwind v4 de bext
En rapport#
- Exécution JSX —
h(), alias d'attribut, contrat d'échappement HTML - Passe de compilation PRISM — comment les sous-arbres statiques se replient en chaînes littérales
- Conventions de données PRISM — colocalisation chargeur/action
- Architecture des îles — amélioration progressive avec les offres groupées client
- Signaux — réactivité fine sans React
- Pipeline de rendu — comment bext envoie une requête à un isolat V8