diff --git a/public/blog-assets/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture/flight_cache_pipeline.svg b/public/blog-assets/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture/flight_cache_pipeline.svg deleted file mode 100644 index e9f0d3e0..00000000 --- a/public/blog-assets/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture/flight_cache_pipeline.svg +++ /dev/null @@ -1,62 +0,0 @@ -The Flight stream cache pipeline in TanStack StartA horizontal pipeline showing a Flight stream produced by a server function flowing as bytes through four cache layers — render-pass deduplication via React.cache, server-side stores like Redis or KV, HTTP and CDN caching, and client-side stores like the router cache or TanStack Query — before being decoded at render time on the client. - - - - - - -Server function -Returns Flight stream - - -bytes - - - - -createFromReadableStream -Decodes at render time - - -Cache layers along the way - - - -Render pass -React.cache -dedupes calls - - - - -Server -Redis, KV, Postgres, -in-memory LRU - - - - -Network -HTTP, CDN, -Cache-Control headers - - - - -Client -Router cache, -TanStack Query - - - - - - - - - -The bytes stay transparent at every layer -No directive owns persistence. The application caches at the layer it already controls. -Decode happens at render time, after every cache boundary. - - \ No newline at end of file diff --git a/public/blog-assets/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture/two_inversions_rsc.svg b/public/blog-assets/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture/two_inversions_rsc.svg deleted file mode 100644 index f6d1c187..00000000 --- a/public/blog-assets/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture/two_inversions_rsc.svg +++ /dev/null @@ -1,57 +0,0 @@ -The two RSC composition modelsSide-by-side comparison of server-owned and client-owned tree composition. In the server-owned model, the server holds the tree and use client marks holes for client components to fill at hydration. In the client-owned model, the client holds the tree and the server fills slots with rendered fragments on request. - - - - -Server-owned tree -use client marks holes - -Client-owned tree -slots accept rendered fragments - - - -Server - - - - -use client - - - - -use client - - - - -Server region - - - - -Client - - - - -Server slot - - - - -Client region - - - - -Server slot - - -Server composes, client hydrates -Seam placed by the server - -Client composes, server fills -Seam placed by the client - \ No newline at end of file diff --git a/public/blog-assets/who-owns-the-tree/header.jpg b/public/blog-assets/who-owns-the-tree/header.jpg new file mode 100644 index 00000000..56fd71c0 Binary files /dev/null and b/public/blog-assets/who-owns-the-tree/header.jpg differ diff --git a/src/blog/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture.md b/src/blog/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture.md deleted file mode 100644 index 86a9bc22..00000000 --- a/src/blog/who-owns-the-tree-rsc-is-a-protocol-not-an-architecture.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -title: 'Who Owns the Tree? RSC Is a Protocol, Not an Architecture' -published: 2026-04-28 -draft: true -excerpt: RSC is usually framed as a single architecture where the server owns the tree. But it is also a protocol, and the protocol supports more than one composition model. The overlooked question is who owns the tree. -authors: - - Tanner Linsley -redirect_from: - - rsc-is-a-protocol-before-it-is-an-architecture ---- - -A few weeks ago we shipped [React Server Components Your Way](/blog/react-server-components), and the most common follow-up was a fair one: - -> Why even bother with two RSC composition models? Pick one. - -So here is the deeper answer. - -When most people argue about RSC, they are really arguing about one specific architecture: the server owns the tree, `'use client'` marks the holes, the framework stitches everything together at hydration. That is the model. That is what people mean when they say "RSC support". - -That model is real, it is important, and TanStack Start supports it. - -But it is not the whole of RSC. - -RSC is also a _protocol_ — a way to serialize rendered React output, client references, and non-JSON values into a stream that can be sent over the wire and reconstructed somewhere else. The conventional server-owned tree is **one way** to use that protocol. It is not the only way. - -The question that gets buried under the conventional model is the simple one: - -**Who owns the tree?** - -If the server owns it, you need a way to drop client interactivity _into_ it. That is `'use client'`. Standard model. - -If the _client_ owns it, you need a way to drop server-rendered UI _into_ it. That is what Composite Components solve in TanStack Start. - -Same protocol. Different direction. Different problems. - -## Why this matters - -Let's make this concrete. You're building a dashboard. The client owns tabs, filters, drag layout, optimistic updates, the command palette, all of it. But one chart pulls from a slow analytics API, runs server-only computation, and would ship hundreds of kilobytes of charting code if you rendered it on the client. - -In the server-owned model, the natural answer is to invert your whole route. Move the dashboard onto the server. Mark every interactive piece with `'use client'`. Hope the boundaries land where you want them. - -That works. But you just adopted a server-first architecture to render _one server-shaped region_. - -The inverse approach is much simpler. Keep the dashboard client-owned. Ask the server for a rendered chart fragment. Drop it into the tree wherever you want, alongside whatever client state you already have. If the chart has no interactive client regions, you don't even ship a client chunk for it. - -```tsx -import { createFromReadableStream } from '@tanstack/react-start/rsc' - -function Dashboard() { - const { data: chart } = useSuspenseQuery({ - queryKey: ['analytics-chart', range], - queryFn: async () => - createFromReadableStream(await getAnalyticsChart({ data: { range } })), - }) - - return ( - - - - - {/* Server-rendered output, dropped into a client-owned tree */} - {chart} - - - - - - - ) -} -``` - -Same protocol underneath. Same Flight format. Same renderer. The seam just moved. - -That is not a workaround for RSC. It is another valid way to use the RSC protocol. - -## The real question: who owns the tree? - -Composing UI across a network boundary is fundamentally an ownership problem. One side owns the tree. The other side fills regions of it. The interesting question is which side decides what goes where. - -### Server-owned trees - -In the standard RSC model, the server owns the tree. You write server components, mark the parts that need interactivity with `'use client'`, and the framework handles the rest. At hydration, the client receives the rendered tree along with a manifest mapping client references to actual modules, and React hydrates the interactive regions. - -The seam is placed by the server. The server says, "this region is interactive, and _this_ client component fills it." - -The client receives a tree it didn't compose. It hydrates what arrived. - -This is the model Next.js builds around, and it is coherent. It fits when the server is the natural owner of the page — content-heavy routes, marketing pages, SEO-driven content, the kind of page where the tree shape is mostly a function of server data. - -### Client-owned trees - -The inverse model gets discussed less because most frameworks don't expose a primitive for it. So let's say it out loud: - -What if the _client_ owns the tree, and the server fills server-shaped regions on request? - -This is the natural shape of a lot of the apps you actually ship. Dashboards, builders, admin tools, editors, data apps — anything where routing, layout, and interactivity live primarily on the client. - -In this model, you write a client tree like normal. Where you want server-rendered content, you leave a slot. The server renders content shaped for that slot and ships it as data. The client decides when to render it, where to drop it, how to compose it with local state, and whether to swap it out later. - -The seam is placed by the _client_. The server just says, "here is some rendered UI shaped for this region." The client decides what to do with it. - -That is the symmetry: - -
- The two RSC composition models: server-owned trees use client boundaries for interactivity, while client-owned trees use server-rendered slots for server regions. -
The two RSC composition models differ in which side owns the tree and which side fills regions of it.
-
- -| Standard RSC Model | Composite Components Model | -| -------------------------------------------- | --------------------------------------------- | -| Server owns the tree | Client owns the tree | -| Client fills interactive holes | Server fills rendered slots | -| `'use client'` marks client regions | `` renders server regions | -| Best for server-shaped routes | Best for client-shaped applications | -| The client hydrates what the server composed | The client composes what the server rendered | - -Server-owned RSC answers one question: - -> The server has the tree. How do I get interactivity into it? - -Client-owned RSC answers the inverse: - -> The client has the tree. How do I get server-rendered content into it? - -They solve different problems. They are not substitutes. - -## Both models can reach the extremes - -There is a fun symmetry here: both models can reach the _opposite_ extreme. - -Server-owned model? Push `'use client'` high enough in the tree and the route effectively becomes an SPA. The server still owns the outer entry, but everything below the boundary is client-composed. - -Client-owned model? Render a server component high enough and the route behaves like a server-rendered page. The client still receives and composes the result, but the server is doing essentially all the work. - -So the real distinction is not which model can technically reach which outcome. Both can. - -The distinction is which side the framework _naturally pulls you toward_, and how much friction you hit when you move the other direction. - -The client is the final destination of the UI. That makes the more powerful default a client-owned composition model with strong server composition primitives. It keeps the app rooted where the user experience actually runs while still letting you do server-rendered regions, server-only computation, streaming, caching, and progressive enhancement wherever they make sense. - -The win isn't choosing client over server. The win is not getting locked onto one side of the line in the first place. - -## Why most frameworks only ship half - -The standard RSC model assumes server-owned trees, so the primitives are designed around that direction. `'use client'`, hydration boundaries, streaming, suspense fences, manifest-driven reference resolution — all of it assumes the server is composing and the client is receiving. - -That is fine for what those frameworks were built for. - -It's also why those frameworks don't have a great answer when you ask: - -> How do I render this server fragment inside a client tree I'm already composing? - -The closest answer is usually "make the client thing a `'use client'` boundary inside a server tree." That works when the route is server-shaped. When the route is _client-shaped_, you're inverting the entire architecture for what is structurally a small request. - -Composite Components fill that gap. A server function returns a rendered React fragment as Flight data. The client passes it to `` and provides slots through `children` or render props. The server-rendered fragment can position those slots, but the client still owns the surrounding tree. - -```tsx -// server -const getPost = createServerFn().handler(async ({ data }) => { - const post = await db.posts.get(data.postId) - return { - src: await createCompositeComponent( - ({ children }: { children?: React.ReactNode }) => ( -
-

{post.title}

-

{post.body}

-
{children}
-
- ), - ), - } -}) - -// client -function PostPage({ postId }: { postId: string }) { - const { data } = useSuspenseQuery({ - queryKey: ['post', postId], - queryFn: () => getPost({ data: { postId } }), - }) - - return ( - - - - ) -} -``` - -This is _additive_. Start still supports `'use client'` when you want server-owned composition. Composite Components expose the inverse. A single app can use either, both, or neither — per route, per component, per use case. - -This is not even foreign to React's original framing. The first Server Components RFC describes Client Components as the regular components you already know, and notes that Server Components can pass other Server Components _as children_ to Client Components. From the Client Component's perspective, that child is already rendered output. The same RFC also describes granular refetching from explicitly chosen entry points as part of the design direction, even though the initial demo refetched the whole app. - -So the inverse model isn't a rejection of RSC. It is closer to taking the protocol seriously and exposing a composition direction the protocol already makes possible. - -## What this means for "RSC support" - -The phrase "RSC support" has been doing too much work. - -When most frameworks say it, they mean "we support the standard server-owned model." That is a reasonable thing to mean, and the standard model covers a lot of cases. - -But the protocol is bigger than the conventional architecture. That doesn't mean RSC is _just_ serialization — there are real architectural best practices around waterfalls, bundling, streaming, and invalidation. The point is narrower: the conventional server-owned application model is not the only architecture the protocol can support. - -A framework can support the RSC protocol while exposing different composition models on top of it. Whether that counts as "RSC support" depends on whether you mean the protocol or one specific architecture built on it. - -The cleaner framing is this: - -**RSC is a protocol with multiple valid composition models.** - -Server-owned is one. Client-owned is another. Both use the same Flight format, the same renderer, the same reference machinery. They differ in who owns the tree and where the seams sit. - -## A powerful primitive, not the whole pipeline - -This is where the conversation tends to get distorted. - -RSC isn't _just_ a serialization format. It's also React's attempt to bring data fetching, streaming, code splitting, server access, and client interactivity into one coherent component model. - -That is a worthy goal. - -But it is still _one way_ to organize those concerns. - -Routing solves waterfalls. Loaders start data work before render. Query libraries cache, dedupe, prefetch, stream, invalidate, and coordinate server state. HTTP caches responses. CDNs cache fragments. Server functions expose backend work without forcing the route tree to become server-owned. - -So the question isn't whether RSC can solve these problems. It can. - -The question is whether solving them through a rigid RSC architecture should be the **default answer for every application**. - -Start's position is different: RSC is a powerful primitive in the pipeline, not the pipeline itself. - -Use RSC where rendered server UI is the right abstraction. Use routing, loaders, query caches, server functions, HTTP, and client state where those are the better abstractions. Stop treating RSC as the coupon code that fixes all of them at once. - -This is intentional. Start doesn't reject RSC. It rejects making RSC the organizing principle for problems that are often better solved by smaller, more composable tools. - -The point isn't to avoid RSC. The point is to avoid turning RSC into a silver bullet. - -That same distinction explains how Start handles caching. - -## Why Start doesn't ship a caching directive - -People keep asking why Start doesn't ship something like Next's `"use cache"`. - -Short answer: `"use cache"` assumes a framework- or platform-owned persistence layer. - -The directive marks a function or component as cacheable. The runtime handles serialization, key derivation, storage, and invalidation. Which means the framework has to own — or at least define — the persistence contract underneath it. - -The directive doesn't _eliminate_ the persistence question. It relocates it. You write one line at the call site, and the platform fills in everything below it: memory, disk, database, edge storage, invalidation, durability, sharing across instances, deployment-specific behavior. - -That shape works when your framework is tightly coupled to a specific platform. - -Start targets Cloudflare Workers, Netlify, Vercel, Node, Bun, Railway, and any Nitro target. There's no single portable persistence layer across all of those, so there's no honest directive shape that means the same thing everywhere. - -This isn't theoretical. The portability story for Next caching outside Vercel has already been a moving target — earlier OpenNext Cloudflare docs listed [Composable Caching](https://opennext.js.org/cloudflare/former-releases/0.5) as unsupported until Next stabilized the feature, and current Cloudflare docs list [Composable Caching](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/) as supported but still experimental. - -Start takes the more transparent route. - -A page can render its stable regions normally and pull one expensive region as a separate RSC fragment. That fragment gets its own HTTP cache headers, its own cache tags, its own invalidation. Update one region and you don't blow up the entire page cache. - -That's the important piece: the RSC output is not trapped inside a framework-owned page cache. It is a stream of rendered UI moving through whatever cache layers your application already controls. - -A server function returns a Flight stream as bytes. Those bytes can be cached at whatever layer you already own: - -
- Flight bytes passing through cache layers: render pass, server, network, and client, with decoding at render time after each cache boundary. -
Flight output stays transparent through the cache layers the application already controls.
-
- -| Layer | Cache option | -| ----------- | ------------------------------------------------------- | -| Render pass | `React.cache` to dedupe calls within a render | -| Server | Redis, KV, Postgres, in-memory LRU, or whatever you use | -| Network | HTTP caching and `Cache-Control` headers | -| Client | Router cache, TanStack Query, or any client-side store | - -`createFromReadableStream` decodes those bytes at render time, after the cache boundary. So the cacheable primitive isn't a directive that hides persistence. It is transparent RSC output flowing through standard cache layers. - -This isn't a worse `"use cache"`. It is a different architectural choice. - -The directive shape is right when the framework and platform can own the cache contract. The transparent-bytes shape is right when the framework needs to stay portable across runtimes. - -## When to reach for which - -Inside a Start app, the practical guidance is straightforward. - -**Server-owned RSC, when the route is server-shaped.** Docs, blog posts, marketing pages, SEO-driven content — anything where the tree shape is mostly a function of server data. The server owns the page. Add `'use client'` where you need interactivity. - -**Composite Components, when the route is client-shaped.** Dashboards, builders, admin tools, editors, previews, data-rich product surfaces. The client owns layout, state, navigation, and interaction. The server contributes rendered regions where they actually help. - -**Both, when the app has different shapes in different places.** Marketing routes can be server-owned. Dashboard routes can be client-owned. A docs page can use server-owned RSC while an interactive playground in the same app uses Composite Components. You don't have to commit to one architecture up front. - -**Neither, when RSC isn't the right tool.** Plenty of routes don't need RSC at all, and Start doesn't make them pay for it. Fully client-side routes, static routes, simple server-data routes — keep them simple. - -## Closing - -The better question isn't: - -> Does this framework support RSC? - -It's: - -> Which RSC composition models does it expose? - -TanStack Start supports the standard server-owned model when that's the right fit. It also exposes the inverse: client-owned trees that compose server-rendered fragments as data. Both use the same RSC protocol. They just place ownership, composition, and caching seams in different parts of the system. - -RSC is a powerful protocol. It is a powerful primitive. It is not, by itself, an application architecture — and a framework's job is to let you compose it with everything else, the way **you** know your app needs it. - -Same TanStack philosophy as always. You know what's best for your application. The framework should get out of the way. - -## Further reading - -This post was partly prompted by Viktor Lázár's [RSC as a serializer, not a model](https://dev.to/lazarv/rsc-as-a-serializer-not-a-model-56nj), which argues that TanStack Start exposes RSC more as a serializer than as the conventional RSC application model. - -Daishi Kato's [My Thoughts on RSC: Is It Just Serialization?](https://newsletter.daishikato.com/p/my-thoughts-on-rsc-is-it-just-serialization) is worth reading as a contrast. I agree with the narrower point — RSC is _not_ merely serialization. Where I push back is the broader implication that RSC's architectural best practices should pull a framework toward a server-owned application model. - -My position is that RSC is a powerful protocol and a powerful primitive, but not a complete application architecture by itself. The framework around it should still be free to compose it with routing, loaders, query caches, server functions, HTTP caching, client state, and the rest of the ecosystem — instead of letting RSC become the organizing abstraction for the whole app. - ---- - -If you've been waiting for an RSC story that doesn't ask you to invert your whole architecture, this is it. RSC support in TanStack Start is [experimental and ready to play with](/start/latest/docs/framework/react/guide/server-components). - -Let's build something amazing together. diff --git a/src/blog/who-owns-the-tree.md b/src/blog/who-owns-the-tree.md new file mode 100644 index 00000000..69e43a47 --- /dev/null +++ b/src/blog/who-owns-the-tree.md @@ -0,0 +1,187 @@ +--- +title: 'Who Owns the Tree? RSC as a Protocol, Not an Architecture' +published: 2026-04-28 +draft: true +excerpt: RSC is usually framed as a single architecture where the server owns the tree. But it's also a protocol, and the protocol supports more than one composition model. The overlooked question is who owns the tree. +authors: + - Tanner Linsley +redirect_from: + - rsc-is-a-protocol-before-it-is-an-architecture + - who-owns-the-tree-rsc-is-a-protocol-not-an-architecture +--- + +![Who Owns the Tree?](/blog-assets/who-owns-the-tree/header.jpg) + +A few weeks ago we shipped [React Server Components Your Way](/blog/react-server-components), and the most common follow-up was: + +> Why even bother with two RSC composition models? Pick one. + +When most people talk about RSCs, they're usually referencing one specific architecture where the server owns the tree, `'use client'` marks the holes, and the framework stitches everything together at hydration. That's the model that people have in their heads when they say "RSC support". + +It's an important model and even more important that people know and understand that TanStack Start supports it, but... + +RSC is also a _protocol_, a way to serialize rendered React output, client refs, and non-JSON stuff into a stream that can be streamed to the client and reconstructed. The "conventional" server-owned tree is just **one way** to use that protocol. + +**So, who owns the tree?** + +If and when the server owns it, you need a way to drop client interactivity _into_ it, which is exactly what `'use client'` is for. Hopefully this doesn't come as a surprise. It's in the react docs and how pretty much every RSC framework model works to this day, includin Start. + +However, if the _client_ owns it, you'll need a way to drop _server-rendered UI into it_. That's what Composite Components solve in TanStack Start. + +Both of these models are powered by the same protocol. + +## It matters more often than you think + +Alright, imagine you're building a dashboard where the client owns almost everything: tabs, filters, drag layout, optimistic updates, the command palette, all of it. But one chart happens to pull from a crappy and slow analytics API, runs a bunch of server-only computation and happens to require hundreds of kilobytes of charting code just to produce the markup. + +In the server-owned model approach, the obvious solution is to invert your whole route to the server, marking every interactive component along the way with `'use client'` and hope that the boundaries land where you want them. + +Don't get me wrong, this totally works. People do this every day in Next.js. But what you _really_ just did was adopted a server-first architecture for your whole app just to render _one single server-shaped region_ into a mostly client-side controlled tree. + +Now imagine the opposite being possible. Just keep the dashboard client-owned, ask the server through api/server function for the rendered chart markup, then drop it into the tree wherever you want, alongside whatever client state you already have. + +```tsx +import { createFromReadableStream } from '@tanstack/react-start/rsc' + +function Dashboard() { + const { data: chart } = useSuspenseQuery({ + queryKey: ['analytics-chart', range], + queryFn: async () => + createFromReadableStream(await getAnalyticsChart({ data: { range } })), + }) + + return ( + + + + + {/* Server-rendered output, dropped into a client-owned tree */} + {chart} + + + + + + + ) +} +``` + +The seam just moved a bit, and it's more transparent than what you're used to. And guess what, it's a totally valid and _awesome_ way to use the RSC protocol. + +## Both models can reach the extremes + +There is a fun symmetry here where both models can reach the _opposite_ extreme pretty easily. + +If you use a Server-owned model, just push `'use client'` high enough in the tree and the route effectively becomes an SPA. The server still owns the outer entry, but everything below the boundary is client-composed. + +If it's client-owned, render a server component as high as you can in the tree and the route suddenly behaves like a server-rendered page. + +Something interesting to point out though is that in \*both models, the client still receives and composes the result, even if that result is fully server-rendered\*\*. + +So the real distinction isn't which model can technically reach which outcome. Both can do it. It's more about which side the framework _naturally pulls you toward_, and how much friction you hit when you move the other direction. + +If you're shipping an eventual-SPA, the client is the final destination of the UI whether you like it or not, which is why I frequently make the case that the "client-owned" composition model with strong server composition primitives covers more ground and capability than the other. It keeps the control layer rooted where the user experience actually runs while still letting you do server-rendered regions, server-only computation, streaming, caching, and progressive enhancement wherever they make sense. + +## Why most frameworks only ship half + +The standard RSC model assumes server-owned trees, so the primitives are designed around that direction. `'use client'`, hydration boundaries, streaming, suspense fences, manifest-driven reference resolution all assume the server is composing and the client is receiving. + +That's fine for what those frameworks were built for and \*\*TanStack Start supports `'use client'` exactly the same. + +However, other frameworks don't really have a great answer when you ask: + +> How do I render this server fragment inside a client tree I'm already composing? + +or + +> How can I fetch and cache a server component from a useQuery/useEffect? + +The closest answer is usually "make the client thing a `'use client'` boundary inside a server tree" which works when the route is server-shaped, but when the route is _client-shaped_, you're inverting the entire architecture for little ROI. + +**Composite Components fill that gap.** A server function returns a rendered React fragment as Flight data. The client passes it to `` and provides slots through `children` or render props. The server-rendered fragment can position those slots, but the client still owns the surrounding tree. + +```tsx +// server +const getPost = createServerFn().handler(async ({ data }) => { + const post = await db.posts.get(data.postId) + return { + src: await createCompositeComponent( + ({ children }: { children?: React.ReactNode }) => ( +
+

{post.title}

+

{post.body}

+
{children}
+
+ ), + ), + } +}) + +// client +function PostPage({ postId }: { postId: string }) { + const { data } = useSuspenseQuery({ + queryKey: ['post', postId], + queryFn: () => getPost({ data: { postId } }), + }) + + return ( + + + + ) +} +``` + +This is _additive_ and textbook **inversion of control**. Yes, start still supports `'use client'` when you want server-owned composition, but Composite Components enable the same idea from the opposite control plane. + +This isn't even foreign to React's original framing. The first Server Components RFC describes Client Components as the regular components you already know, and notes that Server Components can pass other Server Components _as children_ to Client Components. From the Client Component's perspective, that child is already rendered output. The same RFC also describes granular refetching from multiple entry points as part of the design direction even though the initial demo refetched the whole app. + +So ironically, this inverse model is _even closer_ to the original concepts by taking the protocol seriously and exposing a composition direction the protocol already makes possible. + +## A powerful primitive, not the whole pipeline + +"RSC support" is a phrase that feels overused and overstated to mean something like "correct" or "blessed", which is a weird thing to say about a primitive. I think more accurately, frameworks are using the RSC primitives in the way that has been **revealed and marketed to them thus far**, which is fine if that model covers your use cases. Ours needed more. + +RSC is more than serialization. It's React's attempt to bring data fetching, streaming, code splitting, server access, and client interactivity into one coherent model. Worthy goal, but it's still _one way_ to organize those concerns, and most of those concerns already have great answers elsewhere in the ecosystem. + +Routing solves waterfalls. Loaders kick off data work before render. Query libraries dedupe, cache, prefetch, stream, and invalidate. HTTP and CDNs cache responses. Server functions expose backend work without dragging the whole tree onto the server. + +So the question isn't whether RSC _can_ solve these problems. It can. The question is whether routing every concern through a rigid RSC architecture should be the **default answer for every app**. + +Start's answer is no. RSC is a powerful primitive in the pipeline, not the pipeline itself. Use it where rendered server UI is the right abstraction, and use the rest of the toolkit where those fit better. That's not rejecting RSC, it's rejecting RSC-as-silver-bullet for problems smaller, more composable tools already solve. + +That same instinct is why Start also doesn't ship a caching directive. + +## Why Start doesn't ship a caching directive + +People keep asking why we don't ship something like Next's `"use cache"`. Short answer: that directive assumes a framework- or platform-owned persistence layer, and Start can't honestly make that assumption. + +The directive marks something cacheable and the runtime handles the rest; keys, storage, invalidation, durability, cross-instance sharing, all the deployment-specific stuff. Which means the framework (or the platform under it) has to own the persistence contract. The directive doesn't _eliminate_ that question, it just hides it behind a one-liner at the call site. + +That works great when your framework is married to a specific platform. Start isn't. Cloudflare Workers, Netlify, Vercel, Node, Bun, Railway, any Nitro target; there's no single portable persistence layer across all of that, so there's no honest directive shape that means the same thing everywhere. + +Start takes the transparent route instead. A server function returns a Flight stream as bytes, and those bytes can be cached at any layer you already control. + +| Layer | Cache option | +| ----------- | ------------------------------------------------------- | +| Render pass | `React.cache` to dedupe calls within a render | +| Server | Redis, KV, Postgres, in-memory LRU, or whatever you use | +| Network | HTTP caching and `Cache-Control` headers | +| Client | Router cache, TanStack Query, or any client-side store | + +`createFromReadableStream` decodes the bytes at render time, after the cache boundary. The cacheable primitive isn't a directive that hides persistence; it's transparent RSC output flowing through cache layers your app already understands. + +A directive is the right shape when the framework and platform can own the cache contract. Transparent bytes is the right shape when the framework needs to stay portable. We chose portable. + +## Closing + +The better question isn't "does this framework support RSC?" It's: + +> Which RSC composition models does it expose? + +TanStack Start exposes both, and you can mix them per route, per component, per use case. Same TanStack philosophy as always: you know what's best for your application, and the framework should get out of the way. + +--- + +If you've been waiting for an RSC story that doesn't ask you to invert your whole architecture, this is it. RSC support in TanStack Start is [experimental and ready to play with](/start/latest/docs/framework/react/guide/server-components).