Skip to content

Commit b6cf638

Browse files
use RSC payload to SSR server components
1 parent 3385d6e commit b6cf638

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1374
-434
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ You can find the **package** version numbers from this repo's tags and below in
1616
## [Unreleased]
1717
*Add changes in master not yet tagged.*
1818

19+
### Improved
20+
- Improved RSC rendering flow by eliminating double rendering of server components and reducing the number of HTTP requests.
21+
- Updated communication protocol between Node Renderer and Rails to version 2.0.0 which supports the ability to upload multiple bundles at once.
22+
- Added the ability to communicate between different bundles on the renderer by using the `runOnOtherBundle` function which is globally available for the rendering request.
23+
24+
[PR 515](https://github.com/shakacode/react_on_rails_pro/pull/515) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
25+
26+
27+
## [4.0.0-rc.13] - 2025-03-07
28+
1929
### Added
2030
- 🚀 **Introducing React Server Components Support!** 🎉
2131
- Experience the future of React with full RSC integration

Gemfile.ci

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gem "react_on_rails", github: "shakacode/react_on_rails", branch: "abanoubghadban/pro465/use-rsc-payload-to-render-server-components-on-server"

Gemfile.lock

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ GIT
66
byebug (~> 11.0)
77
pry (>= 0.13, < 0.15)
88

9+
GIT
10+
remote: https://github.com/shakacode/react_on_rails.git
11+
revision: c7d4d42682f4620a8ff02cf519e0a61a96534441
12+
branch: abanoubghadban/pro465/use-rsc-payload-to-render-server-components-on-server
13+
specs:
14+
react_on_rails (15.0.0.alpha.2)
15+
addressable
16+
connection_pool
17+
execjs (~> 2.5)
18+
rails (>= 5.2)
19+
rainbow (~> 3.0)
20+
921
PATH
1022
remote: .
1123
specs:
@@ -284,12 +296,6 @@ GEM
284296
ffi (~> 1.0)
285297
rdoc (6.12.0)
286298
psych (>= 4.0.0)
287-
react_on_rails (15.0.0.alpha.2)
288-
addressable
289-
connection_pool
290-
execjs (~> 2.5)
291-
rails (>= 5.2)
292-
rainbow (~> 3.0)
293299
regexp_parser (2.9.2)
294300
reline (0.6.0)
295301
io-console (~> 0.5)
@@ -462,7 +468,7 @@ DEPENDENCIES
462468
pry-theme
463469
puma (~> 6)
464470
rails (~> 7.1)
465-
react_on_rails (= 15.0.0.alpha.2)
471+
react_on_rails!
466472
react_on_rails_pro!
467473
rspec-rails
468474
rspec-retry

docs/node-renderer/js-configuration.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Here are the options available for the JavaScript renderer configuration object,
1717
1. **logHttpLevel** (default: `process.env.RENDERER_LOG_HTTP_LEVEL || 'error'`) - The HTTP server log level (same allowed values as `logLevel`).
1818
1. **fastifyServerOptions** (default: `{}`) - Additional options to pass to the Fastify server factory. See [Fastify documentation](https://fastify.dev/docs/latest/Reference/Server/#factory).
1919
1. **bundlePath** (default: `process.env.RENDERER_BUNDLE_PATH || '/tmp/react-on-rails-pro-node-renderer-bundles'` ) - path to a temp directory where uploaded bundle files will be stored. For example you can set it to `path.resolve(__dirname, './.node-renderer-bundles')` if you configured renderer from the `/` directory of your app.
20-
1. **workersCount** (default: `env.RENDERER_WORKERS_COUNT || defaultWorkersCount()` where default is your CPUs count - 1) - Number of workers that will be forked to serve rendering requests. If you set this manually make sure that value is a **Number** and is `>= 1`.
20+
1. **workersCount** (default: `env.RENDERER_WORKERS_COUNT || defaultWorkersCount()` where default is your CPUs count - 1) - Number of workers that will be forked to serve rendering requests. If you set this manually make sure that value is a **Number** and is `>= 0`. Setting this to `0` will run the renderer in a single process mode without forking any workers, which is useful for debugging purposes. For production use, the value should be `>= 1`.
2121
1. **password** (default: `env.RENDERER_PASSWORD`) - The password expected to receive from the **Rails client** to authenticate rendering requests.
2222
If no password is set, no authentication will be required.
2323
1. **allWorkersRestartInterval** (default: `env.RENDERER_ALL_WORKERS_RESTART_INTERVAL`) - Interval in minutes between scheduled restarts of all workers. By default restarts are not enabled. If restarts are enabled, `delayBetweenIndividualWorkerRestarts` should also be set.
@@ -63,10 +63,14 @@ const config = {
6363
// All other values are the defaults, as described above
6464
};
6565

66+
// For debugging, run in single process mode
67+
if (process.env.NODE_ENV === 'development') {
68+
config.workersCount = 0;
69+
}
6670
// Renderer detects a total number of CPUs on virtual hostings like Heroku or CircleCI instead
6771
// of CPUs number allocated for current container. This results in spawning many workers while
6872
// only 1-2 of them really needed.
69-
if (process.env.CI) {
73+
else if (process.env.CI) {
7074
config.workersCount = 2;
7175
}
7276

docs/react-server-components-rendering-flow.md

Lines changed: 24 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -24,91 +24,35 @@ In a React Server Components project, there are three distinct types of bundles:
2424
- Code splitting occurs automatically for client components
2525
- Chunks are loaded on-demand during client component hydration
2626

27-
## Current Rendering Flow
27+
## Rendering Flow
2828

29-
When a request is made to a page using React Server Components, the following sequence occurs:
29+
When a request is made to a page using React Server Components, the following optimized sequence occurs:
3030

31-
1. Initial HTML Generation:
31+
1. Initial Request Processing:
3232
- The `stream_react_component` helper is called in the view
3333
- Makes a request to the node renderer
34-
- Renderer uses the **Server Bundle** to generate HTML for all components
35-
- HTML is streamed to the client
36-
37-
2. RSC Payload Generation:
38-
- Browser shows the initial html
39-
- Browser makes a separate fetch request to the RSC payload URL
40-
- Calls `rsc_payload_react_component` on the server
41-
- Node renderer uses the **RSC Bundle** to generate the RSC payload
42-
- Server components are rendered and serialized
43-
- Client components are included as references
34+
- Server bundle's rendering function calls `generateRSCPayload` with the component name and props
35+
- This executes the component rendering in the RSC bundle
36+
- RSC bundle generates the payload containing server component data and client component references
37+
- The payload is returned to the server bundle
38+
39+
2. Server-Side Rendering with RSC Payload:
40+
- The server bundle uses the RSC payload to generate HTML for server components using `RSCServerRoot`
41+
- `RSCServerRoot` splits the RSC payload stream into two parts:
42+
- One stream for rendering server components as HTML
43+
- Another stream for embedding the RSC payload in the response
44+
- `RSCPayloadContainer` component embeds the RSC payload within the HTML response
45+
- HTML and embedded RSC payload are streamed together to the client
4446

4547
3. Client Hydration:
46-
- RSC payload is processed by React runtime
47-
- Client component chunks are fetched based on references
48-
- Components are hydrated progressively
49-
50-
## Current Limitations
51-
52-
This approach has two main inefficiencies:
53-
54-
1. **Double Rendering**: Server components are rendered twice:
55-
- Once for HTML generation using the server bundle
56-
- Again for RSC payload generation using the RSC bundle
57-
58-
2. **Multiple Requests**: Requires two separate HTTP requests:
59-
- Initial request for HTML
60-
- Secondary request for RSC payload
61-
62-
```mermaid
63-
sequenceDiagram
64-
participant Browser
65-
participant View
66-
participant NodeRenderer
67-
participant ServerBundle
68-
participant RSCBundle
69-
70-
Note over Browser,RSCBundle: 1. Initial HTML Generation
71-
Browser->>View: Request page
72-
View->>NodeRenderer: stream_react_component
73-
NodeRenderer->>ServerBundle: Generate HTML
74-
ServerBundle-->>NodeRenderer: HTML for all components
75-
NodeRenderer-->>Browser: Stream HTML
76-
77-
Note over Browser,RSCBundle: 2. RSC Payload Generation
78-
Browser->>NodeRenderer: Fetch RSC payload
79-
NodeRenderer->>RSCBundle: rsc_payload_react_component
80-
RSCBundle-->>NodeRenderer: RSC payload with:<br/>- Server components<br/>- Client component refs
81-
NodeRenderer-->>Browser: Stream RSC payload
82-
83-
Note over Browser: 3. Client Hydration
84-
Browser->>Browser: Process RSC payload
85-
loop For each client component
86-
Browser->>Browser: Fetch component chunk
87-
Browser->>Browser: Hydrate component
88-
end
89-
```
90-
91-
> [!NOTE]
92-
> 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.
93-
94-
## Future Improvements
95-
96-
These inefficiencies will be addressed in the upcoming ["Use RSC payload to render server components on server"](https://github.com/shakacode/react_on_rails_pro/pull/515) PR. The new flow will be:
97-
98-
1. Initial Request:
99-
- `stream_react_component` triggers node renderer
100-
- Renderer uses **RSC Bundle** to generate RSC payload
101-
- Payload is passed to rendering function in **Server Bundle**
102-
- HTML of server components is generated using RSC payload
103-
- Client component references are filled with HTML of the client components
104-
105-
2. Single Response:
106-
- HTML and RSC payload are streamed together, with the RSC payload embedded inside the HTML page
10748
- Browser displays HTML immediately
108-
- React runtime uses embedded RSC payload for hydration
109-
- Client components are hydrated progressively
49+
- React runtime uses the embedded RSC payload for hydration
50+
- Client components are hydrated progressively without requiring a separate HTTP request
11051

111-
This improved approach eliminates double rendering and reduces HTTP requests, resulting in better performance and resource utilization.
52+
This approach offers significant advantages:
53+
- Eliminates double rendering of server components
54+
- Reduces HTTP requests by embedding the RSC payload within the initial HTML response
55+
- Provides faster interactivity through streamlined rendering and hydration
11256

11357
```mermaid
11458
sequenceDiagram
@@ -121,9 +65,9 @@ sequenceDiagram
12165
Note over Browser,ServerBundle: 1. Initial Request
12266
Browser->>View: Request page
12367
View->>NodeRenderer: stream_react_component
124-
NodeRenderer->>RSCBundle: Generate RSC payload
125-
RSCBundle-->>NodeRenderer: RSC payload
126-
NodeRenderer->>ServerBundle: Pass RSC payload
68+
NodeRenderer->>ServerBundle: Execute rendering request
69+
ServerBundle->>RSCBundle: generateRSCPayload(component, props)
70+
RSCBundle-->>ServerBundle: RSC payload with:<br/>- Server components<br/>- Client component refs
12771
ServerBundle-->>NodeRenderer: Generate HTML using RSC payload
12872
12973
Note over Browser,ServerBundle: 2. Single Response

lib/react_on_rails_pro/request.rb

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def reset_connection
1414

1515
def render_code(path, js_code, send_bundle)
1616
Rails.logger.info { "[ReactOnRailsPro] Perform rendering request #{path}" }
17-
form = form_with_code(js_code, send_bundle, is_rsc_payload: false)
17+
form = form_with_code(js_code, send_bundle)
1818
perform_request(path, form: form)
1919
end
2020

@@ -28,27 +28,55 @@ def render_code_as_stream(path, js_code, is_rsc_payload:)
2828
end
2929

3030
ReactOnRailsPro::StreamRequest.create do |send_bundle|
31-
form = form_with_code(js_code, send_bundle, is_rsc_payload: is_rsc_payload)
31+
form = form_with_code(js_code, send_bundle)
3232
perform_request(path, form: form, stream: true)
3333
end
3434
end
3535

3636
def upload_assets
3737
Rails.logger.info { "[ReactOnRailsPro] Uploading assets" }
38-
perform_request("/upload-assets", form: form_with_assets_and_bundle)
3938

40-
return unless ReactOnRailsPro.configuration.enable_rsc_support
39+
# Check if server bundle exists before trying to upload assets
40+
server_bundle_path = ReactOnRails::Utils.server_bundle_js_file_path
41+
unless File.exist?(server_bundle_path)
42+
raise ReactOnRailsPro::Error, "Server bundle not found at #{server_bundle_path}. " \
43+
"Please build your bundles before uploading assets."
44+
end
45+
46+
# Create a list of bundle timestamps to send to the node renderer
47+
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
48+
target_bundles = [pool.server_bundle_hash]
49+
50+
# Add RSC bundle if enabled
51+
if ReactOnRailsPro.configuration.enable_rsc_support
52+
rsc_bundle_path = ReactOnRails::Utils.rsc_bundle_js_file_path
53+
unless File.exist?(rsc_bundle_path)
54+
raise ReactOnRailsPro::Error, "RSC bundle not found at #{rsc_bundle_path}. " \
55+
"Please build your bundles before uploading assets."
56+
end
57+
target_bundles << pool.rsc_bundle_hash
58+
end
59+
60+
form = form_with_assets_and_bundle
61+
form["targetBundles"] = target_bundles
4162

42-
perform_request("/upload-assets", form: form_with_assets_and_bundle(is_rsc_payload: true))
43-
# Explicitly return nil to ensure consistent return value regardless of whether
44-
# enable_rsc_support is true or false. Without this, the method would return nil
45-
# when RSC is disabled but return the response object when RSC is enabled.
46-
nil
63+
perform_request("/upload-assets", form: form)
4764
end
4865

4966
def asset_exists_on_vm_renderer?(filename)
5067
Rails.logger.info { "[ReactOnRailsPro] Sending request to check if file exist on node-renderer: #{filename}" }
51-
response = perform_request("/asset-exists?filename=#{filename}", json: common_form_data)
68+
69+
form_data = common_form_data
70+
71+
# Add targetBundles from the current bundle hash and RSC bundle hash
72+
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
73+
target_bundles = [pool.server_bundle_hash]
74+
75+
target_bundles << pool.rsc_bundle_hash if ReactOnRailsPro.configuration.enable_rsc_support
76+
77+
form_data["targetBundles"] = target_bundles
78+
79+
response = perform_request("/asset-exists?filename=#{filename}", json: form_data)
5280
JSON.parse(response.body)["exists"] == true
5381
end
5482

@@ -118,42 +146,54 @@ def perform_request(path, **post_options) # rubocop:disable Metrics/AbcSize,Metr
118146
response
119147
end
120148

121-
def form_with_code(js_code, send_bundle, is_rsc_payload:)
149+
def form_with_code(js_code, send_bundle)
122150
form = common_form_data
123151
form["renderingRequest"] = js_code
124-
populate_form_with_bundle_and_assets(form, is_rsc_payload: is_rsc_payload, check_bundle: false) if send_bundle
152+
populate_form_with_bundle_and_assets(form, check_bundle: false) if send_bundle
125153
form
126154
end
127155

128-
def populate_form_with_bundle_and_assets(form, is_rsc_payload:, check_bundle:)
129-
server_bundle_path = if is_rsc_payload
130-
ReactOnRails::Utils.rsc_bundle_js_file_path
131-
else
132-
ReactOnRails::Utils.server_bundle_js_file_path
133-
end
134-
if check_bundle && !File.exist?(server_bundle_path)
135-
raise ReactOnRailsPro::Error, "Bundle not found #{server_bundle_path}"
156+
def populate_form_with_bundle_and_assets(form, check_bundle:)
157+
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
158+
159+
add_bundle_to_form(
160+
form,
161+
bundle_path: ReactOnRails::Utils.server_bundle_js_file_path,
162+
bundle_file_name: pool.renderer_bundle_file_name,
163+
bundle_hash: pool.server_bundle_hash,
164+
check_bundle: check_bundle
165+
)
166+
167+
if ReactOnRailsPro.configuration.enable_rsc_support
168+
add_bundle_to_form(
169+
form,
170+
bundle_path: ReactOnRails::Utils.rsc_bundle_js_file_path,
171+
bundle_file_name: pool.rsc_renderer_bundle_file_name,
172+
bundle_hash: pool.rsc_bundle_hash,
173+
check_bundle: check_bundle
174+
)
136175
end
137176

138-
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
139-
renderer_bundle_file_name = if is_rsc_payload
140-
pool.rsc_renderer_bundle_file_name
141-
else
142-
pool.renderer_bundle_file_name
143-
end
144-
form["bundle"] = {
145-
body: get_form_body_for_file(server_bundle_path),
177+
add_assets_to_form(form)
178+
end
179+
180+
def add_bundle_to_form(form, bundle_path:, bundle_file_name:, bundle_hash:, check_bundle:)
181+
raise ReactOnRailsPro::Error, "Bundle not found #{bundle_path}" if check_bundle && !File.exist?(bundle_path)
182+
183+
form["bundle_#{bundle_hash}"] = {
184+
body: get_form_body_for_file(bundle_path),
146185
content_type: "text/javascript",
147-
filename: renderer_bundle_file_name
186+
filename: bundle_file_name
148187
}
149-
150-
add_assets_to_form(form, is_rsc_payload: is_rsc_payload)
151188
end
152189

153-
def add_assets_to_form(form, is_rsc_payload:)
190+
def add_assets_to_form(form)
154191
assets_to_copy = ReactOnRailsPro.configuration.assets_to_copy || []
155-
# react_client_manifest file is needed to generate react server components payload
156-
assets_to_copy << ReactOnRails::Utils.react_client_manifest_file_path if is_rsc_payload
192+
# react_client_manifest and react_server_manifest files are needed to generate react server components payload
193+
if ReactOnRailsPro.configuration.enable_rsc_support
194+
assets_to_copy << ReactOnRails::Utils.react_client_manifest_file_path
195+
assets_to_copy << ReactOnRails::Utils.react_server_client_manifest_file_path
196+
end
157197

158198
return form unless assets_to_copy.present?
159199

@@ -180,18 +220,14 @@ def add_assets_to_form(form, is_rsc_payload:)
180220
form
181221
end
182222

183-
def form_with_assets_and_bundle(is_rsc_payload: false)
223+
def form_with_assets_and_bundle
184224
form = common_form_data
185-
populate_form_with_bundle_and_assets(form, is_rsc_payload: is_rsc_payload, check_bundle: true)
225+
populate_form_with_bundle_and_assets(form, check_bundle: true)
186226
form
187227
end
188228

189229
def common_form_data
190-
{
191-
"gemVersion" => ReactOnRailsPro::VERSION,
192-
"protocolVersion" => "1.0.0",
193-
"password" => ReactOnRailsPro.configuration.renderer_password
194-
}
230+
ReactOnRailsPro::Utils.common_form_data
195231
end
196232

197233
def create_connection

0 commit comments

Comments
 (0)