Îles résumables sur Bun : leçons de conception issues du cadre en grès cérame
Comment un framework axé sur le serveur traite HTML comme valeur par défaut, limite JavaScript à des îlots reprendables, et gère l’export statique, les erreurs, le CSP ainsi que le déploiement.
La plupart des pages sur le Web se contentent de quelques contrôles interactifs ajoutés en surface, tandis que le modèle de développement dominant envoie toute la page au navigateur sous forme d’une application JavaScript. Stoneware, un jeune framework open source TypeScript basé sur Bun, part d’un postulat inverse : l’HTML est ce que le navigateur reçoit par défaut, et un composant doit opter explicitement avant que du JavaScript ne lui soit envoyé. Examiner sa conception est un moyen utile de comprendre les concepts tels que les îles, la reproductibilité, l’exportation statique, ainsi que les aspects moins spectaculaires de l’ingénierie de framework, comme les messages d’erreur, l’ordre de sécurité et l’emballage pour le déploiement. À la fin, vous devriez être en mesure de déterminer quand une architecture basée sur des serveurs et des îles convient à votre projet, ainsi quels pièges éviter si vous en développez ou en adoptez une.
Pourquoi une page de contenu ne devrait pas devenir une application
Imaginez une page de produit typique. Elle comporte un titre, une photo, une description, une liste des spécifications, un prix, des produits associés, des avis ainsi que des éléments de navigation. Parmi tout cela, seules deux choses répondent réellement à l’utilisateur : le menu mobile et le bouton « Ajouter au panier ». Tout le reste est du contenu statique que le serveur sait déjà comment générer.
Même une approche basée sur le rendu côté client ou une hydratation complète demande au navigateur de télécharger, d’analyser et d’exécuter du code représentant toute la page, uniquement pour que ces deux éléments de contrôle fonctionnent. La question liée à l’approche serveur en premier est simple : que se passerait-il si le navigateur ne recevait que du code pour les parties qui en ont besoin ?
Le modèle mental : HTML plus îles
L’architecture divise une page en deux types de zones. La majeure partie est constituée d’HTML généré sur le serveur. Les éléments interactifs forment des îlots, de petites régions autonomes qui contiennent leur propre JavaScript. Tous se retrouvent dans le même document dans le navigateur.
Web page
│
┌───────────┴───────────┐
│ │
HTML Islands
│ │
Server rendered JavaScript
│ │
└───────────┬───────────┘
│
Browser
La conséquence principale est que l’interactivité ne vous oblige plus à distribuer toute l’application. Rendre un widget interactif entraîne la transmission du code de ce widget uniquement, et non celui de la page.
Utiliser Bun plutôt que d’assembler une chaîne d’outils
Choisir Bun ne signifie pas pour autant que Node.js est obsolète. Node dispose d’un écosystème immense et alimente une grande partie du JavaScript utilisé en production ; il reste donc un environnement de exécution tout à fait fiable. Ce qui est intéressant, c’est ce qui change lorsque un framework est conçu autour d’un environnement de exécution qui intègre déjà les outils nécessaires à la plupart des projets.
Bun fournit un environnement d’exécution JavaScript accompagné d’un gestionnaire de paquets, d’un agrégateur et d’un exécuteur de tests. Pour les créateurs de frameworks, cela élimine beaucoup de composants superflus. Au lieu d’empiler un framework sur Node, puis un gestionnaire de paquets, ensuite un agrégateur distinct et enfin un exécuteur de tests séparé, la conception permet de considérer tous ces éléments comme une base cohérente.
Bun
│
┌────────────┼────────────┐
│ │ │
Runtime Tooling Testing
│ │ │
└────────────┼────────────┘
↓
Stoneware
Il est important de bien distinguer les rôles : Bun est la plateforme, tandis que Stoneware est le framework qui s’y installe. Si vous souhaitez une comparaison plus approfondie des environnements d’exécution eux-mêmes, consultez Node.js, Deno et Bun comparés.
L’HTML en premier, pris au sérieux
Le rendu côté serveur existe depuis longtemps, donc l’idée de « rendre du HTML sur le serveur » ne semble pas particulièrement remarquable. Le principe fondamental derrière Stoneware est que le serveur doit générer un HTML véritablement utile avant même que le navigateur n’ait besoin de comprendre quoi que ce soit concernant l’application.
Prenons un composant qui affiche un produit avec un titre et un prix.
<ProductCard
title="MacBook Pro"
price={1999}
/>
Rien dans ce composant ne nécessite que le navigateur conserve un modèle JavaScript de celui-ci. Le serveur peut le transformer en simple balisage :
<div class="product-card">
<h2>MacBook Pro</h2>
<span>$1999</span>
</div>
Ce résultat est déjà complet. Les utilisateurs peuvent le lire, les robots de recherche peuvent l’indexer, et le navigateur peut le rendre immédiatement. Aucun script n’est impliqué dans la livraison du contenu lui-même.
Faire de JavaScript une décision explicite
Ajoutons maintenant un bouton de panier. Contrairement à la fiche produit, il doit réagir aux clics, mettre à jour l’état et probablement communiquer avec une API.
<AddToCart product={product} />
C’est dans ce composant que le comportement côté client est justifié, ce qui en fait une île. Le reste de la page reste sous forme d’HTML, et la page finale ressemble à ceci :
Product page
│
├── Product title HTML
├── Product description HTML
├── Product image HTML
├── Product specifications HTML
│
└── Add to cart JavaScript island
La page n’est pas « sans JavaScript ». Elle est scriptée de manière sélective, et ce choix est fait par composant plutôt que pour toute la page. Cette distinction est importante car elle permet de garder les fonctionnalités par défaut simples et fait en sorte que chaque morceau de code intégré soit le résultat d’un choix conscient et visible.
Où se situe la frontière de l’île
Une île marque la frontière entre le contenu affiché sur le serveur et le comportement exécuté côté client. Un site de documentation illustre clairement ce schéma. Le corps de l’article, les titres, les exemples de code, les images, les liens, la navigation et le pied de page constituent tous du contenu. Seules quelques fonctionnalités sont véritablement interactives : la recherche, un changeur de thème, des boutons de copie dans les blocs de code et éventuellement un arbre de navigation extensible.
Documentation page
│
├── Article ─────────────── SSR
├── Code blocks ─────────── SSR
├── Images ──────────────── SSR
├── Navigation ──────────── SSR
│
├── Search ──────────────── Island
├── Theme switcher ──────── Island
└── Copy button ─────────── Island
Ce type de page, principalement composé de contenu avec quelques zones interactives, correspond exactement à l’usage prévu par le framework.
Hydratation versus reprise
Une objection légitime est que tout cela n’est rien d’autre que du SSR. C’est en partie vrai : le serveur génère l’HTML. La différence réside dans ce qui se passe une fois cet HTML reçu.
Avec l’hydratation classique, la séquence est à peu près la suivante :
- le serveur envoie l’HTML ;
- le navigateur télécharge le JavaScript de l’application ;
Le navigateur finit par reconstruire une application dont il a déjà reçu la sortie. Du point de vue de l’utilisateur, ce travail représente une charge inutile pure : les pixels étaient déjà à l’écran.
Stoneware, quant à lui, est conçu autour d’îles interactives reprendables. Le serveur envoie de l’HTML ainsi que tout l’état nécessaire à ces îles interactives, et le navigateur reprend là où le serveur s’est arrêté plutôt que de réexécuter la page pour retrouver cet état. L’objectif est d’éviter que de petites régions interactives ne forcent une reconstruction complète de la page.
Un objectif d’optimisation différent
La reprise possible redéfinit la question des performances. Au lieu de se demander comment accélérer l’hydratation de tout, on s’interroge sur la quantité de travail du navigateur que l’on peut éviter complètement. Le chargement passe de « HTML plus tout le JavaScript de l’application plus une étape d’hydratation » à « HTML plus uniquement le code nécessaire pour l’interaction, en reprenant depuis l’état généré sur serveur ». Pour les sites à fort contenu, cela représente souvent un avantage bien plus important que toute optimisation liée à l’hydratation.
Si vous travaillez avec React, la même logique sous-tend les composants serveur ; l’architecture derrière le rendu sans bundle décrit cette approche et propose une comparaison utile.
Un approche centrée sur le serveur ne signifie pas uniquement du serveur
Rien de tout cela n’est un argument contre les applications côté client. Les éditeurs collaboratifs, les outils de conception, les jeux navigateur et les applications interactives complexes ont des besoins très différents, et la plupart de leurs composants sont par nature interactifs. L’architecture vise plutôt l’autre extrémité du spectre : des pages où la majeure partie du contenu est naturellement affichée sur le serveur et où l’interaction constitue l’exception.
L’export statique découle de la même logique
Si l’HTML est la norme, une question qui se pose naturellement est de savoir pourquoi un serveur devrait fonctionner du tout pour des pages qui ne changent jamais à chaque demande. Stoneware peut exporter une application sous forme de fichiers statiques et les transmettre à un CDN.
Stoneware application
│
▼
export
│
▼
dist/
│
▼
CDN
La documentation, les pages de marketing et les catalogues de produits peuvent souvent être créés à l’avance et servis directement depuis le edge. Les routes qui nécessitent réellement une logique par requête peuvent rester générées côté serveur. L’avantage est que le fait de passer une route d’un mode statique à un mode SSR ne nécessite pas de changer le modèle de programmation de toute l’application.
Les routes dynamiques nécessitent une liste explicite de chemins
L’exportation statique devient plus complexe dès qu’une route comporte un paramètre. Une route comme /products/[sku] pourrait, en principe, correspondre à un nombre illimité d’URL, ce qui empêche l’exportateur de deviner quelles pages générer. Le framework demande donc à la route de les énumérer :
export function staticPaths() {
return products.map(product => ({
sku: product.sku
}));
}
Avec cette liste, l’exportateur crée un vrai fichier HTML pour chaque SKU connu, par exemple dist/products/laptop-1/index.html. C’est là que la conception du framework dépasse le simple rendu de JSX : le framework doit comprendre comment les routes, les données, l’étape de compilation et la cible de déploiement sont liées entre elles. Un cas limite pratique à prévoir concerne la situation où une route dynamique ne possède absolument pas de staticPaths() ; le framework doit soit échouer de manière explicite lors de l’export, soit continuer à rendre cette route côté serveur, et l’ignorer silencieusement est la pire option.
Les messages d’erreur font partie du rendeur
En son essence, un rendeur prend une composante et génère du HTML. La difficulté réside dans la grande variété d’éléments enfants et de propriétés qu’il doit gérer : valeurs textuelles et numériques, tableaux et null, éléments et composantes imbriquées, signaux et attributs, erreurs générées, tâches asynchrones, ainsi que, inévitablement, des valeurs qui ne peuvent pas être rendues.
Une erreur fréquente consiste à écrire <span>{product}</span> alors que l’on voulait en réalité <span>{product.name}</span>. Un message générique du type « valeur de type objet ne peut pas être rendue » ne vous indique presque rien sur l’endroit où chercher. Stoneware, quant à lui, décrit la valeur problématique :
Cannot render a plain object with keys: id, title, price.
et précise ensuite le chemin de la composante qui a conduit à cette situation :
in <span>
in <Price>
in <ProductCard>
in <Home>
L’énumération des clés de l’objet indique la propriété que vous visiez probablement, et la trace du composant pointe vers le fichier exact à ouvrir. Un framework est évalué non seulement en fonction de son parcours optimal, mais aussi selon la rapidité avec laquelle il vous aide à surmonter les problèmes qui surviennent.
Lorsqu’un micro-benchmark cache le vrai coût
Récupérer cette trace de composant signifiait à l’origine envelopper de nombreuses opérations de rendu dans des blocs try/catch. Un micro-benchmark isolé indiquait que ce coût supplémentaire était négligeable. Cependant, lors du rendu réel d’une page, l’enveloppement de chaque élément faisait augmenter le coût de rendu d’environ 38 %.
Cette explication est familière à quiconque effectue des benchmarks en JavaScript : dans un benchmark minuscule et répétitif, le compilateur optimiseur du moteur peut éliminer ou déplacer une grande partie du travail que vous essayez de mesurer, de sorte que le chiffre obtenu reflète une version du code ayant fait l’objet d’optimisations.
Les micro-benchmarks peuvent induire en erreur lorsque le temps d’exécution optimise justement ce qui est mesuré.
La solution était structurelle : la charge liée au suivi des erreurs était confinée aux limites des composants, et chaque élément utilisait des opérations d’enregistrement et de restauration moins coûteuses pour maintenir le parcours. Les tests A/B sur des rendus réels n’ont alors montré aucune différence significative. La leçon à retenir est que les performances du rendeur dépendent autant de l’absence de dégradation lors de l’ajout de fonctionnalités pratiques pour les développeurs que de la vitesse brute, et que toute affirmation concernant les performances doit être validée sur des charges de travail représentatives.
Paramètres par défaut en matière de sécurité et ordre du pipeline
Une application de base ne devrait pas exiger que son développeur se souvienne de chaque primitive de sécurité avant de la mettre en ligne. Stoneware est livré avec des paramètres par défaut tels que la protection CSRF et le support de la politique de sécurité du contenu.
L’ordre du pipeline de traitement des requêtes est délibéré :
- La protection CSRF s’exécute en premier ;
- le middleware de l’application suit ;
- vient ensuite la correspondance des routes et l’affichage du contenu ;
- tout est renvoyé via un seul point de sortie.
Si le middleware de l’application s’exécutait avant la vérification CSRF, du code utilisateur pourrait se retrouver sur un chemin permettant de contourner les mesures de sécurité au niveau du framework, par exemple en renvoyant prématurément des données ou en réécrivant la requête. En intégrant cet ordre directement dans le framework, plutôt que de se contenter de le documenter en espérant que tout le monde le respecte, on crée précisément la règle que doit imposer un framework. Un seul point de sortie garantit également que des en-têtes tels que CSP soient appliqués de manière cohérente à chaque réponse.
Étendre un CSP strict sans le désactiver
Une politique restrictive constitue un bon paramètre par défaut, mais les sites réels chargent des outils d’analyse, des widgets de paiement, des API, des polices web et des cartes. Le problème le plus courant est que le développeur se heurte à un script bloqué et désactive complètement la CSP. Une meilleure conception permet d’étendre des directives individuelles tout en conservant le reste de la politique par défaut intact :
csp: {
scriptSrc: ["https://www.googletagmanager.com"],
connectSrc: ["https://www.google-analytics.com"],
imgSrc: ["https://www.google-analytics.com"],
}
Le principe repose sur des paramètres par défaut sécurisés ainsi que sur une liste d’autorisation explicite, directive par directive, pour les tiers. Lors de la conception d’une telle API, il faut déterminer clairement si une source fournie complète la directive par défaut ou la remplace, et documenter cette décision, car ces deux comportements sont possibles et leur différence a des conséquences sur la sécurité.
Les bugs les plus difficiles apparaissent après le moteur de rendu
Emballage des ressources isolées pour Vercel
La version 0.1.8 a modifié la manière dont Stoneware se déploie sur Vercel. Pour cette cible, les fragments clients générés sont désormais intégrés dans le paquet serveur sous forme de données base64, ce qui fait d’eux une partie du paquet traité que la plateforme déploie ; ils sont ensuite servis depuis /_stoneware/*.
Stoneware build
↓
server bundle
├── server code
├── CSS assets
└── island assets
↓
Vercel
↓
/_stoneware/*
Ce comportement est facultatif et limité à la cible Vercel. Une déploiement dans un conteneur dispose déjà des fichiers sur le disque et n’a aucune raison de conserver une deuxième copie de chaque élément au sein du paquet serveur. En résumé, les plateformes d’hébergement diffèrent quant à ce qu’elles incluent dans un déploiement, ce qui oblige souvent à adopter des stratégies adaptées à chaque cible plutôt qu’une solution universelle.
Tests permettant de valider la correction
Le projet compte plus de 500 tests automatisés couvrant le rendu, le routeur, les îles et signaux, les feuilles de style et la distribution des ressources, l’exportation statique, CSRF et CSP, les cibles de déploiement, le rapport d’erreurs, ainsi que des entrées hostiles ou inhabituelles telles que les tentatives de parcours de chemin, les fichiers binaires et les chemins vers des ressources mal formatés.
Le nombre en lui-même n’est pas important. La mesure plus utile est la suivante :
Utiliser un agent de codage sans externaliser la conception
- Pour chaque route, le mode SSR ou l’export statique est-il approprié ?
staticPaths() ?Un agent peut étudier ces questions et mettre en œuvre une réponse choisie, mais quelqu’un doit encore remettre en question la conception et vérifier le résultat.
Plausible n’est pas synonyme de correct
Un agent de codage peut produire une implémentation convaincante très rapidement, et cette vitesse est précieuse. Cela signifie également qu’il peut se tromper rapidement. Le problème des ressources de Vercel illustre ce cercle vicieux : la première correction copiait les ressources générées dans public/, ce qui semblait logique et passait les tests locaux ; mais un déploiement réel a montré que cette hypothèse était fausse. La deuxième tentative a modifié le modèle de déploiement lui-même. Les tests de cas limites ont ensuite révélé un autre bug dans cette version.
Ce cycle fait partie de l’ingénierie logicielle ordinaire, et non d’une défaillance de l’IA. Ce qui change, c’est la vitesse de chaque itération, ce qui rend la vérification du début à la fin dans l’environnement cible réel encore plus importante, et non moins.
Pourquoi le choix du temps d’exécution compte au-delà de la vitesse
Réduire l’histoire à « Bun est plus rapide que Node » manquerait le but, d’autant que la vitesse brute n’est de toute façon pas une philosophie de framework. L’expérience plus intéressante consiste à concevoir un framework en partant du principe qu’un environnement d’exécution moderne et une chaîne d’outils intégrée sont disponibles dès le premier jour. La combinaison offerte par Bun d’environnement d’exécution, de gestion de paquets, de regroupement de fichiers et de tests en fait une base pratique pour ce type d’exploration architecturale.
Où s’intègre cette architecture
Cette approche se révèle particulièrement utile lorsque la majeure partie d’une page est constituée de contenu et que seule une partie est interactive :
- Documentation : les articles et les exemples de code sont générés côté serveur ; la recherche, le changement de thème et les boutons de copie constituent des éléments isolés.
- E-commerce : les détails des produits, les images et le contenu SEO sont générés côté serveur ; le panier d’achat et les filtres sont des éléments isolés.
Où c’est probablement le mauvais outil
Si votre produit est en réalité une application de bureau exécutée dans un navigateur, telle qu’un éditeur collaboratif, un outil graphique, un jeu, un tableau de bord très interactif ou toute application où presque tous les composants fonctionnent côté client, la part de la page qui peut rester sous forme d’HTML est faible. Le modèle des entités isolées ajoute alors des limites sans apporter beaucoup d’avantages, et une architecture axée sur le client est probablement plus adaptée. Un framework peut avoir une opinion tranchée sans prétendre convenir à tous les types de charges de travail.
Essayer
Lors de la rédaction de ce texte, le stoneware est encore dans sa série 0.1.x, il peut donc présenter des imperfections ; consultez le répertoire pour connaître son état actuel. Les améliorations prévues comprennent davantage d’intégrations, de meilleurs outils de diagnostic et d’avertissements de développement, plus d’exemples et de cibles de déploiement, une documentation plus complète ainsi que davantage d’applications pratiques. Pour structurer un projet et démarrer le serveur de développement :
bun create stoneware my-app
cd my-app
bun dev
Un bon premier projet d’essai est quelque chose de petit mais riche en contenu : un blog, un site de documentation, un catalogue de produits, un portfolio ou un site professionnel. Tout en le développant, demandez-vous constamment quelle partie de la page a réellement besoin de JavaScript.
Points clés
- Tenez l’HTML pour la sortie par défaut et faites en sorte que le JavaScript côté client soit activé uniquement pour chaque composant ; la page devient ainsi scriptée de manière sélective plutôt que d’être considérée comme une application.