Aujourd'hui, cela fait exactement un mois que j'ai déménagé aux États-Unis ; une fois arrivé en Caroline du Nord et le quotidien installé, j'avais à peu près fait le tour de la région et des sites les plus proches, et me voilà donc désœuvré. Sauf que je ne suis justement pas quelqu'un qui supporte l'oisiveté : dès que j'ai du temps libre, je me trouve quelque chose à faire reprendre le blog, ça compte, non ?, mais malheureusement la logistique n'est pas très pratique ici aux États-Unis, donc impossible de dessiner des cartes pour le moment. Il ne me reste plus qu'à revenir écrire un peu de logiciel pour passer le temps, et par la même occasion reprendre et combler les trous que j'avais laissés il y a quelques mois ?
Mais avant de commencer à râler sur les autres, il faut sans doute d'abord combler quelques bases sur seam & SeamJS (x mais qu'est-ce que c'est que ce truc au juste vu que le billet précédent remonte à plusieurs mois, vous avez très probablement tout oublié, et le README.md sur GitHub, j'ai la flemme de le retoucher pour l'instant
Le rendu comme protocole
L’interface utilisateur doit être décrite, et non exécutée.
Pour faire simple, Seam1 est d'abord un protocole, pas un énième SSR runtime. Autrement dit : à la compilation on produit un squelette HTML contenant des slot, et à l'exécution / à la requête on se contente de glisser des valeurs dans ces slot ; if / each / match sont eux aussi des nœuds de protocole figés dans le squelette. Du coup, au moment où la requête arrive, aucun composant d'UI n'a été exécuté, et le serveur n'a qu'une seule chose à faire : . Il n'a donc plus aucune raison d'être Node ou Bun -- un backend en Rust / Go peut bien remplir une page de HTML, non, où est la difficulté ?! Cela dit, plus loin, le JS Runtime n'est pas totalement éliminé ; je veux dire qu'on peut s'en passer, à condition d'accepter certaines limitations Quoi qu'il en soit, le gain de performance global est indiscutablement exponentiel, puisque obtenir la plupart des effets du SSR ne demande plus de faire tourner un UI render au moment de l'exécution / de la requête...
Le modèle à la requête ? 2
À peu près depuis le second semestre de cette année, j'ai entièrement port ce site de TanStack Start React vers SvelteKit Svelte. Une fois toute la migration terminée, j'ai constaté que c'était vraiment bien : le coût en performance de ssr a baissé d'environ un ordre de grandeur, le coût d'hydratation est minime, ça tourne très agréablement, et la philosophie de compilation du framework est excellente et très légère. Mais du côté de Kit, il reste toujours des choses qui me laissent insatisfait. La plupart peuvent se régler avec des plugins c'est du Vite assemblé, après tout, mais il y a toujours des parties auxquelles on ne peut pas toucher ; cela dit, le problème vient sans doute de moi, car ce que seam veut changer entre probablement en conflit fondamental avec n'importe quel framework meta existant au fond, je remets en cause leur modèle d'exécution (une fonction render à chaque requête), leur frontière de données (load et UI collés ensemble) et leurs capacités backend (rivées au runtime JS).
Pseudo-SSR 3
Inutile d'y réfléchir, quelqu'un va forcément demander : ce n'est pas juste Astro / Qwik / Marko ? , seam est, par la forme du langage, ce qui se rapproche le plus de Marko ; par le peu de JS envoyé par défaut, de Astro ; par le fait de ne pas replay hydrate, de Qwik. Sauf que leur request-time continue d'exécuter un programme d'interface : les render propres à island, l'unique SSR de Qwik, les fonctions de template JS compilées par Marko — toutes exécutent encore un morceau de renderer à chaque requête.
Ces mille {#each}, au fond, je ne fais moi aussi qu'assembler des HTML, donc ce n'est pas que je ne produis pas de document4 ; la différence, c'est que ce que j'assemble est un protocole déjà autonome, et non des Tree de composants régénérés. Seam veut AST → IR → HTML avec slot ; à la requête, on ne fait qu'interpréter le protocole, plus le injection des zones de données dynamiques, il n'y a donc plus besoin de faire tourner la partie des fonctions de UI render. N'importe quel backend peut donc remplir, et une fois cette dernière couche residual-isée, le problème du vendor lock in JS5 se trouve résolu. Donc en réalité, ma direction générale et celle des solutions existantes citées plus haut ne sont même pas sur la même ligne (x)
Adopter
Seam hydrate, mais ni à la manière classique ni à celle de Qwik. L'hydratation classique, c'est le client qui render une seconde fois puis compare avec DOM ; Qwik SSR un HTML plus l'état des composants, le client resume et ne replay pas ; alors que chez Seam, au request-time, même le UI renderer n'a en réalité pas besoin de tourner : le squelette d'hydratation, le premier écran et les données de hydrate viennent tous du même injection. Le client reçoit un DOM déjà cohérent plus les mêmes données, et accroche les événements et le client state selon le IR produit à la compilation adopt ; ce n'est pas un replay, et ce n'est évidemment pas non plus resume tout un arbre de composants.
C'est précisément pour ça que « rendre une fois de chaque côté puis comparer », ce genre de mismatch6 est passé du défaut à l'état illégal. Je ne prétends pas que l'implémentation est à 100 % : je dis que ce genre d'écart n'a aucune existence légitime dans le modèle. S'il apparaît, c'est un bug dans mon implémentation compiler / injector, et il peut être fix ; les exemples typiques sont (le squelette n'est pas passé par le parseur HTML, l'échappement ne correspond pas, l'IR et le squelette sont désynchronisés).
Bien sûr, skeleton doit lui-même être parse-stable7 ; fourrer n'importe comment des comment dans table, c'est une implémentation mal faite, et ce n'est pas à mettre sur le dos de mon modèle. Ce que le build-time calcule, c'est la table d'hydratation (qui est statique, qui est slot, ce qui est lié), pas les données figées à l'avance -- en revanche il fige bien le type de données de ce slot. Les valeurs elles-mêmes restent librement remplaçables au request-time ; simplement, HTML et payload doivent être une seule et même chose.
Le même graphe ?
Astro Certes, le premier écran est lui aussi un document, et ClientRouter reste possible ensuite, mais la différence tient à ceci : sa partie dynamique reste islands qui render chacun de leur côté, et ClientRouter ressemble au fond davantage à document morph qu'au même graphe de composants encore vivant après l'hydratation. La pile technique d'Astro ne convient donc tout simplement pas pour cela ; elle n'a rien de fautif, elle n'a simplement pas été conçue pour l'usage que tu veux en faire. Tu peux bien sûr objecter qu'il y a la View Transitions API8, mais alors je te réponds : est-ce que la View Transitions API met du beurre dans les épinards, est-ce qu'elle te laisse enrober tes pages d'une animation Motion9… je m'égare un peu ;
De toute façon, avant l'hydratation personne ne peut rien bouger, et l'absence d'animation sur la première frame10, avant que JS ne démarre, n'est la faute de personne ; Seam garantit seulement que injection est correct, que document est correct et que le point d'entrée client est correct. La vraie contrainte se situe après l'hydratation : la navigation client injecte les données dans ce graphe toujours vivant (le layout persiste), au lieu de repasser par injection pour remplacer document. Bref, ce que Seam reprend après l'hydratation, c'est le même graphe de composants statique.
Il y a aussi Marko 6, qui se rapproche déjà beaucoup de ce que j'ai en tête (AST de template, granularité fine à la compilation, resume plutôt que replay) ; mais face à Marko, on peut encore tracer deux lignes techniques : d'une part request-time exécute toujours un produit de compilation JS, toute la chaîne reste soudée au runtime JS ; d'autre part l'auteur écrit en Marko, pas en Svelte. Je suis convaincu qu'en 2026 la plupart des gens, quand ils parlent de front-end, ne retiendront comme UI Stack un tant soit peu répandue que React, Vue, Svelte, Solid ; si je n'en parlais pas, beaucoup ne sauraient sans doute même pas ce qu'est Marko. Bâtir sur Svelte, ce n'est donc pas vraiment « recopier un principe et y coller un backend » : je préfère l'expliquer comme emprunter une UI que quelqu'un a écrite, que des gens utilisent, dont on corrige sans cesse les bugs et qu'on polit sans cesse -- rien que des choses familières à tout le monde.
Autre point : ce que le « sans hydratation » de Qwik économise, c'est le replay côté client, mais pas ce seul SSR côté serveur11(?), donc dommage. Aujourd'hui, aucun framework ne me satisfait, et aucun ne me donne assez d'API pour que je puisse y ajouter quelque chose ; c'est précisément pour ça qu'il y a quelques mois je me suis lancé dans ce truc qu'est SeamJS ; et aujourd'hui, il s'agit justement d'en replanifier la roadmap.
Aujourd'hui, je suis tout à fait d'accord avec « Svelte: for building, not frameworking.12 »
React
Parlons d'abord de React : en laissant de côté le verrouillage commercial par Vercel Next.js et le fait qu'il ait son propre roadmap, il y a d'abord les CVE qui pleuvent de partout, rien que ça condamne à surveiller les Social Media à chaque instant pour voir si une nouvelle faille a été report & release. Les Router des autres sont globalement inutilisables, Remix tout particulièrement13, mais en 26 il existe une option très particulière : TanStack Start. J'utilise TanStack Router depuis longtemps et je peux dire que, hormis un style de nommage et des structures de données pas très élégants14, sur le plan fonctionnel c'est la seule option que je valide. C'est un peu jeune, certes, mais ça règle le problème de lenteur au premier accès à une page après démarrage à froid que le processus next-server de Next.js 16 n'a toujours pas résolu. Next.js a d'ailleurs toujours eu ce à-coup inexplicable ; ce n'est pas un manque de puissance du serveur, je l'attribuerais plutôt soit au Load de démarrage qui n'est pas terminé, soit à runtime qui compile encore certaines choses. Et cette architecture est par nature peu adaptée au principe des instances multi-régions démarrées au plus près, comme chez Cloudflare Worker ; seul un processus Node long traditionnel à la Vercel15 échappe à ce problème (?) je m'égare un peu, sur Next.js je pourrai écrire tout un article rien que pour râler, mais TanStack16 me paraît vraiment assez mûr pour un usage commercial, j'en ai au moins fait le site commercial d'un projet. Mais ça ne fait jamais que la seule des deux options (x)
Vue
Pour Vue : personnellement, quand je suis passé de Vue 2 à Vue 3, je n'ai plus vraiment adhéré à la syntaxe ni aux mécanismes ; le Vue que j'ai en tête restera toujours Vue 2. Il est indéniable que Vue 3 apporte quantité d'excellents feature, mais comparé à Vue 2, son cœur me semble avoir changé -- et beaucoup changé. Par ailleurs, d'un point de vue commercial, Vue n'est-il pas de plus en plus complètement ligoté à la couche meta, contrôlé par l'écosystème ? Sa situation ressemble énormément à celle de Svelte : tous deux open source, mais il est en réalité très difficile qu'un tiers se lève pour t'offrir une seconde implémentation. C'est précisément pour cela que la redistribution des cartes provoquée par l'arrivée de TanStack Start a porté un coup si rude à Next.js ; il faut saluer Tanner Linsley. Il y a encore un point, un problème de Vue qu'on ne peut pas ignorer : dans les premières années, Vue était vraiment une innovation ; mais n'importe quel UI Stack rencontre des problèmes d'écosystème une fois que celui-ci a grandi, c'est-à-dire un certain nombre de compromis faits au nom de la compatibilité ? Ce problème est particulièrement flagrant sur React, et le Vue d'aujourd'hui ne fait pas exception. Quand ce genre de contenu17 commence à apparaître, je considère généralement que c'est du Slop -- sans pouvoir dire pour autant que c'est mauvais, car dès lors que Nuxt devient le prochain framework « de qualité entreprise », le premier critère n'est plus la justesse technique ni l'élégance, mais une certaine rétrocompatibilité assortie de stabilité (x
Donc Evan You n'a en réalité rien fait de mal, on pourrait même dire qu'il fallait bien que quelqu'un endosse le rôle du , et il se trouve qu'il était le mieux placé pour cela : il a simplement fait ce qu'il fallait…
Solid
Le framework SolidJS est excellent et son cœur est suffisamment stable, mais personne ne veut lui écrire un écosystème. Solid est au fond le Rust d'il y a quelques années, sauf que Rust, lui, a tenu bon ; Rust connaît lui aussi, en programmation, cette situation d'écosystème pauvre où il faut tout réinventer soi-même, mais la chaîne d'outils de Rust permet de réinventer la roue sans friction et de faire tourner la plus petite unité utilisable — réinventer la roue n'est donc pas si difficile, au fond ? Malheureusement, SolidJS n'a pas encore franchi cette étape et s'est heurté à l'hiver de l'IA ; ce sera particulièrement difficile à partir de 26. Le cœur de Solid 2.0 est déjà très bon et très stable, mais la périphérie n'a jamais pris son élan. L'idée est parfaitement juste, elle est seulement née au mauvais moment. Et même les quelques projets périphériques n'existent que sur un ou deux points épars (TanStack a par exemple un Router expérimental pour Solid), sans jamais former un écosystème complet. Cela le place aujourd'hui comme demain dans une position ingrate : pas d'écosystème complet, et à l'ère actuelle d'explosion des Agents, tant que la boule de neige n'a pas commencé à rouler, un Agent ne le choisira pas en priorité. Et si même l'IA ne le privilégie pas, cela signifie encore moins d'occasions de recevoir des contributions, surtout à une époque où même Linus Torvalds se met à . Je trouve cela vraiment dommage pour Solid : bonne idée, choix techniques justes, implémentation élégante — mais quel dommage d'être né au mauvais moment…
«Le moi d'avant n'aurait peut-être pas vu là un problème, tout écrire soi-même n'aurait rien eu de gênant. Mais hélas, le moi d'aujourd'hui ne pense plus ainsi»
Le point d'équilibre
Sur cette voie, celui qui se tient exactement au point d'équilibre parfait, c'est en fait le seul Svelte. Il a un meilleur écosystème que SolidJS (et une meilleure année de naissance), ses performances ne sont guère en retrait, et parfois même, grâce à son compilateur, il se révèle souvent plus rapide et meilleur que SolidJS ?! Bien sûr, quelqu'un objectera ici que dès que le nombre de pages atteint un certain ordre de grandeur, ou plus l'utilisateur navigue, le coût en ressources18 de Svelte face à SolidJS ne cesse de croître ? Que les requêtes réseau se multiplient ? Eh bien, en théorie c'est vrai, mais je pense qu'il faut regarder les pages réelles : sur 99,9 % des sites personnels ou de contenu, les visiteurs ne restent pratiquement jamais longtemps, et une longue visite peut même être considérée comme un cas très rare. Au moins sur mon site, les enchaînements de clics en profondeur sont pour l'instant extrêmement rares, donc « feuilleter beaucoup de pages = Svelte y perd davantage » ne me pose pas de problème. Mais ce n'est pas la raison pour laquelle j'ai choisi Svelte ; le cœur du sujet reste l'AST du markup.
Il y a aussi le Dashboard, ce genre de chose qu'on ouvre page après page, et là cette statistique ne sert à rien -- mais après tout, les utilisateurs d'un Dashboard restent bien moins nombreux que le vrai public du contenu. Donc « un très grand nombre de pages consultées fait grimper le coût », je considère que c'est au fond un faux problème ; du moins pour l'instant, peut-être qu'un jour je verrai les choses autrement, mais à l'ère de l'information effrénée qui est la nôtre, j'ai du mal à nier que ces utilisateurs sont extrêmement rares ; et même si quelqu'un explore en profondeur par curiosité et devient un lecteur fidèle, cette exploration n'a très probablement lieu qu'à la première visite, ensuite il repasse simplement voir les nouveaux articles ; en termes de rendement statistique, ce cas extrême n'est tout simplement pas proportionné. Je préfère donc croire ceci : dans la plupart des cas, Svelte offre de meilleures performances globales.
Par rapport aux trois frameworks ci-dessus, Svelte n'a pas seulement une situation et un positionnement plus adaptés : il est aussi très proche de la sensation d'écrire du HTML brut ; à l'époque où Svelte 3 commençait tout juste à se répandre et où l'extension de fichier .svelte venait d'être fixée, c'était même la recommandation officielle tant que l'extension VSCode officielle n'existait pas encore, au point qu'on trouve encore aujourd'hui cette phrase sur la page de l'extension Svelte :
Si vous avez ajouté
"files.associations": {"*.svelte": "html" }à vos paramètres VSCode, supprimez-le.
Par ailleurs, Svelte m'offre encore bien des avantages. D'abord, il ne pousse pas l'interface vers JSX accompagné d'un tas de JS function19 ; ensuite, <style> est un produit de la compilation et non runtime : le premier laisse la structure dans markup AST, le second permet au CSS d'entrer directement dans l'IR, et Tailwind se colle justement aux deux (
Boîte noire
Comme je l'écrivais dans l'article précédent, Seam est d'abord un protocole, pas un runtime SSR de plus. Au build-time, on produit le HTML skeleton porteur de slot ; au request-time, on ne fait que injection ; if / each / match sont eux aussi des nœuds de protocole20
Dommage seulement que j'aie choisi React pour attaquer l'interface : malheureusement, capturer la structure de la page et en produire des variantes est très difficile sur React, et modifier le compilateur représenterait autant de travail que d'en réécrire un…
Si on ne change rien, on peut encore traiter le compilateur React comme une boîte noire et exécuter pour de vrai tout un tas de render + diff pour deviner la structure et s'en servir ; des décisions finies comme nullable / enum restent énumérables, mais dès qu'on tombe sur un prédicat du genre price > 10, l'espace des valeurs du type ne se laisse tout simplement plus multiplier jusqu'au bout. En V1, dans ce cas, on te laisse seulement fournir les mocks à la main, par exemple en découpant en trois selon price < 10 / = 10 / > 10 ;
Mais c'est précisément là que le bât blesse(?) Pour réussir cette découpe en 3 exacte, il faudrait que tu saches que le point de coupe est à 10, or le compilateur React est opaque pour moi, on ignore quelle est la condition. Du coup, le prix du TypeSafe21 devient : c'est l'utilisateur qui fait le travail du compilateur.
En théorie, ça ne devrait pas retomber sur l'utilisateur, sinon le résultat, c'est des escape partout22, et CTR23 ne couvre toujours que ces quelques nullable / enum, la découverte de structure revient à ne rien faire, et je ne sais plus trop quel intérêt il reste à tout ça.
Mais cette fois, il ne s'agit pas de changer le protocole lui-même. Ce que je disais au départ à propos de skeleton, slot et injection peut très bien rester en l'état, et le backend peut toujours ne pas exécuter de code d'interface ; ce qu'il faut changer, c'est la couche située en amont du protocole, la question de savoir comment la page de réponse est compilée en ces nœuds.
Et là, je ne peux que remercier Svelte pour le plus beau cadeau qu'il m'ait fait -- markup
Parce que la structure du balisage, je n'ai absolument pas besoin de la deviner !!!
L'AST visible
Rien que parce que le balisage est vraiment du balisage, la structure de la page a toutes ses chances de devenir quelque chose qu'un compilateur peut lire, plutôt qu'un arbre de composants produit par un tas de JS exécuté à l'exécution. Je veux juste dire : pourquoi une interface parfaitement normale devrait-elle absolument pouvoir être dessinée par exécution arbitraire ?Je ne nie pas ce point de vue, mais ce que je veux dire, c'est qu'au moins 97 % du temps, le dessin par exécution arbitraire ne sert à rien, voir ici pour le détail
Prenons un contre-exemple, dans React
function Card({ user }) {
return user
? <div className="card">
<Avatar user={user} />
<span>{user.name}</span>
</div>
: null
}Au fond, il s'agit toujours d'exécuter une fonction JavaScript, puis de regarder ce qu'elle renvoie. Même si le compilateur est très intelligent, le modèle de base auquel il fait face reste : JS execution → JSX expression → element tree. Or, dans Svelte, les choses sont très différentes.
{#if user}
<div class="card">
<Avatar {user} />
<span>{user.name}</span>
</div>
{/if}Ce qu'elle donne directement au compilateur, c'est
Component
├── IfBlock
│ └── Element div
│ ├── Component Avatar
│ └── Element span
│ └── Expression user.namePour une application Kit ordinaire, c'est déjà très bien, mais pour une chose comme Seam, qui veut décomposer server rendering en compile-time skeleton + CTR + SSR fallback, la portée est vraiment bien plus grande que ça.
Parce que cet arbre m'a donné deux niveaux d'information à la fois : d'abord le dynamisme des valeurs → slot, mais cela, la V1 savait déjà le faire ; le plus important, c'est en fait le second, qui m'a donné le dynamisme de la structure → des nœuds de flux de contrôle au niveau du protocole. Plutôt que de deviner la boîte noire du compilateur de React, le compilateur de Svelte, lui, expose carrément le vrai résultat frontend pour que je puisse le lire, par exemple {user.name} relève du premier, et seul {#if user} relève du second.
Par ailleurs, les if / each / match de l'ancien protocole restent valables. Mais le produit cartésien décrit dans l'ancien billet, je crois qu'il faut le restreindre : sa condition de validité, c'est en réalité l'espace des branches, pas l'espace des valeurs. Pour des décisions finies comme nullable / enum / bool, les combinaisons sont en nombre fini, et l'énumération payante de compile-time tient mathématiquement ; en revanche, les string / number / timestamp de JTD ne sont pas énumérables en eux-mêmes, et les branches découpées par des prédicats comme price > 10, inventory < 5, items.length === 0 restent introuvables par le seul produit cartésien des valeurs de types ; dans sentinel, quel que soit le nombre qu'on y mette, on part toujours du même côté.
Si l'on veut vraiment les ramener à des décisions finies, il faut d'abord convertir derive en bool / enum, et cette étape, JTD ne l'offre pas gratuitement (rires). Donc V1 sound seulement sur nullable / enum, pas sur l'espace de valeurs d'un champ quelconque ; le vrai problème d'ingénierie, c'est plutôt que ces blocs-là sont devinés par diff, et que IR ne peut pas s'expliquer lui-même.
Abaissement
Mais à l'ère de Svelte, ces nœuds peuvent être générés directement depuis l'AST, et la raison n'est pas de grappiller quelques ms en changeant de stack, c'est plutôt de faire passer le flot de contrôle de l'observation24 à lowering -- la découverte de la structure revient à l'AST, les décisions en nombre fini passent par des nœuds de protocole, et il n'y a plus à démultiplier N exemplaires de HTML ; les types servent à contraindre payload, pas à découvrir l'arbre. Au bout du compte, dans Svelte, les values à ce Layer ressemblent donc à ceci ↓
STATIC
<div class="card">
DYNAMIC
{user.name}
STATIC
</div>Ce genre de DYNAMIC n'a bien souvent pas besoin du niveau de React, ce「vrai rendu25」; garder le slot suffit.
Plus finement encore, Svelte peut même découper un seul element de cette façon
structure: static
attributes: static
text node #0: dynamicC'est exactement ce dont Seam a le plus besoin, et ce qu'il préfère. Dans React, pour savoir si attributes est static, il ne reste en gros que l'énumération par force brute : on fait passer toutes les entrées possibles, si attribute ne change pas c'est static, et si ça change on lui colle un slot.
Les branchements structurels, c'est encore pire : même savoir si ce bout de DOM est là ou non oblige à rendre deux fois puis à faire diff, alors que IfBlock de Svelte fait passer cette seconde chose de l'observable au générable, et c'est là le vrai saut qualitatif.
Mais en réalité, dit comme ça ce n'est pas tout à fait exact, ou c'est trop simplifié ; pour être un peu plus précis : un seul element peut encore se découper davantage ; et attributes n'est pas forcément static dès le départ, tout dépend de la façon dont tu l'écris.
class="card" → attributes: static
class:active={on} → name static, value slot
{...rest} → opaque, escape hatch
text node {user.name} → slotIci, par exemple, que la première ligne parte dans le skeleton ne fait pas débat, mais c'est la deuxième ligne qui montre où Svelte est vraiment agréable : class: / style: laissent le nom attr dans l'AST, seule la valeur devient à part slot, sans avoir à marquer tout element comme dynamique. La troisième ligne ne diffère pas fondamentalement du {...props} de React : Seam la traite simplement comme un hatch explicite et n'a plus à faire semblant de l'analyser ; dans React, le className="card" littéral n'a lui non plus pas besoin d'être énuméré. Ce qu'il faut énumérer, c'est « est-ce que cet attr change si on passe un autre jeu de props ». Ce qui manque justement à Svelte, c'est cette dernière forme de devinette — tant que le nom reste dans l'arbre, pas besoin de rendre deux fois pour le savoir 🫠
Bref, React force Seam à reconsidérer beaucoup de choses comme un problème d'exécution, par exemple ↓
const Wrapper = cond ? A : B;
return Wrapper({
children: foo.map(renderItem)
});Ou bien
return foo && bar
? getLayout()(data)
: something();Bien sûr, le React Compiler moderne sait analyser une bonne partie de tout cela, mais le bricoler en profondeur voudrait dire devoir le maintenir indéfiniment ensuite, et ce coût est tout simplement énorme, en énergie comme en temps... Si on touche plutôt à Svelte, ici je n'ai pas l'intention de forker le compilateur, en théorie svelte/compiler expose déjà l'AST.
Ce que Seam doit faire, c'est lire cet arbre puis générer sa propre RI ; la génération de code DOM / SSR officielle peut très bien rester en repli, en théorie il n'y a que l'abaissement de la RI à maintenir, pas Svelte en entier, donc la direction générale concorde et les conflits seront rares — mais ce qui concorde, c'est que la structure est visible à la compilation, pas le modèle d'exécution ; c'est précisément pour ça que le SSR de Svelte / Kit exécute encore la fonction render générée à chaque requête ; alors que dans mon Seam, je préférerais que request-time n'exécute pas cette fonction et se contente d'interpréter le protocole (slot + if / each / match)
Dans le choix de Svelte, il y avait en fait une autre considération : étant admis que Next.js et React sont déjà entièrement26 contrôlés par Vercel et devenus le framework du marché, si Seam promet d'être compatible avec la sémantique de React, alors mon architecture devra finalement autoriser arbitrary JavaScript → determine tree structure.
L'IR de Seam devrait donc être extrêmement conservatrice(x). Au final, ça finirait très probablement ainsi : ce qui est analysable statiquement est optimisé, ce qui ne l'est pas passe en SSR. Ce serait certes encore une page mêlant CTR et SSR, mais le problème se situe justement là, dans ce « non analysable », car dans le monde de React il y a bien trop d'endroits qu'on ne peut pas déterminer avec certitude…
Si l'on ne touche pas au compilateur React, on reste coincé dans la même situation qu'en V1 : traiter le compilateur comme une boîte noire et deviner à l'aveugle ce qu'il produit. Autant dire que c'est extrêmement fragile, inélégant, et que les cas limites se mettent vite à voler dans tous les sens.
Inverser
Donc si je dis maintenant que le langage de composants de Seam, c'est Svelte, alors le compilateur peut même procéder à l'envers : d'abord structure known par défaut, expression dynamic localement, escape hatch là où c'est explicite, puis runtime, et seulement quand rien ne marche vraiment, un repli sur le SSR en dernier recours.
<script>
let { product } = $props();
</script>
<article class="product">
<h1>{product.name}</h1>
<div class="price">${product.price}</div>
<button>Buy</button>
</article>On peut tout à fait se représenter Seam à peu près comme ceci
<article class="product">
<h1><!-- slot 0 --></h1>
<div class="price">$<!-- slot 1 --></div>
<button>Buy</button>
</article>Alors en théorie slot0 = product.name, slot1 = product.price. C'est exactement la résidualisation sur laquelle je travaillais déjà : l'idée centrale est que la page entière n'a pas besoin de passer par le rendu côté serveur, on ne calcule que le résidu réellement dynamique.
Emplacements et branches
Mais le vrai gain, en passant de React à Svelte, se situe en fait à la couche suivante ; c'est-à-dire qu'en cas de changement de structure, on n'a plus besoin de rendre deux fois pour deviner, puisque la structure est déjà fixée à la compilation et qu'il suffit de générer directement l'IR correspondante
<article class="product">
<h1>{product.name}</h1>
<div class="price">${product.price}</div>
{#if product.available}
<button>Buy</button>
{:else}
<p>Sold out</p>
{/if}
</article><article class="product">
<h1><!-- slot 0 --></h1>
<div class="price">$<!-- slot 1 --></div>
<!--seam:if:product.available-->
<button>Buy</button>
<!--seam:else-->
<p>Sold out</p>
<!--seam:endif-->
</article>La valeur reste slot, et la branche est le bloc if de l'ancien protocole. Mais dans la version React, il faut plusieurs diff pour obtenir ce bloc ; alors qu'en Svelte, on peut ici le dériver et le générer directement à partir de IfBlock.
Échappement
Ce n'est évidemment pas pour dire que JSX en est incapable. Simplement, quand on l'écrit pour de vrai, presque personne ne laisse la structure à un endroit bien visible de l'arbre de branchements : en gros, tout le monde finit par écrire quelque chose comme ci-dessous (lol)
const price = formatPrice(product);
const body = product.available
? getAvailableView(product)
: getSoldOutView(product);
return <Layout>{body}</Layout>;Et il y a encore un point important : même si tu sais écrire comme ça, tant que le gros des troupes de React n'écrit pas comme ça, et que les LLM entraînés n'écrivent pas comme ça, le coût de migration et le coût de développement ultérieur augmenteront à coup sûr de façon exponentielle.
Mais avec Svelte, ici, c'est encore une autre histoire
UI structure ≈ template AST, JavaScript ≈ values + behavior, c'est déjà le quotidien ici
C'est même l'approche que Svelte recommande le plus officiellement. Cette différence est structurelle pour la façon dont Seam extrait l'IR. Mais ne comprends pas pour autant que Svelte n'aurait aucune structure dynamique : il possède bel et bien une échappatoire correspondante, et c'est même l'une de ses syntaxes principales
{@render children()}
<svelte:component this={layout} />
<svelte:element this={tag}>
{@html html}Wrapper = cond ? A : B ne disparaît pas ici comme par magie : il s'est simplement resserré depuis « n'importe quelle expression JS » jusqu'à ces quelques nœuds. Pour Seam, cela devient très simple : il suffit de suivre par défaut l'abaissement de l'AST ; ce qu'on voit statiquement dans un trou appartient exactement à celui à qui cela appartient. Le cas le plus courant est {@render} employé comme compose (layout et Card enveloppant children relèvent tous de ce type), tandis que <svelte:component> / <svelte:element> / {@render} dynamique sont, eux, des hatch bien identifiés et passent dans runtime ou SSR fallback ; quant à {@html}, on peut reprendre tel quel le raw HTML slot que j'avais déjà conçu.
Donc l'avantage réel n'est pas que « la structure est toujours connue », mais que « la structure inconnue dispose d'une liste de référence » (x
Dans React, l'inconnu, c'est tout le JS ; dans Svelte, l'inconnu se limite aux ouvertures laissées par les quelques échappatoires évoquées plus haut, rien de plus. Une fois cela mis en place, l'étendue de ce que le CTR doit analyser diminuera nettement, c'est certain.
CTR
Pouvoir utiliser Svelte ne veut pas forcément dire pouvoir utiliser le CTR, mais j'ai ici un critère de référence ↓
C'est ainsi que la référence statique devient compose ; hatch passe toujours par la voie du dessous, et l'on en déduit que l'unité du CTR est le graphe de composants statique, et non le fichier isolé. La plupart sont structure known par défaut, une minorité explicitement escape hatch, et SSR fallback ne se rencontre presque jamais en pratique. Tout le reste, absent du tableau, est traité sans exception comme opaque -- ainsi il n'y a rien à deviner. CTR n'est que le execution model sur ce sous-ensemble, et même si je passe à Svelte je ne compte pas garantir toute l'étendue de Svelte27 (x)
CSS
Un autre gain énorme, c'est en fait le CSS : dans Svelte, on peut l'écrire ainsi :
<div class="card">...</div>
<style>
.card { border-radius: 8px; }
</style>Ici, quand Seam compile un composant, il dispose en même temps déjà de markup dependency + script dependency + style dependency, et le style est un artefact statique. On peut alors déterminer de façon très agressive
On peut même aller plus loin et façonner le processus ainsi ↓
L'ensemble du processus ci-dessus ne requiert aucune intervention d'un moteur d'exécution JS.
Pas de CSS-in-JS
Le CSS-in-JS, je ne l'accepte pas davantage, et ce n'est pas que React m'oblige à prendre en charge une écriture comme celle qui suit
const Button = styled.button`
color: ${p => p.primary ? "red" : "black"};
`;OU
<div css={theme => ({
color: theme.colors.primary
})}>Sauf que cette structure-là, on pouvait déjà la contourner avec Tailwind dès la V1. La principale raison de ne pas brancher CSS-in-JS, c'est qu'une fois branché, le CSS redevient le résultat d'une exécution JS, et il faut alors faire revenir le runtime de styles, le registre, la collecte SSR, le hachage, la cohérence de l'hydratation, le contexte de thème, l'insertion, l'ordonnancement, la déduplication — tout. Un framework ordinaire peut avaler ça ; mais Seam, elle, c'est autre chose ! Ce que je veux supprimer, c'est précisément ce genre de travail côté serveur et à l'exécution, et il n'y a sûrement aucune nécessité, par politesse envers l'écosystème, de réinviter ce qu'on vient tout juste de chasser (ça n'a absolument aucun sens
Et puis, à l'époque où CSS-in-JS est apparu, il réglait précisément ce dont Tailwind CSS et Motion se chargent aujourd'hui. Si Tailwind CSS et Motion suffisent désormais, et le font même mieux et plus élégamment, pourquoi revenir en arrière ? Comme je le disais plus haut : « une fois qu'un écosystème s'est installé, les compromis28 apparaissent partout, et il est rare qu'un refactor cassant vienne tout renouveler. » Alors puisque Seam n'a pas encore d'écosystème, pourquoi ne pas être radical jusqu'au bout ?
Mais je ne sais pas si « radical » est le mot juste ici. Ce qui plaide pour : par rapport aux frameworks classiques, j'abandonne effectivement le lourd environnement d'exécution JS côté serveur (Node ou Bun). Ce qui plaide contre : quelque chose comme le CSS-in-JS, on peut très bien l'abandonner complètement sans que cela pose vraiment problème — j'y vois plutôt désormais . Les bibliothèques CSS-in-JS autrefois les plus connues : Stitches a officiellement annoncé en juin 2023 qu'elle n'était plus activement maintenue, styled-components est entrée en sommeil en mars 2025, et Emotion en est pour l'essentiel au même point.
Il faut alors saluer le <style> de Svelte : CSS → compile-time artifact ne fait pratiquement plus débat dans Svelte (du moins c'est mon avis, et si tu n'es pas d'accord, c'est toi qui as raison)
Tant que ce n'est pas JS → execute → generate CSS → collect → serialize → hydrate, l'écart sera énorme, et surtout les dépendances CSS pourraient peut-être entrer directement dans la Seam IR 🤔
Par exemple, la compilation des composants :
ComponentIR {
skeleton,
dynamic_slots,
css,
client_behavior,
server_dependencies
}On remarquera qu'il ne s'agit pas d'un second modèle de rendu : dans la V1, skeleton plus dynamic_slots, une fois compilés, donnent exactement le HTML de l'ancien protocole ; simplement, la façon dont skeleton plus dynamic_slots sont obtenus m'est devenue plus transparente.
Supposons que l'on procède comme ci-dessous ↓
<script>
let { name } = $props();
</script>
<div class="user">Hello {name}</div>
<style>
.user { font-weight: 500; }
</style>Alors on finira forcément par y arriver
ComponentIR
HTML: <div class="user svelte-x">Hello SLOT(0)</div>
CTR: slot(0) = props.name
CSS: .user.svelte-x { ... }
Client: none
SSR: noneMais attention : ce que tu vois ci-dessus n'est qu'une feuille, et une coupe plutôt idéale -- pas de sous-composants, pas de branchements de structure, pas de client. Le skeleton au niveau de la page est composé à partir du graphe statique de composants ; dès qu'un $state ou un hatch apparaît dans le sous-arbre, le client / rendu côté serveur dans la représentation intermédiaire n'est plus none.
Mais on est déjà très proche de l'état idéal que je veux atteindre pour Seam : un composant Svelte ne correspond plus à « un renderer qu'il faut exécuter », il devient une ressource que le compilateur Svelte 5 peut démonter. Rien que d'y parvenir constitue déjà, pour moi, une très grande amélioration conceptuelle.
La propriété
Par ailleurs, « ne pas pousser l'état global » a pour Seam une portée plus profonde que le confort de développement. Je suis sûr que dans un projet Next.js, ou dans n'importe quel framework full-stack React, tu as déjà croisé le pavé ci-dessous ↓
<ThemeProvider>
<AuthProvider>
<QueryClientProvider>
<I18nProvider>
<RouterProvider>
<App />
</RouterProvider>
</I18nProvider>
</QueryClientProvider>
</AuthProvider>
</ThemeProvider>Bien sûr, ce n'est pas sans solution : cette imbrication de Providers en "pyramide" peut aussi s'écrire sous forme de Compose Providers. En gros, cela donne à peu près ce qui suit.
Concrètement, on balance dans un tableau tous les Provider auparavant imbriqués, on définit une fonction de composition, et il ne reste plus qu'à avaler cette seule couche. Mais pour moi, c'est en réalité se mentir à soi-même : au fond, on ne fait que "cacher" l'imbrication, pas la supprimer. L'imbrication visuelle devient une imbrication logique, en un sens c'est du , de la pure consolation.
const AppProviders = composeProviders([
ThemeProvider,
AuthProvider,
QueryClientProvider,
I18nProvider,
RouterProvider,
]);
function composeProviders(providers: React.FC<{ children: React.ReactNode }>[]) {
return ({ children }: { children: React.ReactNode }) =>
providers.reduceRight(
(acc, Provider) => <Provider>{acc}</Provider>,
children
);
}
function Root() {
return (
<AppProviders>
<App />
</AppProviders>
);
}Un projet React finit vraiment très facilement, voire à 100 %, par ressembler à ça, et cela fait apparaître un problème de couture très pénible : « quelle est au juste l'entrée d'un composant ? » En surface, on peut dire que c'est <ProductCard product={product}/>, comme s'il ne dépendait que de product lui-même ? Mais en réalité
depends on:
product
theme
locale
queryClient
router
auth
featureFlags
...Du coup, le component dependency graph redevient implicite. Pour du traditional SSR, ce n'est pas bien grave : au pire, on « refait tourner l'arbre React en entier ». Mais ce que Seam veut finalement demander, c'est si ProductCard peut faire du CTR de façon autonome, et là ça devient vraiment pénible. On peut même dire qu'il faudrait changer tout le comportement du compilateur React, la DX et les habitudes des utilisateurs -- alors à quoi bon...
Ceux qui utilisent Next.js se moquent29 déjà des performances, et je l'affirme en toute responsabilité ; ils paient bien plus pour l'écosystème de Vercel, car si tu utilises Next.js sans l'héberger sur Vercel, tu n'en tires absolument aucun avantage 😡
Propriété locale
Svelte, à l'inverse, permet très facilement de faire émerger une propriété locale ;
<script>
let { product } = $props();
let quantity = $state(1);
</script>Du point de vue de Svelte et du Seam Compiler, c'est très confortable.
Après tout, « entrée : product + état local du client : quantity »la propriété est vraiment devenue très claire
product.name → server CTR
quantity → client
button onclick → client
rest → staticMais ce n'est là qu'un comportement par défaut, pas vraiment une garantie ; car rune module dans getContext('theme') et .svelte.ts reste une entrée implicite, et QueryClient ne disparaît pas non plus comme par magie. Je crois que Seam adoptera la même attitude que pour la liste de hatch plus haut : ce qui se voit est inscrit dans l'IR (comme dépendance server ou propriété client), ce qui ne s'analyse pas est traité comme opaque -- mieux vaut de toute façon ne pas faire comme si « les Provider n'existaient pas en Svelte ». Mais rien n'est sûr, peut-être qu'en cherchant encore un peu je trouverai une façon de gérer les Provider
À l'époque de React, le plus gros problème de Seam était que l'IR suivait Component → execute renderer → get output ; du coup, ce que j'appelais finalement seam compiler devenait en réalité, d'une certaine manière, un SSR anticipé, ou bien on peut tout à fait le qualifier de
« couche universelle d'orchestration SSR »
Même protocole
On peut donc dire désormais que l'UI, c'est Svelte, mais le protocole seam reste un protocole, toujours centré sur slot et injection, reste à savoir si JTD convient vraiment, il faut que j'y réfléchisse davantage ; du point de vue des décisions d'exécution, tout est maintenant entièrement stratifié, les slots sont residual-isés, Rust / Go / TS server n'ont plus qu'à remplir à l'aveugle sans rien comprendre au code des composants, du zéro JS véritable ; en parallèle, s'il reste des dérivations pures dans le composant, on peut envisager d'ajouter un generated JS / QuickJS runtime, (attention, ici ce sont des données qui tournent, pas de l'UI)
Seul ce qui porte hatch sera SSR fallback, donc tant qu'un vrai SSR n'est pas nécessaire, le backend peut ne pas exécuter le code d'UI du tout -- et il n'est donc plus lié à Node OU Bun. Cela dit (le Go Server ne sera plus maintenu, faute d'énergie de ma part. Si une âme charitable a du temps, elle peut le reprendre et le maintenir sur la durée : ce n'est pas que l'architecture ne supporte plus l'indépendance vis-à-vis du backend)
Autrement dit, je n'ai coupé que l'UI, je prends tout le monde, ce genre de tâche ingrate et laborieuse (?) La partie qui ne change pas peut toujours servir de « Rendering Protocol », le backend n'est pas limité à JS/TS.
Sémantique des runes
Un autre point très important : les runes de Svelte 5 s'accordent très bien avec Seam, même si Svelte 5 commence à ressembler un peu plus à du JS.
let count = $state(0);
let doubled = $derived(count * 2);Mais ce JS n'est pas l'autre JS30, la différence avec useState / useMemo / useContext / useEffect est la suivante : les runes restent des compiler-recognized semantics, donc Seam peut savoir que $state appartient au client, que $derived est dérivé et que $effect relève du client runtime ; ce n'est pas du même ordre de grandeur que const foo = someLibraryHook().
Origine des données
Un point très important ici : « savoir le reconnaître » n'équivaut pas à « pouvoir faire du CTR », et surtout, il ne faut pas supposer d'emblée qu'un $derived finira dans skeleton. Le count * 2 ci-dessus en est justement un contre-exemple, car sa dépendance est $state : c'est donc un produit dérivé côté client. Voyons à quoi ressemble un $derived qui, lui, peut vraiment faire du CTR :
let { product } = $props();
let price = $derived(formatPrice(product.price));product vient de $props, et formatPrice doit en plus être pure et visible ; branché sur le tableau précédent, cela donne ceci.
$state / $effect / onclick → client
$derived
deps ⊆ server data, pure, visible → CTR derive
deps include $state → client
impure or opaque callee → QuickJS or SSR fallback
markup → follow the table above; runes are not a free CTR passDessiné comme ça, c'est peut-être plus facile à comprendre(?)
Bref, ce qui rend Runes agréable, c'est que la propriété est marquée au niveau de la syntaxe, pas le fait que $derived soit CTR par nature. Le seul critère reste au fond celui-ci : d'où viennent les données affichées par le chemin ci-dessous, sont-elles pures, sont-elles visibles (mdr)
Et j'ajoute encore à côté une règle <style> → static CSS dependency graph, pour que toute la vision du monde soit parfaitement cohérente : le HTML, c'est la structure, le CSS un artefact de style, le JS forcément du calcul et du comportement.
Et non pas ce vers quoi le React moderne glisse si facilement : tout en JS31. Pour React lui-même, ce n'est peut-être pas une mauvaise chose, mais hélas Tout est JavaScript est précisément le monde le plus défavorable à la décomposition agressive à la compilation que je veux faire
Qu'est-ce qui sera livré ? 32
Après tout ce discours, qu'est-ce qui sera vraiment mis en œuvre au bout du compte (rires) ? D'abord, sans le moindre doute, le passage de la pile d'interface à Svelte, et on peut s'attendre à ce qu'une fois la chose faite, l'observabilité du compilateur augmente nettement. La raison principale reste que Svelte oblige et encourage à garder structure dans markup AST plutôt qu'à l'enfouir dans un flot de contrôle JS arbitraire, ce qui élargit directement la marge d'analyse de skeleton / CTR et fait redevenir CSS un build artifact au lieu d'un runtime concern.
Cela veut dire que Seam a une chance de faire des dépendances CSS au niveau du composant, du CSS critique, du tree shaking et du CSS chargé à la demande, sans avoir à maintenir en plus le style runtime de server / client.
Et puis la propriété des composants devient enfin nette ; moins de Provider / Context / global runtime dependency, cela veut dire que j'aurai l'occasion de répondre complètement : quelle valeur vient du serveur, à quel endroit un passage par CTR est possible, qui a besoin de client boundary (quel état appartient au client), et si ce component a vraiment besoin de hydrate…
Parmi ces trois, les deux premiers décident directement si CTR restera une simple optimisation ou pourra vraiment devenir le principal modèle d'exécution33 de SeamJS. C'est bien pour cela qu'abandonner React n'est pas forcément, à mes yeux, un compromis technique : à l'inverse, cela peut aussi être une rupture structurelle. Bref, je le redis
Dès qu'un écosystème s'est installé, tout finit par céder au compromis,
et rares sont les refontes en rupture qui apportent une vraie innovation
Puisque je n'ai pas encore vraiment décollé, j'ai bien sûr toute latitude pour tout casser sans avoir à penser, pour l'instant, au coût de migration ; et j'espère vraiment qu'un jour, après d'innombrables ruptures, je trouverai la place qui m'appartient vraiment.
**Même si au bout du compte je suis le seul à m'en servir, ce sera quand même mon « Expérience 🧪 » à moi