internal/analysis/nextjs-turbopack-rsc-vs-react-on-rails-pro.md
Date: 2026-06-17
This is a deep architecture note for understanding how Next.js App Router, React Server Components, and Turbopack work together, and how that compares to React on Rails Pro RSC with Shakapacker, webpack, and Rspack.
Source snapshot:
v16.3.0-canary.42main at
bad061fbd693356009317b4b153cc48f20393ffe17.0.0-rc.5, with the default RSC generator still on the stable
React ~19.0.4 plus [email protected] path, and the Pro dummy/CI path soaking React
19.2.7 with [email protected]Imagine a web page is a toy castle.
Traditional SSR sends a castle picture plus a big toy box, then React checks the picture and attaches buttons. RSC sends the picture, but keeps many kitchen-only tools out of the kid's toy box. The kid receives less JavaScript, and the grown-up can fetch data and build parts before the browser has to ask for them.
The important twist:
flowchart LR
Browser["Browser"] -->|first URL| Host["Host framework"]
Host --> RSC["RSC render"]
RSC --> Flight["Flight payload"]
Flight --> HTMLSSR["SSR HTML render"]
HTMLSSR --> Browser
Flight --> Browser
Browser -->|hydrate client components| ClientJS["Client JS chunks"]
subgraph Next["Next.js"]
NextRouter["App Router"] --> NextRSC["RSC route protocol"]
NextBundler["Turbopack or webpack"] --> NextManifests["Next manifests"]
end
subgraph RORP["React on Rails Pro"]
Rails["Rails view/helper"] --> NodeRenderer["Node renderer"]
Shakapacker["Shakapacker webpack/Rspack"] --> RORPManifests["RSC manifests"]
end
The same React primitives appear in both systems:
renderToPipeableStream or renderToReadableStream for HTML streaming.react-server-dom-* server APIs to produce the Flight stream.react-server-dom-* client APIs to turn the Flight stream back into React nodes.The architecture is different because Next.js makes Flight the router protocol. React on Rails Pro makes Flight a component payload protocol inside Rails-rendered pages.
| Term | Simple meaning | In practice |
|---|---|---|
| Server Component | Code that runs in the server/RSC environment | Can read server data directly and does not ship its implementation to the browser |
| Client Component | Code marked with 'use client' | May use state, effects, event handlers, browser APIs, and must ship browser JS |
| Flight payload | React's RSC wire format | Encodes rendered server output, client references, props, promises, errors, and stream chunks |
| Client reference | A placeholder for a Client Component in the RSC stream | The renderer needs a manifest to turn it into browser and SSR module IDs |
| SSR | HTML rendering | Takes React output and streams HTML so users and crawlers see content before hydration |
| Hydration | Browser attaches interactivity | React reuses existing HTML and activates Client Components |
| Manifest | Build-time lookup table | Connects source modules, chunk files, module IDs, CSS, and server action references |
| Turbopack transition | Turbopack graph context change | Used by Next to move from RSC graph to client, SSR, shared, or server utility graphs |
| Shakapacker | Rails bundling integration | Drives webpack or Rspack and emits Rails-friendly assets and manifests |
react-on-rails-rsc | RORP RSC adapter package | Isolates React RSC bundler/runtime APIs from the main Pro package |
React says Server Components render in an environment separate from the client app or SSR server. They can run at build time or for each request. React also explicitly warns framework authors that the underlying RSC bundler APIs in React 19 do not follow semver across minor versions, so framework/bundler integrations should pin carefully or track canary channels. See React Server Components.
Next's public docs describe the App Router flow this way:
Next's Turbopack docs say Turbopack is a Rust incremental bundler built into Next.js, now the default bundler, and that it uses a unified graph for multiple output environments, lazy bundling, and incremental computation. It also states that RSC is supported for the App Router and Turbopack ensures correct server/client bundling. See Next Turbopack.
Next's current Cache Components docs say caching can happen at the data or UI level, uncached dynamic data should stream behind Suspense, and the build can produce a static shell containing HTML plus serialized RSC payload for client navigation. See Next Caching.
Rspack's public model is different from Turbopack. Rspack is a high-performance Rust bundler with a webpack-compatible API and support for most webpack loaders and many plugin patterns. See Rspack and Rspack loader compatibility.
sequenceDiagram
participant B as Browser
participant N as Next server
participant RM as App page route module
participant AR as app-render.tsx
participant RSDW as React Server DOM
participant Fizz as React DOM Fizz
B->>N: GET /products/1
N->>N: match route and load app page module
N->>RM: render request
RM->>AR: renderToHTMLOrFlight
AR->>AR: parse headers, create work/request stores
AR->>RSDW: render RSC tree into Flight stream
AR->>Fizz: render HTML with AppRouter reading Flight stream
Fizz-->>B: stream HTML shell and progressive chunks
AR-->>B: inline Flight chunks as self.__next_f.push(...)
B->>B: decode Flight, create router state, hydrateRoot
The source-level request path found in the Next.js clone is:
BaseServer.handleRequestImpl
-> BaseServer.renderToResponseImpl
-> BaseServer.renderPageComponent
-> BaseServer.renderToResponseWithComponentsImpl
-> generated app-page handler
-> AppPageRouteModule.render
-> renderToHTMLOrFlight
Important files:
The same route module can answer two related request types:
text/x-component RSC payload only.Next's App Router headers are defined in app-router-headers.ts. Key headers:
rscnext-router-state-treenext-router-prefetchnext-hmr-refreshtext/x-component content typeThe first document request usually has no rsc: 1 header. Next renders both:
On later client navigations, the browser sends rsc: 1 plus the current router tree so the server can produce only the changed route segments.
Next's first request is not "SSR first, then RSC." It is closer to:
The key server functions live in app-render.tsx:
renderToHTMLOrFlightrenderToHTMLOrFlightImplgetRSCPayloadgenerateDynamicRSCPayloadgenerateDynamicFlightRenderResultrenderToStreamFor a normal dynamic HTML render:
flowchart TD
A["renderToHTMLOrFlight"] --> B["renderToHTMLOrFlightImpl"]
B --> C["renderToStream"]
C --> D["getRSCPayload"]
D --> E["createFlightRouterStateFromLoaderTree"]
D --> F["createComponentTree"]
D --> G["InitialRSCPayload"]
G --> H["renderToNodeFlightStream or renderToWebFlightStream"]
H --> I["ReactServerResult"]
I --> J["App reactServerStream=..."]
J --> K["getFlightStream on server side"]
K --> L["AppRouter SSR tree"]
L --> M["renderToNodeFizzStream or renderToWebFizzStream"]
M --> N["continueFizzStream"]
I --> O["createInlinedDataReadableStream"]
O --> N
N --> P["HTML + inline Flight scripts"]
The important detail is that the server-side <App> receives a stream of React Server output, calls getFlightStream, decodes it into React values, creates the initial router state, and renders <AppRouter> into HTML. This is in use-flight-response.tsx and app-render.tsx.
Next converts the RSC stream into script chunks that push into self.__next_f.
sequenceDiagram
participant RSC as Flight stream
participant Inline as createInlinedDataReadableStream
participant HTML as HTML stream
participant Browser as Browser
participant AppIndex as app-index.tsx
RSC->>Inline: binary or text Flight chunks
Inline->>HTML: script bootstrap for self.__next_f
Inline->>HTML: script pushes data chunks
HTML->>Browser: streamed document
Browser->>AppIndex: execute scripts as they arrive
AppIndex->>AppIndex: patch self.__next_f.push
AppIndex->>AppIndex: expose chunks as ReadableStream
AppIndex->>AppIndex: createFromReadableStream
AppIndex->>Browser: hydrateRoot
The inline protocol is implemented in use-flight-response.tsx. The browser consumer is app-index.tsx.
The browser boot sequence:
app-index.tsx creates a buffer for inline Flight chunks.self.__next_f entries.self.__next_f.push so later streamed script chunks feed the same stream.createFromReadableStream from react-server-dom-webpack/client.InitialRSCPayload.createInitialRouterState.ReactDOMClient.hydrateRoot.On navigation, the browser is not asking for new HTML. It asks for a new RSC payload that patches the App Router state.
sequenceDiagram
participant User
participant Router as App Router
participant Cache as Segment cache
participant Fetch as fetchServerResponse
participant Server as Next app-render
participant React as RSC client decoder
User->>Router: click Link or router.push
Router->>Cache: can this route be served from cache?
alt cache hit
Cache-->>Router: Flight data / segment data
else cache miss
Router->>Fetch: URL + current FlightRouterState
Fetch->>Server: GET with rsc:1 and next-router-state-tree
Server->>Server: walkTreeWithFlightRouterState
Server-->>Fetch: text/x-component Flight response
Fetch->>React: createFromFetch/createFromReadableStream
React-->>Router: decoded NavigationFlightResponse
end
Router->>Router: apply router tree patch
Router->>Router: render changed segments
Important client files:
The server-side diffing is in walk-tree-with-flight-router-state.tsx. It compares the requested loader tree with the client's current FlightRouterState, then decides whether to:
FlightDataPath[].This is a major difference from React on Rails Pro. Next uses RSC payloads to update route tree state. React on Rails Pro uses RSC payloads to render named registered server components, usually through RSCRoute.
Next's InitialRSCPayload includes more than rendered JSX:
The initial payload is created by getRSCPayload in app-render.tsx. The route navigation payload is created by generateDynamicRSCPayload in the same file.
That payload is a router payload, not merely a component payload.
Next's App Router treats files as Server Components unless they are marked with 'use client'.
flowchart TD
File["app/page.tsx or layout.tsx"] --> Directive{"Has use client?"}
Directive -->|no| ServerComp["Server Component module"]
Directive -->|yes| ClientRef["Client reference"]
ClientRef --> ClientBuild["Browser client chunk"]
ClientRef --> SSRBuild["SSR version for HTML render"]
ClientRef --> RSCProxy["RSC proxy module"]
ServerComp --> RSCGraph["RSC graph"]
RSCGraph --> Flight["Flight stream references ClientRef"]
Runtime tree construction happens in create-component-tree.tsx. It calls isClientReference and wraps client pages or layouts with ClientPageRoot or ClientSegmentRoot. Server pages receive server-side params and searchParams; client pages receive a client root wrapper and serializable server-provided params.
Validation and transformation are split by bundler mode:
Turbopack is not "webpack but faster" inside Next. It is the build graph engine Next uses to understand the whole app:
Rspack is closer to "webpack-compatible Rust bundler." Turbopack is closer to "Next-owned graph and endpoint compiler."
flowchart TD
CLI["next build"] --> Choice{"Bundler"}
Choice -->|Turbopack| TPBuild["turbopackBuild"]
Choice -->|webpack| WPBuild["webpackBuild"]
TPBuild --> Project["Native Rust Project through @next/swc"]
Project --> Entrypoints["writeAllEntrypointsToDisk"]
Entrypoints --> RawRoutes["Raw route endpoints"]
RawRoutes --> Handle["handle-entrypoints.ts"]
Handle --> ManifestLoader["TurbopackManifestLoader"]
ManifestLoader --> NextManifests["standard .next manifests"]
NextManifests --> Runtime["next start / adapters"]
Key files:
The TypeScript side calls native bindings and receives typed Project, Route, Endpoint, and WrittenEndpoint objects. The Rust side owns the graph, chunks, transitions, and endpoint generation.
Turbopack's Next integration maps an app page to two endpoint flavors:
flowchart TD
AppPage["AppEntrypoint::AppPage"] --> Html["Html endpoint"]
AppPage --> Rsc["Rsc endpoint"]
Html --> ClientAssets["client assets"]
Html --> SSRChunks["SSR chunks"]
Html --> FullManifests["full manifests"]
Rsc --> RSCPayload["RSC endpoint output"]
Rsc --> Minimal["minimal/no extra manifests"]
Important source:
When Turbopack sees a client reference from the RSC graph, it does not simply "exclude the file." It compiles related views of that module:
flowchart LR
RSCImport["RSC imports use client module"] --> Transition["NextEcmascriptClientReferenceTransition"]
Transition --> Browser["client_transition"]
Transition --> SSR["ssr_transition"]
Browser --> BrowserChunk["browser chunk"]
SSR --> SSRChunk["SSR chunk"]
BrowserChunk --> RefModule["EcmascriptClientReferenceModule"]
SSRChunk --> RefModule
RefModule --> Proxy["RSC proxy using react-server-dom-turbopack/server"]
Source:
This is a key contrast with RORP. React on Rails Pro's RSC bundle currently uses
react-on-rails-rsc/WebpackLoader to replace Client Components with references, while the
client/server manifests are produced by RSCWebpackPlugin for webpack or RSCRspackPlugin for
Rspack. Turbopack models the relationships as graph transitions.
flowchart TD
Dev["next dev"] --> Choose{"opts.turbo?"}
Choose -->|yes| HotTP["createHotReloaderTurbopack"]
Choose -->|no| HotWP["webpack/rspack hot reloader"]
HotTP --> Project["Turbopack Project in watch mode"]
Project --> Entries["entrypointsSubscribe"]
Entries --> Routes["current app/page endpoints"]
Project --> ClientHMR["hmrEvents Client"]
Project --> ServerHMR["hmrEvents Server"]
ClientHMR --> Browser["Fast Refresh browser runtime"]
ServerHMR --> Runtime["server chunk refresh"]
Important source:
Next dev mode can rebuild route endpoints and manifests on demand. Turbopack's lazy graph means the first hit to a route may trigger exactly the needed graph work; repeated edits reuse cached computations.
React on Rails Pro's runtime architecture has four boundaries:
There are three JS bundle roles:
sequenceDiagram
participant B as Browser
participant Rails as Rails route/controller/view
participant Helper as stream_react_component
participant Node as Node renderer
participant Server as server-bundle.js
participant RSC as rsc-bundle.js
B->>Rails: GET Rails URL
Rails->>Helper: stream_view_containing_react_components
Helper->>Node: POST render request to server bundle hash
Node->>Server: run SSR rendering request
Server->>Server: renderToPipeableStream begins
Server->>RSC: generateRSCPayload(componentName, props)
RSC-->>Server: Flight stream
Server->>Server: decode Flight into React nodes for SSR
Server->>Server: tee Flight for HTML injection
Server-->>Node: streamed HTML + RSC scripts
Node-->>Rails: streaming response chunks
Rails-->>B: HTML stream
B->>B: hydrate from REACT_ON_RAILS_RSC_PAYLOADS
Key files:
The generated render JS injects a global generateRSCPayload function when RSC support and streaming are enabled. In the server bundle, that function rewrites the current rendering request with a new component name and props, then calls runOnOtherBundle(rscBundleHash, newRenderingRequest). The RSC bundle detects it is the RSC bundle and uses serverRenderRSCReactComponent.
flowchart TD
RenderJS["server_rendering_js_code.rb"] --> Gen["generateRSCPayload global"]
Gen --> RSCParams["railsContext.serverSideRSCPayloadParameters"]
Gen --> Other["runOnOtherBundle(rscBundleHash, request)"]
Other --> RSCBundle["RSC bundle VM"]
RSCBundle --> ReactOnRailsRSC["serverRenderRSCReactComponent"]
ReactOnRailsRSC --> BuildServerRenderer["react-on-rails-rsc buildServerRenderer"]
BuildServerRenderer --> Flight["Flight stream"]
Important source:
packages/react-on-rails-pro-node-renderer/src/worker/vm.tsRSCRequestTracker is the request-scoped coordination point:
getRSCPayloadStream(componentName, props) calls global generateRSCPayload.PassThrough streams.injectRSCPayload.flowchart LR
RSCRoute["RSCRoute during SSR"] --> Get["railsContext.getRSCPayloadStream"]
Get --> Gen["generateRSCPayload"]
Gen --> Source["RSC bundle Flight source stream"]
Source --> Tee1["stream1 for SSR decode"]
Source --> Tee2["stream2 for HTML injection"]
Tee1 --> ClientRenderer["buildClientRenderer/createFromNodeStream"]
Tee2 --> Inject["injectRSCPayload"]
Inject --> Scripts["script pushes"]
Source:
RORP writes into self.REACT_ON_RAILS_RSC_PAYLOADS.
The cache key is based on:
domNodeIdThe HTML injection order is intentional:
That ensures a Client Component reading during hydration can find an array immediately, even if later Flight chunks are still streaming.
sequenceDiagram
participant HTML as React HTML stream
participant Tracker as RSCRequestTracker
participant Inject as injectRSCPayload
participant Browser as Browser
participant Client as getReactServerComponent.client
HTML->>Inject: first HTML chunk
Inject->>Tracker: subscribe to payload streams
Tracker-->>Inject: stream info for each RSC component
Inject-->>Browser: initialize payload array scripts
Inject-->>Browser: component HTML
Inject-->>Browser: payload push scripts
Browser->>Client: create stream from payload array
Client->>Client: createFromReadableStream
Source:
When the browser later needs a server component that was not embedded, getReactServerComponent.client.ts fetches:
/<rscPayloadGenerationUrlPath>/<componentName>?props=<json>
Rails routes that to the RSC payload controller/helper:
This is not a full router-tree navigation protocol. It is a named server-component payload endpoint. That design is a good fit for Rails pages where route ownership remains in Rails.
flowchart TD
Source["Application source"] --> Client["Client bundle"]
Source --> Server["Server bundle"]
Source --> RSC["RSC bundle"]
Client --> Browser["Browser hydration and client components"]
Server --> SSR["Node renderer SSR HTML"]
RSC --> Flight["Node renderer Flight payload"]
Client --> ClientManifest["react-client-manifest.json"]
Server --> ServerClientManifest["react-server-client-manifest.json"]
RSC --> RSCPayload["RSC payload generation"]
The RSC bundle is derived from the server bundle but:
rsc-bundle.jsreact-on-rails-rsc/WebpackLoader for the current RSC transform pathRSCRspackPlugin manifest path when Shakapacker runs under Rspackreact-server conditionreact-server entry files where neededreact-dom/serverSource:
RORP development splits the work into several processes:
flowchart TD
Procfile["Procfile.dev"] --> Rails["rails s"]
Procfile --> ClientDev["shakapacker-dev-server HMR=true"]
Procfile --> ServerWatch["SERVER_BUNDLE_ONLY bin/shakapacker --watch"]
Procfile --> RSCWatch["RSC_BUNDLE_ONLY bin/shakapacker --watch"]
Procfile --> Renderer["node-renderer"]
ClientDev --> BrowserHMR["browser HMR / refresh"]
ServerWatch --> ServerBundle["server-bundle.js"]
RSCWatch --> RSCBundle["rsc-bundle.js"]
ServerBundle --> Renderer
RSCBundle --> Renderer
Rails --> Renderer
Source: Procfile.dev
Practical meaning:
This is more explicit than Next dev. Next has one dev server coordinating route discovery, Turbopack graph updates, HMR events, manifests, route modules, and server runtime. RORP exposes the moving parts as separate Rails/Shakapacker/node processes, which is easier to integrate into Rails but has more process-level coordination.
flowchart TD
Precompile["assets precompile / shakapacker build"] --> Client["client assets"]
Precompile --> Server["server-bundle.js"]
Precompile --> RSC["rsc-bundle.js"]
Precompile --> ManifestA["react-client-manifest.json"]
Precompile --> ManifestB["react-server-client-manifest.json"]
Deploy["deploy"] --> Rails["Rails app"]
Deploy --> Renderer["Node renderer"]
Rails --> Upload["upload/check assets"]
Upload --> Renderer
Server --> Renderer
RSC --> Renderer
ManifestA --> Renderer
ManifestB --> Renderer
Production RORP has the same conceptual artifacts as development, but they are prebuilt, fingerprinted/hashed, uploaded or mounted for the renderer, and not watched. The Node rendering pool chooses the server bundle hash for SSR and the RSC bundle hash for RSC payload streaming.
Source:
| Dimension | Next.js App Router + Turbopack | React on Rails Pro RSC |
|---|---|---|
| Owner of routing | Next owns file-system routes and client router | Rails owns routes/controllers/views; React is mounted via helpers |
| RSC unit | Route segment tree | Registered component payload |
| Navigation protocol | Flight payload patches App Router state | RSCRoute fetches named component payloads |
| Initial document | HTML plus self.__next_f inline Flight stream | Rails HTML stream plus self.REACT_ON_RAILS_RSC_PAYLOADS payload arrays |
| Server process | Next server/route module runtime | Rails process plus Node renderer process |
| Build graph | Turbopack single unified graph for client/server/RSC endpoints | Shakapacker orchestrates webpack/Rspack configs for client/server/RSC bundles |
| Client boundary | 'use client' becomes client reference through Next transforms and graph | react-on-rails-rsc Webpack/Rspack loaders and plugins handle references/manifests |
| Manifests | Next client reference/server action/build/app path manifests | react-client-manifest.json, react-server-client-manifest.json, Rails/Shakapacker asset manifests |
| Caching | Deep route/segment/prefetch/cache component model | Rails caching, Pro streaming/cache helpers, node renderer bundle/cache behavior |
| Dev model | one Next dev server with Turbopack project and HMR streams | Rails + Shakapacker dev server + server bundle watcher + RSC watcher + node renderer |
| Production model | .next output with route modules, endpoints, manifests | Rails assets plus private server/RSC bundles and manifests for Node renderer |
| Best fit | Next-owned React app where App Router is the application shell | Rails-owned application incrementally adopting React/RSC |
Next owns all of these layers:
Because of that, Next can make the Flight payload include router-state patches, partial prerender data, static-stage lengths, stale-time hints, and runtime-prefetch streams.
React on Rails Pro deliberately does not own all those layers. Rails owns routes and request state. The Pro integration optimizes a different shape:
That means the RORP payload should probably stay component-centered unless a future Rails-side router abstraction wants to become Flight-aware.
flowchart TD
ServerWork["Server-only work"] --> LessJS["less browser JavaScript"]
DataDuringRender["data during render"] --> FewerWaterfalls["fewer client fetch waterfalls"]
Streaming["streaming + Suspense"] --> EarlierHTML["earlier visible HTML"]
ClientRefs["client references"] --> PreciseChunks["only interactive chunks hydrate"]
Both systems improve performance by:
Next can also optimize:
These are App Router capabilities, not generic React RSC capabilities.
RORP can optimize:
The initial RSC embedding path in RORP is already the right performance instinct: it avoids the common "SSR HTML plus immediate Flight refetch" penalty.
flowchart LR
Webpack["webpack"] --> API["webpack plugin/loader ecosystem"]
Rspack["Rspack"] --> API
Rspack --> RustFast["Rust implementation and SWC-friendly speed"]
Turbopack["Turbopack"] --> NextGraph["Next-native unified graph"]
NextGraph --> Endpoints["typed route endpoints"]
NextGraph --> Transitions["RSC/client/SSR transitions"]
Shakapacker is the Rails integration layer. It can drive webpack or Rspack and expose a Rails-friendly development/build story.
Using webpack:
react-on-rails-rsc internals are webpack-shapedUsing Rspack:
react-on-rails-rsc releases also ship native Rspack-facing exports:
react-on-rails-rsc/RspackPlugin, react-on-rails-rsc/RspackLoader, and
react-on-rails-rsc/RSCReferenceDiscoveryPluginRSCRspackPlugin for manifests under Rspack while
keeping react-on-rails-rsc/WebpackLoader for the RSC transformThat last point is important. Older RORP RSC explanations often say "Rspack just reuses the webpack
RSC plugin/loader/runtime." That was a useful shorthand for the webpack-compatible path, and it is
still directionally true for many webpack-shaped APIs. It is no longer precise enough. The package
now exposes explicit Rspack integration points while preserving the Webpack exports. Current dummy
config tests show the native Rspack manifest plugin selected under Rspack while the RSC transform
loader remains WebpackLoader-based because RspackLoader reports client modules to
RSCRspackPlugin and passes source through.
Local docs with practical details:
Next.js with webpack uses loaders/plugins to stitch App Router RSC behavior into webpack compilation. Next.js with its experimental Rspack path uses webpack-compatible APIs through next-rspack. Next.js with Turbopack computes the same conceptual outputs in the Rust graph.
The clean source-level contrast:
next-flight-loader, flight-client-entry-plugin, flight-manifest-pluginAppProject, TransitionOptions, NextEcmascriptClientReferenceTransition, EcmascriptClientReferenceModule, ClientReferencesGraphTurbopack is not the same kind of choice as Rspack in Shakapacker. Rspack preserves much of the webpack-shaped integration surface, and RORP's RSC package now also exposes native Rspack plugin and loader entry points. Turbopack moves the integration into Next-specific Rust code and typed route endpoints.
react-on-rails-rsc Is SeparateThe separate package is architecturally justified.
Reasons:
react-on-rails-pro package should not have to rev every time a React RSC bundler internal changes.react-on-rails-rsc, webpack/Rspack, and Pro.Current local version signals on origin/main at 17.0.0-rc.5:
~19.0.4 and react-on-rails-rsc
19.0.5.~19.0.4 and pins
[email protected].react-on-rails-rsc is ^19.0.5.package.json overrides scope the OSS dummy app to React 19.2.x and the Pro/RSC dummy app to
React / React DOM ^19.2.7 plus [email protected], so CI can soak-test the
React 19.2 path before it becomes the default.[email protected] peers to React / React DOM ^19.2.7.So the current policy is not "only React 19.0.x works" and not yet "19.2.x is the default generator target." It is a two-track policy:
~19.0.4 plus [email protected]19.2.7 plus [email protected]The product docs need one clear compatibility statement:
React on Rails Pro RSC supports:
React / React DOM default install: ~19.0.4
React / React DOM 19.2 soak path: 19.2.7 with react-on-rails-rsc 19.2.0-rc.1
react-on-rails-rsc default install: 19.0.5
webpack: supported through WebpackPlugin plus WebpackLoader
Rspack: supported through RSCRspackPlugin/RspackPlugin for manifests plus the WebpackLoader
RSC transform path in current generated configs
Given React's warning, the separate package is the compatibility adapter. In other words,
react-on-rails-rsc should be the package that tracks React RSC protocol/bundler internals, while
react-on-rails-pro and the generator decide which adapter version is the default install target.
Next turns RSC into a route protocol. React on Rails Pro turns RSC into a component protocol inside Rails.
Next.js route protocol:
flowchart LR
URL["URL"] --> RouteTree["App Router tree"]
RouteTree --> RSC1["RSC payload"]
RSC1 --> Patch["router state patch"]
Patch --> SegmentCache["segment cache"]
React on Rails Pro component protocol:
flowchart LR
RailsURL["Rails URL"] --> View["ERB/view/helper"]
View --> Component["registered component"]
Component --> RSC2["RSC payload"]
RSC2 --> ReactNode["React node under RSCRoute"]
This explains almost every downstream difference:
These are not "should implement immediately" items. They are design inspirations.
Next guards navigation with build IDs and deployment IDs. RORP already keys by component/props/dom id and bundle hash at higher levels, but a clear client-side protocol/version/build stamp could make stale embedded payloads easier to detect.
Next's App Router prefetches route RSC payloads. RORP could expose a small prefetch primitive for RSCRoute payloads where props are known before user interaction.
Next has many route/build manifests, but also a consistent place to inspect them. RORP users would benefit from doctor output that says: "client manifest has X client refs, server client manifest has Y refs, RSC bundle can resolve react-server condition."
Next makes RSC requests visible as text/x-component. RORP has /rsc_payload/:component_name; the docs could show exactly how to inspect it, decode chunks, and correlate component names to manifest entries.
General Shakapacker Rspack support is documented. RSC-specific docs should distinguish the
Webpack-shaped transform path (WebpackLoader) from the native Rspack manifest path
(RspackPlugin / RSCRspackPlugin) and the separate RspackLoader reporting export, then state
which React / react-on-rails-rsc pairs are tested.
The value of React on Rails Pro is preserving Rails ownership. RSC should enhance Rails pages, not require a Next-like app directory.
The explicit Rails, node renderer, server bundle, and RSC bundle boundaries are operationally useful in Rails deployments.
Next can do that because Next owns the whole stack. RORP users benefit from Shakapacker's webpack/Rspack bridge.
React's own docs warn framework implementers about this. The separate react-on-rails-rsc package exists partly to absorb this volatility.
generateRSCPayload.If this analysis becomes published docs, I would split it:
docs/pro/react-server-components/mental-model.md
docs/pro/react-server-components/architecture.md
RSCRequestTracker/rsc_payload endpointdocs/pro/react-server-components/nextjs-comparison.md
docs/pro/react-server-components/version-compatibility.md
react-on-rails-rsc rangeNext.js RSC is a full application routing architecture. Turbopack is deeply integrated into that architecture, so it can compute route endpoints, client references, SSR chunks, RSC payload endpoints, and manifests as one unified graph.
React on Rails Pro RSC is a Rails integration architecture. It adds RSC to Rails pages without taking routing away from Rails. Its central trick is the server bundle to RSC bundle handoff plus request-scoped Flight stream teeing, so SSR and hydration share the same RSC payload without an initial refetch.
That makes the two systems spiritually similar at the React protocol layer, but strategically different at the framework layer.