Commit 41b95ac
authored
Use RSC payload to render server components on server (#515)
# Using RSC Payload to Render Server Components on Server
## Overview
This PR implements a significant architectural improvement to the React
Server Components (RSC) rendering flow in both `react_on_rails` and
`react_on_rails_pro`. Previously, server components were rendered twice
- once for HTML generation and again for RSC payload. This PR unifies
the process by generating the RSC payload first and using it for
server-side rendering, eliminating double rendering and reducing HTTP
requests.
## Previous Rendering Flow
Before this improvement, the React Server Components rendering process
worked as follows:
1. Initial HTML Generation:
- The `stream_react_component` helper is called in the view
- Makes a request to the node renderer
- Renderer uses the **Server Bundle** to generate HTML for all
components
- HTML is streamed to the client
2. RSC Payload Generation:
- Browser shows the initial html
- Browser makes a separate fetch request to the RSC payload URL
- Calls `rsc_payload_react_component` on the server
- Node renderer uses the **RSC Bundle** to generate the RSC payload
- Server components are rendered and serialized
- Client components are included as references
3. Client Hydration:
- RSC payload is processed by React runtime
- Client component chunks are fetched based on references
- Components are hydrated progressively
This approach had two main inefficiencies:
1. **Double Rendering**: Server components are rendered twice:
- Once for HTML generation using the server bundle
- Again for RSC payload generation using the RSC bundle
2. **Multiple Requests**: Requires two separate HTTP requests:
- Initial request for HTML
- Secondary request for RSC payload
```mermaid
sequenceDiagram
participant Browser
participant View
participant NodeRenderer
participant ServerBundle
participant RSCBundle
Note over Browser,RSCBundle: 1. Initial HTML Generation
Browser->>View: Request page
View->>NodeRenderer: stream_react_component
NodeRenderer->>ServerBundle: Generate HTML
ServerBundle-->>NodeRenderer: HTML for all components
NodeRenderer-->>Browser: Stream HTML
Note over Browser,RSCBundle: 2. RSC Payload Generation
Browser->>NodeRenderer: Fetch RSC payload
NodeRenderer->>RSCBundle: rsc_payload_react_component
RSCBundle-->>NodeRenderer: RSC payload with:<br/>- Server components<br/>- Client component refs
NodeRenderer-->>Browser: Stream RSC payload
Note over Browser: 3. Client Hydration
Browser->>Browser: Process RSC payload
loop For each client component
Browser->>Browser: Fetch component chunk
Browser->>Browser: Hydrate component
end
```
> [!NOTE]
> For simplicity, this diagram shows the RSC payload being fetched after
the HTML is fully streamed to the client. In reality, the browser begins
fetching the RSC payload and starts hydration immediately as soon as it
receives the necessary HTML, without waiting for the complete page to be
streamed. This parallel processing enables faster page interactivity and
better performance.
## Key Improvements
1. **Elimination of Double Rendering**: Server components are now
rendered only once - in the RSC bundle - and the resulting payload is
used by both server and client.
2. **Single HTTP Request**: The client no longer needs to make a
separate request for the RSC payload, as it's embedded in the initial
HTML response.
3. **Improved Performance**: Reduced server load and faster page
interactivity by eliminating redundant rendering and requests.
## Technical Implementation
### Inter-Bundle Communication
The core of this implementation is a mechanism for communication between
bundles in the node renderer:
- Introduced `runOnOtherBundle` function in the VM context that allows
executing code in a different bundle
- Added `generateRSCPayload` function that uses `runOnOtherBundle` to
run rendering requests in the RSC bundle
```javascript
// Simplified representation of the flow
globalThis.generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) {
const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters;
const propsString = JSON.stringify(props);
const newRenderingRequest = renderingRequest.replace(/\(\s*\)\s*$/, `('${componentName}', ${propsString})`);
return runOnOtherBundle(rscBundleHash, newRenderingRequest);
}
```
### Key Components for RSC Rendering
Two new components are introduced in React on Rails to facilitate
server-side RSC rendering:
#### RSCServerRoot
`RSCServerRoot` is the core component responsible for:
- Requesting the RSC payload from the RSC bundle via the
`generateRSCPayload` function
- Creating server component elements using the RSC payload
- Rendering both the server component HTML and embedding the RSC payload
for client hydration
This component operates as follows:
1. Receives the component name and props to render
2. Creates an SSR manifest by merging client and server manifests
3. Fetches the RSC payload stream for the component
4. Splits the stream to use for both rendering and embedding
5. Renders the server component on the server using the RSC payload
6. Returns a React fragment containing both the rendered component and
the RSCPayloadContainer
#### RSCPayloadContainer
`RSCPayloadContainer` handles embedding the RSC payload within the HTML
response by:
- Accepting an RSC payload stream as input
- Processing the stream chunks progressively
- Creating script elements that inject the payload into
`self.REACT_ON_RAILS_RSC_PAYLOAD`
- Suspending rendering until new chunks arrive, enabling streaming
This creates a seamless experience where:
- Server components render immediately on the server
- RSC payload is embedded directly in the HTML response
- Client can hydrate components progressively without a separate request
### New Rendering Flow
1. When `stream_react_component` is called:
- The server bundle rendering function calls `generateRSCPayload` with
the component name and props
- This executes the component rendering in the RSC bundle
- RSC bundle generates the payload containing server component data and
client component references
- The payload is returned to the server bundle
2. The server bundle then:
- Uses the RSC payload to generate HTML for server components
- Renders client components normally
- Embeds the RSC payload within the HTML response
- Streams the result to the client
3. On the client:
- HTML is displayed immediately
- React runtime uses the embedded RSC payload for hydration
- Client components are hydrated progressively
### Configuration and Detection
- Added conditional rendering function selection based on bundle type:
```javascript
ReactOnRails[ReactOnRails.isRSCBundle ? 'serverRenderRSCReactComponent' : 'streamServerRenderedReactComponent']
```
- RSC bundles now expose `ReactOnRails.isRSCBundle` flag to identify
their special purpose
- Server bundles automatically detect and use RSC payload when available
## Visual Representation
```mermaid
sequenceDiagram
participant Browser
participant View
participant NodeRenderer
participant RSCBundle
participant ServerBundle
Note over Browser,ServerBundle: 1. Initial Request
Browser->>View: Request page
View->>NodeRenderer: stream_react_component
NodeRenderer->>ServerBundle: Execute rendering request
ServerBundle->>RSCBundle: generateRSCPayload(component, props)
RSCBundle-->>ServerBundle: RSC payload with:<br/>- Server components<br/>- Client component refs
ServerBundle-->>NodeRenderer: Generate HTML using RSC payload
Note over Browser,ServerBundle: 2. Single Response
NodeRenderer-->>Browser: Stream HTML with embedded RSC payload
Note over Browser: 3. Client Hydration
Browser->>Browser: Process embedded RSC payload
loop For each client component
Browser->>Browser: Fetch component chunk
Browser->>Browser: Hydrate component
end
```
## Testing
- Added tests for the `serverRenderRSCReactComponent` function
- Verified correct payload generation and embedding
- Ensured proper hydration of client components using the embedded
payload
- Confirmed reduced network requests and rendering time
## Performance Improvements
- **Server Load**: Reduced overhead of rendering server components twice
- **Network Traffic**: Reduced by eliminating separate RSC payload
requests.
- **Time to Interactive**: Improved by streamlining the rendering and
hydration process
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Enhanced React Server Components (RSC) rendering flow to eliminate
double rendering and reduce HTTP requests, resulting in faster
interactivity.
- Added support for simultaneous upload and management of multiple
bundles and assets.
- Introduced a global function for inter-bundle communication during
rendering requests.
- Added Redis-based streaming support for server-rendered React
components, enabling advanced data-fetching patterns.
- Provided new guides and documentation for rendering RSC inside client
components and optimizing hydration flows.
- Added new sample pages, components, and routes demonstrating advanced
RSC, streaming, and router integration patterns.
- **Improvements**
- Upgraded the protocol version between Node Renderer and Rails to
2.0.0.
- Improved error handling and validation for asset and bundle
operations.
- Optimized bundle and asset management for better concurrency and
resource usage.
- Enhanced test coverage for multi-bundle and streaming scenarios.
- Updated dependencies for improved compatibility and performance.
- **Bug Fixes**
- Fixed edge cases in bundle upload, asset existence checks, and error
propagation during streaming.
- **Documentation**
- Updated and expanded documentation to reflect the new RSC rendering
flow, protocol changes, and advanced usage patterns.
- **Chores**
- Updated development dependencies and configuration for improved
tooling and testing support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->1 parent 60f7c4b commit 41b95ac
File tree
104 files changed
+3731
-714
lines changed- docs
- node-renderer
- lib/react_on_rails_pro
- concerns
- server_rendering_pool
- packages/node-renderer
- src
- worker
- tests
- fixtures
- projects/spec-dummy
- 220f7a3
- spec
- dummy
- app
- controllers
- concerns
- models
- views
- layouts
- pages
- client/app
- components
- RSCPostsPage
- packs
- ror-auto-load-components
- utils
- config
- webpack
- spec
- helpers
- support
- system
- react_on_rails_pro
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
104 files changed
+3731
-714
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
19 | 31 | | |
20 | 32 | | |
21 | 33 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
287 | | - | |
| 287 | + | |
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
| |||
462 | 462 | | |
463 | 463 | | |
464 | 464 | | |
465 | | - | |
| 465 | + | |
466 | 466 | | |
467 | 467 | | |
468 | 468 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
66 | 70 | | |
67 | 71 | | |
68 | 72 | | |
69 | | - | |
| 73 | + | |
70 | 74 | | |
71 | 75 | | |
72 | 76 | | |
| |||
0 commit comments