This is great. I think the only missing part is that I think I'm right in saying that an RSC still sends JS to the client and React hydrates the component, even if there's no client-only stuff, correct? In Astro if there's no client directives or script tags it loads no JS at all.
if you're asking "can RSC be made to only send static HTML with no JS", the answer is obviously yes. you take the RSC, turn it into HTML, and generate that for every page. done. but that wouldn't include any interactivity which is like half the point of RSC
if you're asking "can RSC emit HTML with some client JS and then hydrate *just* the client components" the answer is both no and yes. the question suffers from implicit assumptions.
RSC doesn't send any "client code" for the server stuff. but it does include serialized output JSX in its payload.
so effectively, it's kind of like if you embedded "which client React components should receive which props" as JSON. this makes sense because that's the information you're trying to transfer. that's what serialized JSX is. "which props go to which client components".
from a pedantic perspective, yes, you can say "the entire tree gets hydrated and not just the client parts". but that's kind of nonsense too. "the entire tree" consists of HTML tags and Client components with props. "hydrating" plain HTML tags is very cheap because it doesn't run any user code.
there is some duplication in the output itself between the RSC representation and the initial host HTML into which it gets curled. it's particularly noticeable for plain HTML tags because there's no transformation left to do there. this is something RSC should ideally improve upon in the future.
This is a very good point. Before I fully understood RSC, when I heard that the whole tree was hydrated, it felt like a bummer. But the insight is that the server parts have vanished, and basically have no cost anymore. To me it’s morally equivalent to saying server parts don’t need to hydrate.
RSC doesn't send any JS itself but ofc something needs to convert the RSC tree into pixels. You can do that at build time or request time on the web (via HTML as an intermediary) or on the client with the React runtime
"If a component truly does not require interactivity, just remove 'use client' from it, and then importing it from the Server world would already keep it Server-only."
I think one note absent from RSC discourse is that "server-only" here is, IMO, misdirection. Since React still needs to hydrate...
and it has to start from a single root, the server still has to send all of the metadata for RSCs down on initial load. This is necessary, as I understand it, so that React can find and attach event listeners (and frameworks, e.g. Next use it for routing metadata as well).
This isn't "react-specific" either necessarily, it's a side effect of SPA architecture, which is built on the premise of having a local data model (meaning you know ahead of time what the routes are).
Astro is an MPA framework, so it doesn't suffer from this, but instead (as you write) has MPA-specific drawbacks.
Just think it's worth clarifying that astro components result in "just HTML", whereas RSCs result in HTML + metadata (which in Next is a bunch of script tags at the bottom of the doc).
"Server-only" here refers to the code, not the data. Of course we need to send the data (otherwise what purpose does it serve? Heating our desk?), but we don't have to send the code.
My point was that often I see inexperienced React devs saying that RSCs = zero clientside JS, or more generally, no clientside impact, which is inaccurate, since it's sent twice (once as HTML, again as vdom metadata).
heh i remember someone on twitter accusing the react team of literally "lying" because the RSC payload from "server" components shows up in the output on the client... terms are tricky
A question I thought of as I was re-reading this: Would it be possible for a React meta-framework to implement something like Astro's client directives/progressive hydration for RSCs? Where you could say "don't hydrate this client component until the screen width hits the `sm` breakpoint"?
Ohh interesting. I didn't realize this is what was for. So, e.g. you would wrap your mobile-only sidebar in an to defer the hydration until the small breakpoint is reached?
not sure what the best practice is with breakpoints specifically, maybe @ricky.fm can answer! in general is not just for hydration (scroll above for more examples). we’re just trying to have a few primitives that each may express something about the ui and cause heuristics to kick in
with the idea is to represent coarse areas of user focus and then that ties into multiple features like state preservation between remounts, deprioritizing hydration, deprioritizing state updates while inactive, etc
For Activity you wouldn’t set it to hidden since that would remove it from SSR, and it would be hidden on the content. You would do Activity “visible” which allows React to defer hydrating it like we do for Suspense boundaries (just showing the SSR content until it hydrates)
I'm secretly hoping for a "RCS for the pragmatic SRE" post! RSC introduces real complexity: duplicated payloads (HTML + JSON), slower server responses under load?, hidden fetch caching, and trickier monitoring. Curious how these tradeoffs play out at scale in real-world apps. Time will tell
duplicated payload is only relevant for first load, and even then it’s not paint-blocking. don’t know about web but sdui is highly successful pattern for top native apps. this is like sdui for web. i think there’s a bunch of rough edges but the underlying idea is proven out at least in native.
i thought this would be a single article for many technologies but even for astro alone there’s a bunch to talk about so i figured an article per technology makes sense
i do link to this page from the post. this is somewhat helping (view transitions make the change look seamless) but from what i understand, this still effectively swaps html. so it wouldn’t retain any shared state in the chrome unless you manually persist/restore it. maybe i’m missing something!
yes, hence “with manual logic” wording in my article. i’d still say that this is both not always enough (some things aren’t serializable) and.. just a complete non-problem if you don’t lock yourself into a MPA approach in the first place (this is the more opinionated take)
I think there’s a difference between manual indicators and manual logic. In this case, Astro does all the state restoration logic for you after you apply a single prop telling Astro which component(s/trees) to restore the state for.
Might test it out myself tomorrow to be sure. There was an RFC about SPA mode I recalled that led me here. Seems weird you’d have one (client transitions) without the other (preserved state across transitions).
i’ve looked closer. it’s tricky. it transplants DOM nodes to the new page. this works to some extent but imo is a hack and has consequences where some things are flaky. i’m not sure how to mark “mostly works but sometimes doesn’t and if it doesn’t, you can’t fix it without rewriting your component”
if your target format is html, you’re always limited to at worst fully reloading the page, or manually restoring/pesisting some stuff, or at best swapping or “morphing” individual parts (but unable to carefully refresh anything without losing the state within). both SPA and RSC are more expressive
i guess “at best” with html you can get very granular update instructions by inventing a DSL (see stimulus, htmx, laravel livewire) but then it’s also limited in expressiveness and/or granularity
While not wrong (disclaimer: I haven't yet read your blog post), context is key: do you need that added expressiveness/granularity for that one project you're working on?
Does your blog need it?
Even a docs site with, say, a tree view navigation doesn't *really* need to persist the tree state.
i’m being a bit pedantic in not classifying it as Astro component in this case although it’s tempting to.
first, it just isn’t syntactically. even if all its usages are static, you still can’t add server-only code (eg async fetch) inside of it without breakage or a lot of confusion
if it looks like a normal component, someone may want to add state there later. or anywhere in its subtree. it’ll break every such use! by silently killing what’s intended to be interactive.
from React’s perspective (not shared by Astro) this is a broken feature
so with RSC, you can’t end up in a situation where you add interactivity to something that looks interactive, and then some usage of it turns out to be broken.
if you added state to something that’s already truly server-only, you’ll just get a build error and be forced to “cut the door”
Great read as always! This one allowed me to think deeper about the fluidity of drawing boundaries. Sometimes a component pulls in a large dependency and sometimes it’s lightweight but generates a large JSX output. It’s great that we can force a side, but we need tools for this insight.
So we probably need some kind of profiler that tells us when some composition is increasing the burden on the server or the client and changing this or that will improve things. Maybe just observability in the beginning. There’s the also hydration aspect to think about. 🤯
Comments
if you're asking "can RSC be made to only send static HTML with no JS", the answer is obviously yes. you take the RSC, turn it into HTML, and generate that for every page. done. but that wouldn't include any interactivity which is like half the point of RSC
RSC doesn't send any "client code" for the server stuff. but it does include serialized output JSX in its payload.
we represent that as a tree
Been having a lot of thoughts here... *deep breath*
I think one note absent from RSC discourse is that "server-only" here is, IMO, misdirection. Since React still needs to hydrate...
It saves clientside compute since RSC nodes can be skipped on the client, but the payload is still sent down the wire and has to be parsed.
Just think it's worth clarifying that astro components result in "just HTML", whereas RSCs result in HTML + metadata (which in Next is a bunch of script tags at the bottom of the doc).
It's not a misdirection IMO
Agreed, misdirection is the wrong word.
Does your blog need it?
Even a docs site with, say, a tree view navigation doesn't *really* need to persist the tree state.
You can use a React component once with no client directive and once with client:load.
Is there something more subtitle RSC can do that I don't see?
(I agree on all other limitations)
i’m being a bit pedantic in not classifying it as Astro component in this case although it’s tempting to.
first, it just isn’t syntactically. even if all its usages are static, you still can’t add server-only code (eg async fetch) inside of it without breakage or a lot of confusion
if it looks like a normal component, someone may want to add state there later. or anywhere in its subtree. it’ll break every such use! by silently killing what’s intended to be interactive.
from React’s perspective (not shared by Astro) this is a broken feature
1) if a component is used from the Server world only, importing Server-only things from its module tree wouldn’t break anything
2) if a component is in the client world, importing Client-only things (like useState) anywhere in its deps won’t break it
if you added state to something that’s already truly server-only, you’ll just get a build error and be forced to “cut the door”