Today marks exactly one month since I moved to the US; after landing in NC and getting settled into daily life, I have pretty much finished exploring the surrounding area and the nearby sights, so now I have time on my hands. But I am not exactly the idle type, and the moment I have nothing to do I go looking for something to do picking the blog back up counts as one, right. Unfortunately shipping over here in the US is not very convenient, so I cannot draw boards for now, which leaves me with coming back to write some software to pass the time, and while I am at it, maybe pick up the project I dropped a few months ago and finish it?

But before I start ranting about other people, I probably ought to fill in a bit of background on seam & SeamJS first (x — what on earth is this thing after all, the last post was months ago, so odds are you've forgotten it all, and I can't be bothered to update the README.md on Github yet

Rendering as a Protocol

UI should be described, not executed.

So, put simply, Seam1 is first and foremost a protocol, not yet another SSR runtime. That means build time emits an HTML skeleton carrying slot, and runtime / request time only stuffs values into those slot; if / each / match are likewise protocol nodes baked into the skeleton. So by the time a request arrives, not a single UI component has been executed, and the only thing the server has to do is -- which means it naturally does not have to be Node or Bun anymore. A Rust / Go backend can fill in a page of HTML just as well, how hard could that possibly be?! Though honestly the JS runtime was never completely killed off later either; what I mean is you can drop it, provided you accept some limitations Either way, the overall performance gain is absolutely exponential, because achieving most SSR effects no longer requires running the UI render at runtime / request time...

Request-time model? 2

Starting around the second half of this year, I moved this site entirely off TanStack Start React and fully port over to SvelteKit Svelte. Once the migration was done, I found it genuinely good: the performance overhead of ssr dropped by roughly an order of magnitude, hydration cost is tiny, it runs comfortably, and the framework's compile-first philosophy is excellent and very lightweight. But the Kit side always had a few things that left me unsatisfied. Most of them can be solved with plugins it is glued together out of Vite, after all, yet there are always some parts that cannot be touched; that is most likely my own problem, though, because the places seam wants to change probably conflict fundamentally with every existing meta framework, in essence I am challenging their execution model (running the render function on every request), their data boundary (load and UI mashed together), and their backend capability (welded to a JS runtime).

Pseudo SSR 3

No doubt someone is going to ask here: isn't this just Astro / Qwik / Marko? : in language shape Seam is closest to Marko, in shipping little JS by default it is closest to Astro, and in not doing replay hydrate it is closest to Qwik; but their request-time is still running a UI program island, each with its own render, that Qwik pass in SSR, the JS template functions compiled out by Marko, all still executing a chunk of renderer per request.

So with those thousand {#each} entries, I'm still fundamentally assembling HTML. It's not that I don't generate a document4; the difference is that what I assemble is an already self-contained protocol, rather than regenerating the component Tree. Seam wants ASTIRHTML with slot; at request time it only interprets the protocol and adds the injection for the dynamic-data regions, so there's no need to run the UI render part of the functions anymore. That means any backend can fill in that last layer, which is already residual-ified, and that also solves the vendor lock-in to JS5 problem. So really, my overall direction isn't on the same line as the existing work above (x)

Adopt

Seam does hydrate, but not the traditional way, and not the Qwik way either. Traditional hydration has the client render once more and diff against DOM; Qwik SSRs out HTML + component state, and the client resumes without replaying; but Seam's request-time does not even need to run the UI renderer: the hydration skeleton, the first screen, and the hydrate data all come from the same injection. What the client receives is an already-correct DOM + the same data, and it attaches events and client state according to the compile-time IR adopt, which is neither replay nor, naturally, resumeing a component tree.

This is exactly why the mismatch6 class of "render on both sides, then compare" has gone from being the default to being an illegal state. I am not claiming the implementation is 100% there; it is that this kind of divergence is invalid in the model: if it shows up, it is a bug in my compiler / injector implementation that can be fix, the typical examples being (the skeleton did not survive the HTML parser, escaping does not match, IR and skeleton out of sync).

Of course skeleton itself has to be parse-stable7; stuffing comment into table at random counts as an implementation that did not handle things properly, and should not be pinned on my model. Build-time computes the hydration mapping (what is static, what is a slot, what gets bound), not a premature freezing of the data, yet it does fix the data type of this slot for good. The data values themselves can still be swapped freely at request-time; it is just that HTML and payload must be the same copy.


Same graph?

Granted, Astro's first screen is a document too, and you can still ClientRouter afterwards, but the difference is that its dynamic parts remain islands that each render on their own; ClientRouter is at heart closer to document morph than to one component graph that is still alive after hydration. So Astro's stack just is not suited to this. Nothing wrong with Astro itself, it simply was not designed to be used this way. You could of course say there is the View Transitions API8 to use, then I would have to ask you whether the View Transitions API can put food on the table, and whether it lets you wrap Motion animations9 across pages... I have wandered a bit off topic;

Either way, nobody can move before hydration; having no animation before first frame10 JS comes up was never anyone's fault to begin with. Seam only guarantees that injection is right, document is right, and the client entry point is right; the real constraint here comes after hydration: client navigation is done by pushing data into that still-living graph (layout persists), rather than going through injection once more to swap document. In short, what Seam takes over after hydration is the same static component graph.

There is also Marko 6, which is already quite close to what I have in mind (template AST, compile-time fine-grained updates, resume rather than replay); but compared with Marko, the technical lines that can still be drawn between them come down to two: one is that request-time still runs JS compiler output, with the whole chain tied to the JS runtime; the other is that the author's language is Marko, not Svelte. I believe that when most people talk about the frontend in 2026, they will only count React, Vue, Svelte and Solid as the mainstream UI Stack; if I did not bring it up, many people might not even know what Marko is. So building on Svelte, I am not really "copying the principles and bolting on a backend"; I prefer to describe it as borrowing a UI that someone writes, someone uses, and someone keeps fixing bugs in and polishing, all of it material everyone already knows.

And another thing: what Qwik's "no hydration" saves is the client-side replay, but it does not save the server-side SSR pass11(?) So, regrettably, no framework today satisfies me, or gives me enough API to have any chance of adding on top of it, and that is why a few months ago I went and built this SeamJS thing; so what today is really about is re-planning its roadmap.

The me of today wholeheartedly agrees with "Svelte: for building, not frameworking.12"

React

Let's start with React. Setting aside its commercial tie to Vercel Next.js and the fact that it has its own roadmap, the first problem is that CVEs rain down on it, which alone means you are doomed to watch Social Media around the clock for whatever new vulnerability gets reported & released. The other Routers are basically unusable, Remix especially13, yet in 2026 there is one very special option: TanStack Start. I have used TanStack Router for a long time, and apart from the naming style and data structures not being very pretty14, it is the only option I endorse on functionality. It is on the new side, but it solves the cold-start first-visit speed problem that the next-server process in Next.js 16 still has not solved to this day. Next.js has always had this inexplicable stutter, and it is not that the server lacks power; I would attribute it to either the startup Load not having finished, or runtime still compiling something. And that structure is inherently a poor fit for the Cloudflare Worker principle of many instances in many regions, each started recently; only a traditional long-lived Node process like Vercel's15 avoids this problem (?) That is drifting a bit; Next.js deserves a whole separate rant later, but TanStack16 really is, in my view, mature enough for commercial use; at the very least I have put it on the sales site of a certain project. But it is only the second of two options (x)

Vue

As for Vue, when I personally migrated from Vue 2 to Vue 3 I could no longer get behind its syntax and mechanisms; the Vue in my heart will always be Vue 2. There is no denying that Vue 3 has many excellent feature, but compared with Vue 2 its core feels different to me, and very different at that. Then, looking at it from a commercial angle, is Vue being tied ever more tightly to the meta layer and controlled by its ecosystem? Its situation feels very much like Svelte: both are open source, but it is genuinely almost impossible for a third party to step up and build a second implementation for you, which is exactly why the shake-up brought by TanStack Start hit Next.js so hard; Tanner Linsley deserves real credit. There is one more thing, a problem of Vue that cannot be ignored: the early Vue really was a revolution, but every UI Stack runs into ecosystem problems once its ecosystem takes off, that is, certain compromises made for compatibility? and this problem is especially glaring in React; today's Vue is no exception. When this kind of content17 starts appearing, I usually consider it Slop, though you cannot say it is bad either, because once Nuxt becomes the next "enterprise-grade" framework, the primary consideration is no longer whether the technology is correct or elegant, but a certain backward compatibility + stability (x

So Evan You did not really do anything wrong; you could even say someone had to step up and play the , and he just happened to be the best fit for it, doing nothing more than the most fitting thing...

Solid

SolidJS is a fine framework, and its core is stable enough, but nobody wants to build an ecosystem for it. Solid is really just the early-days Rust, except Rust has already made it through; Rust in the programming world is likewise in a spot where the ecosystem is thin and you have to build every wheel yourself, but the Rust toolchain lets you build that wheel smoothly and get a minimal working unit running, so building wheels is not actually all that hard, is it? Unfortunately, SolidJS has not made it through that stage yet, and then it ran straight into the AI winter; it will get especially hard after 2026. The Solid 2.0 core is already very good and very stable, but the periphery never snowballed. The idea is exactly right, it just arrived at the wrong time; and the few peripheral pieces that do exist live in one or two scattered corners (TanStack, for instance, has an experimental Router for Solid) and cannot be assembled into a complete ecosystem. That leaves it in an awkward position now and in the future: no complete ecosystem, in an era where Agents are exploding; if the snowball has not started rolling, Agents will not pick it first, and if AI does not prefer it, that means fewer chances of getting contributions, especially in an age when even Linus Torvalds has started . Solid feels like a real shame: the idea is good, the technology is correct, the implementation is elegant, but it truly is a pity that it was born at the wrong time...

"Perhaps the me of the past would not have seen this as a problem -- writing everything myself would have been fine -- but sadly, the me of today no longer thinks so"


The Balance Point

So along that road, the one that lands right on the perfect balance point is really only Svelte. It has a better ecosystem than SolidJS (a better birth year), its performance is not far behind, and sometimes, thanks to its Compiler, it is often even faster and leaner than SolidJS?! Of course someone here will surely say that once the number of pages climbs to a certain scale, or the more a user browses, Svelte's resource overhead18 compared with SolidJS will keep growing? Network requests will keep multiplying? Well, in theory that is true, but I think it has to be looked at against real websites: on 99.9% of personal or content sites, visitors basically do not stay long, and lingering a long time can even be considered a very rare case. At least on my site, deep click-throughs are currently very few, so "flipping through many pages = Svelte loses more" is not a problem for me. But that is not the reason for choosing Svelte; the real core is still the markup AST.

And then there is the Dashboard, the kind of thing where pages do get clicked open one after another; this statistic does not help there, but the Dashboard's users are, after all, far fewer than the real content audience, are they not. So I think "browsing a huge number of pages makes the overhead grow" is essentially a false premise; at least for now, maybe I will see it differently later, but in today's fast-paced information age I find it hard not to admit that such users are extremely rare; even if someone explores deeply out of interest and converts into a long-term follower, that exploration most likely happens only on the first visit, and afterwards they just drop by to read the newly published articles. Statistically speaking, the return on this extreme case is simply not proportional. So I am more inclined to believe: in most scenarios Svelte delivers better overall performance.


Beyond sitting in a more suitable position relative to the three frameworks above, Svelte actually feels very close to writing bare HTML; in fact, in the early days when Svelte 3 had just caught on and the .svelte extension had only just been settled, this was the officially recommended approach before the official VSC extension came out, so much so that you can still find this line on the Svelte extension page today:

If you added "files.associations": {"*.svelte": "html" } to your VSCode settings, remove it.

Beyond that, Svelte also offers me plenty of benefits. First, it doesn't push the UI toward JSX plus a pile of JS function19; and <style> is a compile-time artifact rather than runtime. The former keeps the structure inside markup AST; the latter lets CSS go straight into the IR, and Tailwind happens to fit both sides (


Black box

As I wrote in the previous post, Seam is first and foremost a protocol, not yet another SSR runtime. Build-time produces HTML skeleton carrying slot, and request-time only does injection; if / each / match are likewise protocol nodes20

The pity is that I had already picked React for the UI and started cutting; unfortunately, capturing page structure and generating variants on top of React is extremely hard, and modifying the compiler would be no less work than rewriting one...

Loading diagram…

Without modifying it, you could still treat the React compiler as a black box and actually run a pile of render + diff to guess the structure and use that; finite decisions like nullable / enum can still be enumerated, but the moment you hit a predicate like price > 10, the type's value space simply cannot be multiplied out. In V1 this situation just made you supply mocks by hand, for instance cutting three times at price < 10 / = 10 / > 10;

But this is exactly where the problem lies(?) Making those three precise cuts requires you to know the cut point is at 10, yet the React compiler is opaque to me; we have no idea what the condition is, so the price of being TypeSafe21 becomes the user doing the compiler's job.

In theory this should not be dumped on the user; otherwise the outcome is escape everywhere22, CTR23 still only covers that same bit of nullable / enum, structure discovery amounts to not having been done at all, and then it is unclear how much meaning is left in it.

But this time I am not saying the protocol itself has to change either. The skeleton, slot and injection I described before can, I think, actually stay, and the backend still does not have to run UI code; what has to change is the layer in front of the protocol, the question of how the answer page gets compiled into those nodes. And here I cannot help but thank Svelte for the greatest gift it gave me -- markup

Because I don't have to guess the structure of markup at all!!!


Visible AST

On the strength of "markup is real markup" alone, the page structure absolutely has a chance to become something a compiler can read, rather than a component tree produced by a pile of JS executing at runtime. Let's just say it: why must a perfectly good UI be able to draw itself through arbitrary execution?I'm not denying that view, but what I want to say is that at least 97%+ of the time, arbitrary execution for drawing is never needed,for the details, see here

Take a counterexample: in React,

function Card({ user }) {
  return user
    ? <div className="card">
        <Avatar user={user} />
        <span>{user.name}</span>
      </div>
    : null
}

at its core is still executing a JavaScript function and seeing what it returns; however clever the compiler is, the basic model it faces remains: JS executionJSX expressionelement tree Svelte, on the other hand, is quite different

{#if user}
  <div class="card">
    <Avatar {user} />
    <span>{user.name}</span>
  </div>
{/if}

What it hands the compiler is directly

Component
├── IfBlock
│   └── Element div
│       ├── Component Avatar
│       └── Element span
│           └── Expression user.name

That is already very good for an ordinary Kit app, but for something like Seam, which wants to break server rendering down into compile-time skeleton + CTR + SSR fallback, the significance is far more than a little.

Because this tree gives me two layers of information at once. The first is value dynamicsslot, but that was already achievable back in V1; what matters more is that the latter gives me structural dynamicsprotocol control-flow nodes. Rather than guessing at the black box of React's compiler, Svelte's compiler just lays a proper front end out for me to read; for example, {user.name} is the former and {#if user} is the latter.

Loading diagram…

Also, the if / each / match from the old protocol are still valid, but the Cartesian product from the earlier older post needs a qualification, I think: the condition under which it holds is actually the branch space, not the value space. For finite decisions like nullable / enum / bool the combinations are finite, and paying compile-time to exhaust them is mathematically sound; but JTD's string / number / timestamp are inherently non-enumerable, and the branches carved out by predicates like price > 10, inventory < 5, items.length === 0 cannot be discovered by a Cartesian product over typed values alone; sentinel with any one number filled in will always go down the same side.

If you really want to collapse them into finite decisions, you have to first derive them into bool / enum, and that step is not something JTD hands you for free (x) so V1 holds sound on nullable / enum, but not on arbitrary field value spaces; the real engineering problem is more that those blocks are guessed out by diff, and IR cannot explain itself.

Lowering

But in the Svelte era these nodes can be generated straight from the AST, and the reason for doing so is not to shave off those few ms by switching stacks; it is far more about turning the control flow from observation24 into lowering -- structure discovery is handed to the AST, finite decisions use protocol nodes, and there is no longer any need to multiply out N copies of HTML; types constrain payload rather than discovering the tree. So in the end, in Svelte, values on this Layer look like the following ↓

STATIC
<div class="card">

DYNAMIC
{user.name}

STATIC
</div>

Much of this kind of DYNAMIC does not even need React's level of "true rendering25"; keeping the slot is enough.

Going a step finer, Svelte can also break a single element down like this

structure: static
attributes: static
text node #0: dynamic

This is exactly what Seam needs most and likes most. In React, to find out whether attributes is static, you basically have no choice but brute-force enumeration: run every possible input, and if attribute stays unchanged it is static; if it changes, give it a slot.

Structural branches fare even worse: even "is this piece of DOM here or not" has to be answered by rendering twice and then diff, whereas Svelte's IfBlock turns that latter question from observable into generatable, and that is the real qualitative leap.

But actually, putting it that way isn't entirely right either, or is too simplistic; to be a little more precise, a single element can still be split further, but attributes is not necessarily static from the start -- it really depends on how you write it

class="card"             → attributes: static
class:active={on}        → name static, value slot
{...rest}                → opaque, escape hatch
text node {user.name}    → slot

For instance, there is no dispute that the first line here goes into the skeleton, but the second line is where Svelte really shines: class: / style: keep the attr name in the AST while only the value becomes slot, so the whole element need not be marked dynamic. The third line is not fundamentally different from React's {...props}; Seam just treats it as an explicit hatch and stops pretending to analyse it. In React, a literal className="card" does not really need enumerating either; what has to be enumerated is "will this attr change after a different set of props". That last kind of guesswork is exactly what Svelte does away with -- as long as the name is still on the tree, you don't have to render twice to find out 🫠

Either way, React forces Seam to treat a lot of things as a runtime problem all over again, for example ↓

const Wrapper = cond ? A : B;

return Wrapper({
  children: foo.map(renderItem)
});

Or alternatively

return foo && bar
  ? getLayout()(data)
  : something();

Of course, the modern React Compiler can analyse a good deal of this, but hacking on it means maintaining it forever, and that cost is enormous, in effort and in time alike... If I go with Svelte instead, I don't plan to fork the compiler here,since in theory svelte/compiler already exposes the AST.

What Seam does is read that tree and then generate its own IR; the official DOM / SSR codegen can all stay as a fallback, and in theory the only thing that needs maintaining is the IR lowering, not all of Svelte, so the overall direction is aligned and conflicts will be few -- but what is aligned is "structure is visible at compile time", not the execution model; precisely because of this, Svelte / Kit's SSR still runs the generated render function once per request; what I would rather have in my Seam, though, is that request-time does not run this function at all and only interprets the protocol (slot + if / each / match)

There was actually one more consideration behind choosing Svelte: given that Next.js and React are already completely26 controlled by Vercel as market frameworks,if Seam promised compatibility with React semantics, then my architecture would ultimately have to allow arbitrary JavaScriptdetermine tree structure.

So Seam's IR would have to be extremely conservative(x) In the end it would most likely come down to: optimise whatever can be statically analysed, and fall back to SSR for whatever cannot. That still yields a page mixing CTR + SSR, but the trouble sits precisely in that "cannot analyse" part, and the problem is that the React world has far too many places that are "not determinable"...

If we don't modify the React compiler, we're stuck in the same position as V1: treating the Compiler as a black box and guessing at its output, which is extremely fragile, inelegant, and prone to edge cases popping up everywhere.

Invert

So if I now say that Seam's component language is simply Svelte, then the compiler can even work in reverse: start from structure known by default, add local expression dynamic and explicit escape hatch, then move into runtime, and only when nothing else can handle it fall back to SSR at the very end.

<script>
  let { product } = $props();
</script>

<article class="product">
  <h1>{product.name}</h1>
  <div class="price">${product.price}</div>
  <button>Buy</button>
</article>

Seam could perfectly well understand it as something like the following

<article class="product">
  <h1><!-- slot 0 --></h1>
  <div class="price">$<!-- slot 1 --></div>
  <button>Buy</button>
</article>

So in theory slot0 = product.name and slot1 = product.price. This is the residualization I was already doing: the core idea is that the whole page never has to go through SSR, and only the truly dynamic residue gets computed.


Slots and Branches

But the real payoff of switching from React to Svelte is actually one layer down: no more rendering twice to guess when the structure changes, because the structure is already settled at compile time, so the matching IR can be emitted directly.

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

The value is still slot, and the branch is the if block from the old protocol. But in the React version it takes several rounds of diff before you can get hold of this block; in Svelte, by contrast, it can be derived and generated directly from IfBlock.


Escape

This is not to say JSX can't do it, of course. It's just that when you actually write it, hardly anyone leaves the structure somewhere easy to see on the branch tree; almost everyone ends up writing it like this(x)

const price = formatPrice(product);
const body = product.available
  ? getAvailableView(product)
  : getSoldOutView(product);

return <Layout>{body}</Layout>;

And there is one more thing that really matters: even if you can write it this way, as long as the main body of React code is not written this way, and the LLMs being trained are not written this way, then the migration cost and the later development cost will absolutely grow exponentially.

But Svelte is different here again

UI structuretemplate AST, and JavaScriptvalues + behavior is already everyday practice here

It is even the practice Svelte officially encourages most. This distinction is structural for how Seam extracts its IR. But please do not take this to mean Svelte has no dynamic structure; in fact it has its own escape hatch for that, and it is one of the main pieces of syntax

{@render children()}
<svelte:component this={layout} />
<svelte:element this={tag}>
{@html html}

Wrapper = cond ? A : B does not vanish into thin air here; rather, it is narrowed from "any JS expression" down to these few nodes, which makes things very simple for Seam: just follow the default of going through AST lowering; a hole that is statically visible belongs to whoever is inside it; the most common case is {@render} as compose (layout, and Card wrapping children, are all of this type), while <svelte:component> / <svelte:element> / dynamic {@render} are the explicit hatch and go into runtime or SSR fallback; and for {@html}, the raw HTML slot I designed directly can simply be reused.

So the real advantage isn't "the structure is always known" -- it's "unknown structure comes with a reference list" (x

In React the unknown is the whole of JS; in Svelte the unknown is just the openings of those few Escapes above, nothing more. Once that is applied, the range of content CTR has to analyse will shrink markedly

CTR

Working in Svelte does not mean it can necessarily go through CTR, but I do have a reference standard here ↓

Loading diagram…

Only then is the static reference compose; things still go through the hatch below, from which you can conclude that the unit of CTR is the static component graph, not the individual file. Most cases are structure known by default, a few are explicitly escape hatch, and you almost never run into SSR fallback day to day. Anything else not on the table is treated as opaque across the board -- settle it and there is no guessing left at all. CTR is only the execution model over this subset, and even if I switch to Svelte I do not intend to guarantee the whole of Svelte's range27 (x)

CSS

Another huge win is actually CSS, which in Svelte can be written as

<div class="card">...</div>
<style>
.card { border-radius: 8px; }
</style>

Here, when Seam compiles a component, it already holds markup dependency + script dependency + style dependency at the same time, and the style is a static artifact. So it can very aggressively determine

Loading diagram…

Or even go one step further and shape the flow like this ↓

Loading diagram…

The whole flow above needs no JS runtime at all

No CSS-in-JS

And I turn down CSS-in-JS too, though not because React forced me to accommodate an expression like the one below

const Button = styled.button`
  color: ${p => p.primary ? "red" : "black"};
`;

OR

<div css={theme => ({
  color: theme.colors.primary
})}>

The thing is, the structure above could already be worked around with Tailwind back in V1. The real reason CSS-in-JS is not wired in is that the moment it is, CSS turns back into a JS execution result, and then the style runtime, registry, SSR collection, hashing, hydration consistency, theme context, insertion, ordering and dedup all have to be invited back in. An ordinary framework can swallow that; but Seam is different! This kind of server / runtime work is exactly what I set out to eliminate, and there is surely no need to invite back, out of ecosystem politeness, the very thing I just chased out (it makes no sense at all

Besides, the problems CSS-in-JS was born to solve in its day were the very things Tailwind CSS & Motion now do; if Tailwind CSS & Motion already solve them well enough, or even better and more elegantly, why turn the clock back? As I said earlier, "once anything's ecosystem takes off, compromises28 appear, and breaking refactors that truly renew it become rare." So since Seam has no ecosystem yet, why not go radical all the way?

But I am not sure whether "radical" is the right word here. Where it might fit is that, compared with traditional frameworks, I am willing to throw away that heavy server-side JS runtime (Node or Bun); where it might not is that, say, dropping CSS-in-JS entirely would actually cause no real problem, I would rather think of it as . The once best-known CSS-in-JS libraries: Stitches was officially declared no longer actively maintained in June 2023, styled-components went into dormancy in March 2025, and Emotion is more or less in the same state.


So I have to praise Svelte's <style> once again here; CSScompile-time artifact is already beyond dispute in Svelte (at least that is how I see it, and if you disagree then you are right)

Unless it is JSexecutegenerate CSScollectserializehydrate, the gap will be huge, and more importantly, perhaps the CSS dependency can go straight into the Seam IR 🤔

For example, component compilation:

ComponentIR {
    skeleton,
    dynamic_slots,
    css,
    client_behavior,
    server_dependencies
}

Note that this is not a separate rendering model: in V1, skeleton + dynamic_slots, once compiled, is exactly that HTML from the old protocol, only now how skeleton + dynamic_slots come to be is more transparent to me.

Suppose we handle it like this ↓

<script>
  let { name } = $props();
</script>
<div class="user">Hello {name}</div>
<style>
.user { font-weight: 500; }
</style>

So in the end it is bound to reach

ComponentIR
  HTML:   <div class="user svelte-x">Hello SLOT(0)</div>
  CTR:    slot(0) = props.name
  CSS:    .user.svelte-x { ... }
  Client: none
  SSR:    none

But note that what you have seen above is only leaves, and fairly ideal slices at that: no child components, no structural branches, no client. The page-level skeleton is composed from a static component graph; once a subtree has $state or hatch, the Client / SSR in the IR will no longer be none.

But this is already very close to the ideal state I want Seam to reach: a Svelte component no longer corresponds to "a renderer that has to be executed", but becomes a resource the Svelte 5 Compiler can take apart; getting that far is already a very large conceptual improvement for me.

Ownership

Beyond that, "not pushing global state" matters more to Seam than DX alone. I am sure you have already seen the following pile of stuff in a Next.js project, or in any React full-stack framework ↓

<ThemeProvider>
  <AuthProvider>
    <QueryClientProvider>
      <I18nProvider>
        <RouterProvider>
          <App />
        </RouterProvider>
      </I18nProvider>
    </QueryClientProvider>
  </AuthProvider>
</ThemeProvider>

Of course, this is not without a solution. This "pyramid" of nested Providers can actually be written as Compose Providers, which ends up looking roughly like the following.

Concretely, you dump the pile of nested Providers into an array, define a composing function, and then consume that single layer. But to me this is really just a way of fooling yourself: in essence it only "hides" the nesting, it does not actually eliminate it. Visual nesting simply becomes logical nesting, a kind of , pure self-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>
  );
}

React projects really do end up like this very easily, or rather 100% of the time, and that produces a problem Seam finds deeply annoying, namely "what exactly is a component's input?" On the surface you could say <ProductCard product={product}/> seems to depend only on product itself? But what about in reality

depends on:
product
theme
locale
queryClient
router
auth
featureFlags
...

So the component dependency graph becomes implicit once again. For traditional SSR that is not a big deal; worst case you just "run the entire React tree once more". But what Seam ultimately wants to ask is whether ProductCard can be CTR'd on its own, and at that point things get genuinely painful. You could even say it would mean changing the behaviour of the whole React compiler along with users' DX and habits, so why bother...

People using Next.js have basically stopped caring about performance29, and I can say that with full responsibility; what they are really paying for is the Vercel supporting infrastructure, and if you use Next.js without hosting on Vercel you get none of the benefits at all 😡

Local ownership

Svelte, by contrast, lends itself very easily to forming local ownership;

<script>
  let { product } = $props();

  let quantity = $state(1);
</script>

From the perspective of Svelte / the Seam Compiler, this is very comfortable

After all, with "input: product + local client state: quantity" the ownership really is perfectly clear

product.name     → server CTR
quantity         → client
button onclick   → client
rest             → static

But that is only the default, not really a guarantee; because the rune module inside getContext('theme') and .svelte.ts is still an implicit input, and QueryClient will not simply vanish either. I think Seam's attitude will be the same as with the hatch list earlier: whatever it sees gets marked into the IR (a server dependency or client ownership), whatever it cannot analyse is treated as opaque, and it is better not to pretend that "Svelte has no such thing as a Provider". Though not necessarily; maybe with a bit more research I could find a way to handle Providers

The biggest problem Seam had in the React era was that the IR went Componentexecute rendererget output, so what I ended up calling seam compiler actually became, in a certain sense, an early SSR, or could just as well be described as

"universal SSR orchestration layer"


Same protocol

So from now on it can be said that the UI is Svelte, but the seam protocol is still just a protocol, still centred on slot and injection, whether JTD is really the right fit is something I still need to think through further; in terms of execution decisions, it is already fully layered: the slots are already residual-ified, and a Rust / Go / TS server only needs to fill them in blindly with no need to understand the component code at all -- truly zero JS; at the same time, if a component still contains pure derivations, then adding a generated JS / QuickJS runtime can be considered, (note that what runs here is data, not UI)

Only hatch ones SSR fallback, so the backend can skip running UI code whenever real SSR is not needed, and naturally there is no lock-in to Node OR Bun -- but then again (the Go Server is slated to stop being maintained, because I personally cannot keep up; if some kind soul has the time, feel free to pick it up and maintain it long-term -- it is not that the architecture stopped supporting backend independence)

In other words, I only cut out the UI, the part where I'd be taking on thankless, effortful work for anyone and everyone (?). The part that stays unchanged can still serve as a "Rendering Protocol", with the backend not limited to JS/TS.

Runes

Another point that matters a great deal is that Svelte 5's runes are very friendly to Seam, even though Svelte 5 has started to look a little more like plain JS

let count = $state(0);
let doubled = $derived(count * 2);

But this JS is not that JS30, and the difference from useState / useMemo / useContext / useEffect is that runes are still compiler-recognized semantics, so Seam can know that $state is owned by the client, $derived is derived, and $effect is client runtime; this is not in the same league as const foo = someLibraryHook().


Data origin

An important point here is that "recognisable" is not the same as "CTR-able", and in particular you should not assume that anything you see as $derived goes into skeleton; the count * 2 above is one counterexample, because its dependency is $state, which makes it a client-derived product. A $derived that can be CTR'd actually looks like this:

let { product } = $props();
let price = $derived(formatPrice(product.price));

product comes from $props, and formatPrice still has to be pure and visible; plugged into the table above, that is what it looks like

$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 pass

Drawing it out like this might make it easier to understand(?)

Svelte and Seam compiler pipeline Svelte source becomes a visible Svelte and Seam AST, then follows three horizontal lanes. Structure becomes static HTML, values become CTR slots, and behavior becomes client JavaScript. Svelte source Svelte / Seam AST visible + typed Structure markup AST Static HTML Values data + derives CTR slots lowered data Behavior state + effects Client JS

Anyway, Runes-friendliness shows up as ownership being marked in the syntax; it is not that $derived comes with CTR eligibility built in. The only real criterion is still what the path below shows: where the data comes from, whether it is pure, and whether it is visible

CTR value lowering A CTR candidate is classified by data origin, purity, and visibility. A simple, pure, visible expression becomes generated JavaScript, while an opaque or complex expression uses QuickJS or an SSR fallback. CTR candidate value / derive Lowering decision origin · purity · visibility never syntax alone simple pure + visible generated JS complex opaque / hatch QuickJS / SSR fallback

Then put a <style>static CSS dependency graph right beside it, and the whole worldview becomes remarkably consistent: HTML is structure, CSS is a style artifact, and JS is always computation / behavior.

rather than the All in JS31 that modern React so easily drifts into. Perhaps for React itself that is not necessarily a bad thing, but unfortunately Everything is JavaScript happens to be the worst possible world for the aggressive compile-time decomposition I want to do


What will Ship? 32

After all that, what will actually land? (x) First, without question, the UI stack moves to Svelte, and once that is done you can expect compiler observability to improve noticeably. The main reason is that Svelte forces and encourages structure to stay inside markup AST rather than being buried in arbitrary JS control flow, which directly widens the analysable space for skeleton / CTR and turns CSS from runtime concern back into build artifact.

This means Seam gets the chance to do component-level CSS dependency, critical CSS, tree shaking and lazy CSS, without having to maintain an extra style runtime for server / client.

Also, Component ownership becomes thoroughly clean; having less Provider / Context / global runtime dependency means I finally get the chance to answer, once and for all, which value comes from the Server, where CTR can pass things through, who needs client boundary (which state belongs to the client), and whether this component really needs hydrate...


Of the three, the first two directly decide whether CTR is merely an optimization or can truly become SeamJS's main execution model33, which is also why, for me, dropping React is not necessarily a technical compromise; conversely, it can be a structural breaking change. In short, I still hold to what I said

Anything, once its ecosystem takes off, will start making compromises; breaking refactors that truly reinvent things become rare

So since I haven't really taken off yet, I naturally have plenty of chances to make breaking changes without worrying about migration costs for now, and I truly hope that one day, after countless breaking changes, I will find the place that is truly mine.

Even if in the end I am its only user, this is still my own "Experiment 🧪"