Accueil / Articles / Îles résumables sur Bun : leçons de conception issues du cadre en grès cérame

Î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.

3252 mots

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 framework exécute et reconstruit l’arbre des composants en mémoire ;
  • des gestionnaires d’événements sont attachés au DOM existant.
  • 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é ?
  • Comment l’export doit-il se comporter pour une route dynamique qui ne dispose pas de staticPaths() ?
  • Comment le rendu doit-il réagir lorsque un composant renvoie un objet simple ?
  • Quelles sont les locations que le processus de construction parcourt pour trouver les feuilles de style ?
  • Comment les chunks clients générés parviennent-ils au navigateur sur Vercel ?
  • Une directive CSP fournie par le développeur s’ajoute-t-elle à la directive par défaut ou la remplace-t-elle ?
  • 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.
  • Sites professionnels : les informations sur l’entreprise, les services et les marchés sont générés côté serveur ; le formulaire de contact constitue une entité isolée ou nécessite une interaction avec le serveur.
  • Sites de contenu : les articles sont générés côté serveur ; les commentaires et la recherche constituent des entités isolées.
  • 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.
  • La reproductibilité change l’objectif, passant d’une hydratation plus rapide à une évitation complète du travail du navigateur, ce qui est particulièrement avantageux sur les pages à fort contenu.
  • L’export statique et le SSR peuvent coexister dans un même modèle de programmation, mais les routes dynamiques nécessitent une liste explicite des chemins.
  • Investissez dans des messages d’erreur qui indiquent la valeur incorrecte ainsi que le chemin du composant ; validez les performances sur des rendus réalistes, et non uniquement à l’aide de micro-benchmarks.
  • Intégrez dans le framework l’ordre des mesures de sécurité, comme le CSRF avant les middleware, et permettez aux développeurs d’étendre les directives CSP plutôt que de désactiver la politique.
  • Les cibles de déploiement diffèrent ; vérifiez le traitement des ressources du début à la fin, et écrivez des tests de régression qui échouent en l’absence de correction.
  • Les agents d’intelligence artificielle accélèrent la mise en œuvre, mais les décisions architecturales et la vérification dans un environnement réel restent des responsabilités humaines.