RSC or Raw Data? Designing a Representation-Aware UI Transport
This is an architecture proposal, not a claim that today's RSC frameworks already negotiate transports this way. The APIs and headers in this article are intentionally hypothetical.
Assume a full-stack framework needs to make resource flows and transactions performant across the client and server: loading, rendering, mutating, caching, invalidating, and synchronizing them without forcing every workload through the same path. It may also provide batteries-included integrations with third-party services such as authentication, databases, payments, queues, observability, and deployment platforms.
In this article, that is what a truly full-stack framework means. It is about more than colocating frontend and backend files. It means understanding where data, computation, rendering, state, caching, and external-service boundaries belong, then choosing the safest and least expensive way to move a result between them.
I keep coming back to one question in the React Server Components discussion:
Is sending the rendered UI actually cheaper than sending the source data and reusing a renderer that is already in the browser?
Ryan Carniato's discussion of the RSC tradeoff sharpened that question for me.
The honest answer is: it depends on the workload.
That sounds obvious, but mainstream framework integrations still tend to choose mainly at the application or route level. An application is server-first or client-first. A route returns Flight or it returns loader data. Newer experiments are creating smaller boundaries, but the framework usually decides who owns the tree and every feature downstream inherits that decision.
I don't think the next useful RSC optimization is another directive or slightly better compression.
I think we are missing a representation-aware UI transport system: an application-level architecture that understands HTML, raw data, and an RSC Flight stream as different representations of the same application result, measures their real cost, and selects the right representation for a route segment or UI region.
Not based on ideology.
Based on bytes, code reuse, CPU, cache state, privacy, navigation shape, and whether React state must survive around the update.
The False Binary
The usual argument is framed like this:
-
RSC sends rendered UI, so the renderer stays on the server
-
Client data sends JSON, so the browser must ship and execute the renderer
Both statements are true, but neither one is a complete cost model.
Using rounded figures from TanStack's measured RSC experiment, suppose a documentation route has these incremental transfer costs:
Raw markdown for one page: 9.4 KB
Rendered Flight tree: 15.0 KB
Additional client renderer: ~18.5 KB once
TanStack reported roughly 27 KB for the complete client renderer, about 18–19 KB more client JavaScript than its RSC version. The midpoint is useful here because we are comparing the incremental cost of choosing data instead of Flight.
For one applicable content request, Flight looks cheaper:
Data mode: 18.5 KB renderer + 9.4 KB data = 27.9 KB
Flight mode: 15.0 KB
But the renderer is reusable. After it has been downloaded, every later request only pays for the source data:
Four pages with data: 18.5 + (4 × 9.4) = 56.1 KB
Four pages with Flight: 4 × 15.0 = 60.0 KB
By the fourth page, data mode wins on transfer size.
The break-even equation is:
J + (N × D) < N × F
N > J / (F - D)
Where:
-
Jis the reusable client renderer cost -
Dis the data payload per navigation -
Fis the Flight payload per navigation -
Nis the number of navigations in the session
For the example:
N > 18.5 / (15.0 - 9.4)
N > 3.30
This is the important part of TanStack's result: once the browser renderer became small enough, repeatedly sending raw content was cheaper than repeatedly sending the rendered Flight representation. That conclusion is specific to requests that reuse the renderer; an average session length alone does not prove that every navigation has the same economics.
But reverse the workload:
Server-only database input: 2 MB
Client chart renderer: 90 KB
Rendered Flight region: 18 KB
Now shipping the raw input is not only expensive, it may be a security mistake. Assuming the rendered chart is a safe projection, the browser does not need two megabytes of server-only rows just to display twelve bars.
In that case, server rendering is doing exactly what it should:
large/server-only input + server renderer → safe, smaller UI projection
Neither transport wins universally.
The workload wins.
What RSC Actually Adds
RSC is not just "JSON, but for JSX."
The Flight stream can describe:
-
React element structure
-
Client component references
-
Serializable props
-
Promises and Suspense boundaries
-
Module references understood by the bundler runtime
-
Incrementally arriving parts of a React tree
The server executes Server Components, and the Flight renderer serializes their result. The browser decodes that result into React's model and reconciles it into a live application.
The official React Server Components documentation describes the critical bundle property: Server Component implementation code and server-only dependencies do not have to become browser JavaScript.
The special architectural property is this:
server produces new React UI
↓
Flight carries the result
↓
React reconciles it into a live client tree
That is how surrounding client state can survive a navigation.
But we need to be precise. RSC does not promise to preserve every state value anywhere on the page. React preserves state when the matching client component keeps the same type, key, and structural position during reconciliation.
If a navigation replaces the component identity or moves it across an incompatible boundary, it remounts and its state resets. That is normal React behavior.
So the accurate statement is:
RSC lets the server produce new UI while React can preserve matching client components around that update.
That is much more interesting than "server rendering," because an arbitrary HTML fragment does not carry React element identity or client references. DOM-morphing and island systems can preserve selected DOM or island state, but replacing React-owned DOM with innerHTML does not preserve the React component instances that owned it.
An Existing Primitive I Want to Explore Further
React already supports rendering trusted HTML through dangerouslySetInnerHTML. A Server Component can produce sanitized HTML and place it inside a stable React-owned boundary today. This proposal is not claiming that HTML inside an RSC application is a new capability.
The exploration is whether a framework can make that choice deliberately. A compiler could first prove that a subtree contains no Client Components or independently updating React state, then compare a normal Flight element tree with an opaque HTML representation or reusable source data. Standard Flight would remain the fallback whenever React needs ownership of the interior.
This might reduce rendered-tree expansion, client reconstruction, and server rendering work for large host-only regions. It might also lose on small content, highly interactive UI, or content whose compressed Flight representation is already efficient. The useful question is not whether opaque HTML is possible. It is whether a framework can identify the safe cases and demonstrate a real end-to-end improvement with measurements.
A Controlled Benchmark of This Approach
I built a production benchmark around the existing primitive. One path serializes a host-only React element tree through Flight. The other renders the same content to sanitized HTML with native Rust, places it in a Server Component through dangerouslySetInnerHTML, then sends that wrapper through standard Flight. The browser still decodes a real Flight response and React still performs the update.
I ran 8 warmups followed by 50 measured navigations per representation:
| Fixture | Normal Flight, Brotli | HTML inside Flight, Brotli | Median React commit | Warm server work reduction |
|---|---|---|---|---|
| Small | 2.0 KiB | 1.3 KiB, 33.8% less | 1.1 ms to 0.6 ms | 86.7% |
| Medium | 4.8 KiB | 2.5 KiB, 48.1% less | 4.2 ms to 2.2 ms | 90.9% |
| Large | 14.8 KiB | 6.2 KiB, 57.8% less | 14.5 ms to 7.4 ms | 94.7% |
The surrounding client shell kept the same state across every navigation. That is the important hybrid property: Flight still owns composition and client identity around the boundary, while the large static interior arrives as one opaque HTML fragment.
These results support further exploration, not a general claim that HTML beats Flight. The fixture is synthetic and contains only host elements. Transfer and browser work were measured over loopback with precomputed Brotli payloads, while warm server production was measured separately. It excludes Client Component references, Suspense waterfalls, database work, CDN latency, and initial HTML duplication. Most importantly, React cannot reconcile or independently update nodes inside the opaque fragment. A framework should select this representation only when it can prove that the interior does not need React ownership.
There are still several places where Flight can become expensive:
-
Rendered-tree expansion: compact source data can produce a much larger serialized element tree.
-
Initial duplication: some SSR integrations send visible HTML alongside a corresponding Flight model. React and Next.js have discussed reusing text already present in the DOM to reduce this duplication.
-
Repeated navigation payloads: shared structure may be transferred again unless the router caches and requests Flight at the segment level.
-
Server work and waterfalls: data loading, sequential async components, and repeated rendering can cost more than encoding the payload.
-
Client reconstruction: large Flight trees create JavaScript objects that React must decode, reconcile, and commit.
These are reasons to measure the whole path, not proof that Flight's JavaScript codec is universally slow.
Three Representations, Three Different Costs
For a navigation, a framework can theoretically return at least three useful representations.
| Representation | Typical content type | Browser responsibility |
|---|---|---|
| HTML | text/html | Parse a document or fragment |
| Data | application/json or another data format | Run a reusable renderer |
| Flight | commonly text/x-component | Decode and reconcile a React tree |
HTML is excellent for the initial document, crawlers, no-JavaScript access, static delivery, and multi-page applications.
Raw data is excellent when:
-
The source is compact
-
The renderer is already cached
-
The UI is highly interactive
-
The client owns the surrounding tree
-
The same renderer will be reused many times
Flight is excellent when:
-
The raw input is large or must remain server-only, and the rendered projection is safe to expose
-
The server renderer has expensive dependencies
-
The realized UI is much smaller than its source
-
The server needs to select UI structure
-
The result must compose with a live React tree
These representations are not interchangeable byte containers. They move different responsibilities across the network boundary.
A Byte Is Not Just a Byte
Transfer size alone is not enough.
The more useful model is a normalized score:
TotalCost(mode) =
w_network × estimated_transfer_time
+ w_latency × round_trip_time
+ w_server × server_cpu_time
+ w_client × client_parse_and_render_time
+ w_js × uncached_renderer_cost
+ w_cache × cache_miss_cost
The weights are device-, product-, and workload-dependent. They convert different units into a decision score; adding raw bytes directly to milliseconds would not be meaningful.
A 20 KB renderer on a desktop with a warm HTTP cache is not the same as 20 KB of new JavaScript on a low-end phone. A 15 KB Flight response generated by an expensive database query is not equivalent to a 15 KB CDN-cached JSON file. A private response accidentally stored in a shared cache is not a performance tradeoff at all. It is an incident.
Correctness, authorization, privacy, and compatibility do not belong in this weighted equation. They are eligibility constraints that run before cost scoring.
A real adaptive transport system needs to measure:
-
Brotli or gzip transfer bytes, not only uncompressed object size
-
Whether renderer chunks are already loaded
-
JavaScript parse, compile, and execution time
-
Flight decoding and reconciliation time
-
Server render duration
-
Data-fetch duration
-
TTFB and number of round trips
-
CDN, router, data, and module cache hits
-
Expected navigation count
-
Device and network constraints
-
Public, private, tenant, and user-specific cache scope
This is why "Flight was 15 KB and JSON was 9 KB" is useful evidence, but not the whole decision.
The Missing Layer Is an Adaptive UI Transport System
I started by calling this a smart proxy, but adaptive UI transport system is more accurate.
The system has three distinct pieces:
-
An AOT analyzer that discovers valid variants and estimates their costs
-
A runtime transport negotiator that selects among those prepared variants
-
A small negotiation protocol that defines request hints, response envelopes, versioning, and recovery semantics
That protocol does not replace HTTP, TCP, or Flight. Flight remains the React representation protocol; this narrower contract only coordinates representation selection between the build system, server, cache, router, and browser.
The word proxy makes it sound like an intermediary could receive a Flight tree and magically convert it back into raw source data. It cannot.
Rendering is generally lossy:
source data → UI result
Once a markdown document becomes headings, paragraphs, highlighted tokens, and component references, the original markdown is not recoverable in a general way.
The transport system therefore cannot invent representations after the fact.
Both representations must be prepared from a common semantic resource:
┌─→ data representation
request → resource load ─┤
└─→ server-rendered Flight representation
The transport negotiator selects between valid, predeclared outputs. It does not transcode one into the other.
That distinction makes the architecture possible.
The Architecture I Have in Mind
At a high level:
BUILD TIME
routes + module graph + policies + representative samples
│
▼
AOT transport analysis
│
▼
transport capability manifest
REQUEST TIME
navigation + client capabilities + cache state
│
▼
runtime transport negotiator
┌───────┼────────┐
▼ ▼ ▼
HTML DATA FLIGHT
│ │ │
└───────┼────────┘
▼
stable client boundary
│
▼
telemetry for the next build
The system has two planes:
-
Control plane: analyzes, scores, selects, caches, and observes representations
-
Data plane: actually sends HTML, data, or Flight
Keeping these separate matters. The RSC renderer should remain responsible for producing Flight. The data serializer should remain responsible for data. The router should remain responsible for navigation. The adaptive system coordinates them without pretending they are the same protocol.
This would not land in one Vite or RSC plugin. A bundler integration can emit module graphs, chunk costs, and the capability manifest. The framework router owns route-tree state and stable UI boundaries. A server adapter exposes the prepared data and Flight variants. The browser runtime decodes the selected representation and handles recovery. The cache and observability layers make the decision operable.
AOT: Decide What Is Possible Before Deployment
Ahead-of-time analysis should be the default.
At build time, the framework already knows a surprising amount:
-
Route and layout graph
-
Server and client module graphs
-
"use client"boundaries -
Client chunk sizes
-
CSS dependencies
-
Whether a browser renderer exists
-
Whether a route imports server-only modules
-
Static params and prerenderable routes
-
Which regions are shared across navigations
-
Cache and privacy declarations
Those are build-time facts. Dynamic data and Flight payload sizes are not. Their estimates require representative fixtures, profiling traces, or production telemetry, and the build report should show their sample source and confidence rather than present them as exact predictions.
The build can use this information to generate a capability manifest:
{
"buildId": "b42",
"segments": {
"docs/[slug]#body": {
"variants": {
"data": {
"endpoint": "/__ui/data/docs/:slug",
"renderer": "docs-body@7d91",
"chunks": ["/assets/docs-body.7d91.js"],
"estimatedGzip": 9200
},
"flight": {
"endpoint": "/__ui/flight/docs/:slug",
"estimatedGzip": 15100
}
},
"policy": {
"sensitivity": "public",
"default": "data"
}
}
}
}
This manifest is not trying to predict every runtime response perfectly. Dynamic data makes that impossible.
Its job is to declare:
-
Which transports are valid
-
Which renderer chunks each data variant needs
-
What the baseline cost looks like
-
Which choices are forbidden
-
How responses must be cached and versioned
Then the framework can provide a build report:
Segment: docs/[slug]#body
Median sampled raw data: 9.4 KB gzip
Median sampled Flight: 15.0 KB gzip
Incremental client renderer: 18.5 KB gzip once
Estimated break-even: 4 navigations
Public cacheability: yes
Recommendation: data
The recommendation can be explicit and reviewable instead of hidden framework magic.
JIT: Select Among Prepared Variants
Just-in-time selection becomes useful when the correct answer depends on the current session.
Imagine two visitors opening the same route:
Visitor A:
- renderer is already loaded
- six-page session
- warm data cache
Visitor B:
- renderer is not loaded
- first navigation
- slow network
Data may be cheaper for Visitor A while Flight is cheaper for Visitor B.
A request could carry versioned, client-reported capability hints:
GET /__ui/docs/rsc HTTP/1.1
Accept: application/json, text/x-component;q=0.9
UI-Build: b42
UI-Tree: root/docs
UI-Renderer: docs-body@7d91
And the response could make the decision visible:
Content-Type: application/json
UI-Transport: data
UI-Segment: docs/rsc#body
UI-Build: b42
Server-Timing: ui-select;dur=0.4, data-load;dur=8.7
This resembles normal HTTP content negotiation. Accept and Vary already exist for selecting representations.
These hints are untrusted. The server can check build and renderer IDs against its manifest, but it must never use them for authorization or privacy decisions.
HTTP negotiation also has known costs: the server cannot perfectly know what is best for the user, capability headers can reduce shared-cache reuse, and custom headers can trigger CORS preflights on cross-origin requests. JIT selection has to earn that complexity.
For public content, separate representation URLs may be simpler:
/__ui/data/docs/rsc
/__ui/flight/docs/rsc
The negotiator can select the URL, while the CDN stores each representation under a stable key.
JIT should also be guarded by AOT output. The runtime may choose only from variants that were built, versioned, and declared compatible.
Otherwise "smart transport" becomes "surprise production branch nobody tested," which is a very sophisticated way to ruin a Friday.
Hard Constraints Must Run Before Cost Scoring
The negotiator should not begin by asking which payload is smaller.
It should begin by eliminating invalid choices.
1. Is the data safe to serialize to this client?
2. Does a compatible client renderer exist?
3. Does the browser have or support the required runtime?
4. Is the response public, tenant-scoped, user-scoped, or private?
5. Can this region change transport without violating state ownership?
6. Does the current build match the browser build?
7. Is this an initial document or a client navigation?
8. Which remaining representation has the lowest measured cost?
For example:
function eligibleTransports(ctx) {
const modes = new Set(['html', 'data', 'flight'])
if (ctx.navigation) modes.delete('html')
if (!ctx.manifest.hasCompatibleRenderer(ctx.rendererId)) modes.delete('data')
if (ctx.resource.sensitivity === 'server-only') modes.delete('data')
if (!ctx.client.supportsFlight) modes.delete('flight')
if (ctx.client.buildId !== ctx.server.buildId) {
return new Set(['document-reload'])
}
return modes
}
Only after these constraints pass should the negotiator compare estimated cost.
Privacy is not a weight in an optimization equation. It is a hard boundary.
Whether the compatible renderer is already loaded affects the cost of data mode, not its eligibility. A missing-but-loadable chunk is part of the data path's transfer and recovery cost.
Renderer Availability Is the Hard Part of JIT
The server does not automatically know what remains in the browser's HTTP cache or JavaScript module registry.
It can infer some things:
-
The current build ID
-
The route segments already acknowledged by the client
-
Renderer versions explicitly reported by the runtime
-
Whether a session previously requested a module
But those are hints, not perfect knowledge. The browser may evict resources. A service worker may have a different cache. A tab may be restored after deployment.
So data mode must remain recoverable:
negotiator selects data
↓
client checks renderer
↓
renderer exists ──────────────→ render data
│
└─ missing → load chunk → render data
│
└─ failure → retry as Flight or reload
The full cost of data mode is therefore:
data payload + missing renderer chunks + client render work
Not merely the JSON response.
This is also why AOT-only selection is a great first implementation. It is predictable, cache-friendly, and much easier to debug.
JIT should be an optimization layered on top, not the foundation holding correctness together.
Choose Per Segment, Not Per Application
The route is already too large a unit for this decision.
A single page may contain:
-
A stable static shell
-
A client-owned filter bar
-
A server-rendered chart whose source rows must remain server-only
-
A markdown article rendered from compact source
-
A comments panel loaded as ordinary data
Choosing one transport for the whole page throws away the strengths of the others.
The useful unit is a route segment or UI resource boundary:
dashboard layout → cached shared Flight segment
├── filters → client component
├── protected analytics chart → Flight resource
├── activity table → raw data + client renderer
└── help article → static HTML or raw markdown
This lines up with work already happening across the ecosystem.
Next.js's client router cache stores RSC results by route segment and reuses shared layouts. React Router's unstable RSC APIs allow route-level server or client ownership. TanStack's currently experimental RSC-as-a-primitive model goes further by allowing a client-owned tree to fetch server-rendered React regions.
The architectural lesson is not that one of these frameworks has found the final abstraction.
It is that tree ownership and transport selection need smaller boundaries.
Client-Owned and Server-Owned Trees Can Coexist
The usual RSC model is server-owned:
Server tree
├── Server Component
├── Server Component
└── Client Component boundary
But a client-owned application can also fetch a Flight resource:
Client tree
├── stateful filters
├── stateful tabs
└── server-rendered chart resource
That second model is important for dashboards, editors, builders, and long-lived applications.
The client remains responsible for interaction and state. A particular region is delegated to the server because its raw input must remain server-only, its renderer is heavy, or its result is cheaper to transfer as UI.
Conceptually:
'use client'
function Dashboard() {
const [range, setRange] = useState('30d')
const chart = useUIResource(analyticsChart, { range })
return (
<>
<RangePicker value={range} onChange={setRange} />
<Suspense fallback={<ChartSkeleton />}>{chart}</Suspense>
</>
)
}
The resource might declare two representations:
export const analyticsChart = defineUIResource({
key: ({ range }) => ['analytics-chart', range],
load: async ({ range }) => {
return getAnalyticsRows(range)
},
transports: {
flight: {
render: (rows) => <ServerChart rows={rows} />,
},
data: {
render: ClientChart,
},
},
policy: {
mode: 'auto',
sensitivity: 'server-only',
},
})
For this resource, the negotiator removes data before scoring because rows must not cross the boundary. The Flight projection is eligible only if everything it serializes is safe for this user to receive.
For a public documentation resource, both variants may remain eligible.
The API is not the important part. The important part is that the resource declares semantics, representations, and policy separately.
The variants also need contract tests. A data renderer and a Flight renderer for the same resource must agree on meaning, accessibility, authorization behavior, loading states, and errors. Adaptive selection is unsafe if the alternatives silently become different products.
State Preservation Needs a Stable Ownership Contract
An adaptive transport system cannot freely alternate between data and Flight inside any arbitrary subtree and expect all state to survive.
Suppose data mode renders:
<ClientMarkdown document={data} />
But Flight mode returns:
<article>{serverRenderedDocument}</article>
Those are different React structures. Switching between them can remount the region.
A safe design needs a stable outer boundary:
<TransportSlot id="article-body">{representation}</TransportSlot>
The surrounding client application can remain stable, while the contents of the slot follow explicit rules.
State can be classified:
-
Outside the slot: expected to survive transport changes
-
Inside a stable client boundary: survives if type, key, and position match
-
Inside representation-specific content: may reset when transport changes
This should be visible in developer tools. A framework must not market "state preservation" while silently changing ownership under a component.
RSC gives us safe React reconciliation. The transport layer still has to provide stable identities.
Flight Is Not Automatically a Minimal Delta
Another common misunderstanding is that every RSC navigation is automatically a tiny tree diff.
Flight is capable of carrying streamed React model updates, but the framework decides:
-
Which server subtree to execute
-
Which route segments to request
-
Whether shared layouts are reused
-
What the navigation payload contains
-
How the client router caches previous segments
A naive framework can render and serialize the entire server tree again on every navigation. React may still preserve matching client state while the network repeatedly carries shared server output.
A smarter router should send its current route-tree state:
current: root / dashboard / profile
next: root / dashboard / settings
When the shared segments are still valid, the server can find the longest common prefix:
reuse: root / dashboard
replace: profile → settings
Then only the changed segment needs a navigation response. Changed parameters, invalidated data, or loading and error boundaries can require more than one segment, so this is a router decision rather than a universal Flight guarantee.
This optimization belongs above the Flight codec. It is routing and cache policy, not a new React wire format.
The Cache Is Part of the Protocol
A hybrid transport layer needs more than one cache:
HTTP/CDN cache
├── HTML representation
├── data representation
└── Flight representation
Browser runtime
├── JavaScript module cache
├── data/query cache
├── Flight segment cache
└── router tree cache
Every cache key needs enough information to prevent incompatible reuse:
resource identity
+ params
+ representation
+ build/protocol version
+ locale
+ cache partition (public, tenant, or user; never raw credentials)
+ relevant route-tree state
Responses should also declare:
-
Cache-Control -
ETagor another validator where useful -
Revalidation/invalidation tags
-
Public versus private scope
-
Deployment or protocol version
User-specific responses should normally be private or no-store; when application caching is intentional, they need an explicit stable user or tenant partition. Raw Authorization or cookie values should not become cache-key material.
If a personalized Flight response is cached under the same key as a public response, the transport system has not optimized the application. It has created a data leak with excellent latency.
The HTTP Vary response field can identify request headers that influenced representation selection, but every new dimension can fragment the cache. Stable representation URLs are often easier to operate.
Versioning Is Not Optional
Flight payloads reference modules from a particular build. Data renderers also belong to a particular build.
The client and server must agree on:
-
Deployment ID
-
Renderer ID
-
Client-reference manifest
-
Flight runtime compatibility
-
Route-tree schema
A response envelope might begin with:
type UIEnvelope = {
buildId: string
routeId: string
segmentId: string
representation: 'data' | 'flight'
rendererId?: string
invalidations?: string[]
}
If the browser sends buildId=b41 and the server only understands b42, the safe behavior is usually a document reload.
Trying to reconcile a new Flight payload against old client-reference chunks is not progressive enhancement. It is protocol roulette.
React also warns that the lower-level APIs used by bundlers and frameworks do not necessarily follow normal semver guarantees across minor versions. A transport system should treat the Flight runtime as a versioned implementation detail, not a forever-stable public interchange format.
Observability Has to Drive the Decision
Without measurements, "auto" is just a nicer spelling of "guess".
For every region and navigation, I would record:
-
Selected transport
-
Reason for selection
-
Compressed response bytes
-
Renderer chunks requested
-
Flight client-reference chunks requested
-
Server data-load duration
-
Server rendering duration
-
First-byte and completion timing
-
Client decode/render duration
-
Cache result at every layer
-
Whether the region remounted
-
Whether fallback or reload was required
The server can expose selection and render timing through the standardized Server-Timing header:
Server-Timing:
ui-select;dur=0.4;desc="data",
data-load;dur=8.7,
serialize;dur=1.1
Developer tools should show something like:
docs/[slug]#body
selected data
reason renderer already loaded
data transfer 8.9 KB gzip
avoided Flight 15.3 KB gzip estimated
client render 4.2 ms
session navigations 6
cache CDN hit
state outer boundary preserved
The feedback loop can then improve the next build's AOT estimates:
runtime telemetry → route profiles → next build recommendation
This is where the system becomes genuinely smart instead of merely configurable.
Failure Modes Need First-Class Design
Hybrid transport creates more branches, so it needs boring, explicit recovery.
Renderer chunk failure
If data arrives but its renderer chunk cannot load:
retry chunk → request an independent Flight variant if available → full reload
The Flight fallback is independent only when it does not require the same failed client chunk.
Flight decode failure
If the Flight runtime or manifest is incompatible:
invalidate segment cache → full reload
Negotiator timeout
Selection should have a deterministic default produced at build time:
negotiator unavailable → use AOT default
Cache disagreement
If the resource version and cached representation differ:
discard stale variant → fetch current build
Offline navigation
Use whichever complete representation is already available:
cached data + cached renderer
or cached Flight segment
or cached HTML document
An architecture is not finished when the optimal path works. It is finished when each layer can fail without inventing corrupted UI state.
Security Rules
RSC can prevent server implementation code and unpassed source data from reaching the browser, but Flight is not an encrypted secrecy tunnel.
Anything serialized into:
-
Host element props
-
Client component props
-
Error messages
-
Server function return values
has crossed the boundary.
The transport system therefore needs an explicit sensitivity policy:
type Sensitivity = 'public' | 'tenant' | 'user' | 'server-only'
And those policies should control:
-
Whether data transport is legal
-
Which cache scopes are legal
-
Which fields may be serialized
-
Which auth checks must run
-
Whether telemetry may include sizes only or payload samples
The transport system is an optimization and negotiation mechanism. It must not become an authorization layer.
Authentication and authorization still run at the origin of the resource, regardless of which representation is selected.
What I Would Build First
I would not begin with fully automatic JIT negotiation.
I would build this in five stages.
1. Measurement
Produce route- and segment-level reports for:
-
Data size
-
Flight size
-
Client renderer size
-
Server and client CPU
-
Repeated layout bytes
-
Navigation count
2. Explicit Representation and Execution Modes
Do not conflate wire representation with render timing or tree ownership. Let a developer choose each axis explicitly:
representation: 'html' | 'data' | 'flight'
renderAt: 'build' | 'request'
ownership: 'client' | 'server'
per route segment or UI resource.
3. Segment-Aware Flight
Reuse shared layouts and fetch only changed route branches.
This probably saves more than trying to compress a repeatedly transmitted full Flight tree.
4. AOT Recommendations
Generate a transport manifest and explain the recommendation:
recommended: data
because: renderer amortizes after four navigations
5. Guarded JIT
Let the runtime choose only when:
-
Multiple variants were built
-
State ownership is compatible
-
Cache keys are correct
-
The client reports a compatible build
-
A deterministic fallback exists
This order keeps correctness ahead of cleverness.
What This Is Not
This proposal is not:
-
A replacement for RSC
-
A universal argument for client rendering
-
A proxy that converts Flight back into source data
-
A reason to ship two implementations for every component
-
A guarantee that the smallest response has the best UX
-
A license to switch component ownership invisibly
It is a way to stop treating a representation choice as an application-wide religion.
My Current Take
RSC has a distinctive combination that ordinary client-data systems do not provide by themselves:
The server can produce React UI without shipping the Server Component implementation, and React can merge that result around already-running client components.
That deserves exploration.
But the large-input-to-small-UI equation is workload-dependent. Sometimes the raw source is already the more compact representation. Sometimes a small renderer amortizes over a long session. Sometimes the raw input must remain server-only and only a safe projection should reach the browser. Sometimes the UI is static enough that neither Flight nor client rendering should be involved after the first build.
The smarter architecture is not:
everything is Flight
or:
everything is JSON
It is:
static content → HTML/static asset
small reusable source → data
large or server-only source → Flight, if the projection is safe
server-heavy region in a client app → Flight resource
shared server layouts → cached Flight segments
With AOT deciding what is legal and likely efficient, JIT selecting among prepared variants when runtime context genuinely matters, and telemetry proving whether the decision helped.
The future I want is not server-first or client-first.
It is representation-aware.
The framework should understand the cost of the thing it is sending before it asks the network to carry it.
Further Reading
Want to challenge the model or help prototype it? Find me on X or Telegram. If you want to take part in the framework's early exploration, email me at kinfetare83@gmail.com. I would love to hear where this breaks.