react_on_rails_pro 16.2.0.beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.controlplane/Dockerfile +49 -0
- data/.controlplane/controlplane.yml +22 -0
- data/.controlplane/gvc.yml +25 -0
- data/.controlplane/postgres.yml +33 -0
- data/.controlplane/rails.yml +49 -0
- data/.controlplane/redis.yml +18 -0
- data/.gitignore +77 -0
- data/.prettierignore +12 -0
- data/.prettierrc +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +120 -0
- data/.scss-lint.yml +205 -0
- data/CHANGELOG.md +570 -0
- data/CI_SETUP.md +502 -0
- data/CONTRIBUTING.md +376 -0
- data/Dockerfile +63 -0
- data/Gemfile +8 -0
- data/Gemfile.development_dependencies +74 -0
- data/Gemfile.loader +32 -0
- data/Gemfile.lock +527 -0
- data/LICENSE +98 -0
- data/LICENSE_SETUP.md +272 -0
- data/README.md +577 -0
- data/Rakefile +13 -0
- data/app/controllers/react_on_rails_pro/rsc_payload_controller.rb +7 -0
- data/app/helpers/react_on_rails_pro_helper.rb +360 -0
- data/app/views/react_on_rails_pro/rsc_payload.html.erb +1 -0
- data/babel.config.js +4 -0
- data/docs/bundle-caching.md +205 -0
- data/docs/caching.md +234 -0
- data/docs/code-splitting-loadable-components.md +313 -0
- data/docs/code-splitting.md +349 -0
- data/docs/configuration.md +165 -0
- data/docs/contributors-info/onboarding-customers.md +6 -0
- data/docs/contributors-info/releasing.md +40 -0
- data/docs/contributors-info/style.md +33 -0
- data/docs/home-pro.md +146 -0
- data/docs/installation.md +203 -0
- data/docs/js-memory-leaks.md +22 -0
- data/docs/node-renderer/basics.md +92 -0
- data/docs/node-renderer/debugging.md +38 -0
- data/docs/node-renderer/error-reporting-and-tracing.md +160 -0
- data/docs/node-renderer/heroku.md +102 -0
- data/docs/node-renderer/js-configuration.md +91 -0
- data/docs/node-renderer/troubleshooting.md +5 -0
- data/docs/profiling-server-side-rendering-code.md +179 -0
- data/docs/react-server-components/add-streaming-and-interactivity.md +190 -0
- data/docs/react-server-components/create-without-ssr.md +448 -0
- data/docs/react-server-components/glossary.md +102 -0
- data/docs/react-server-components/how-react-server-components-work.md +243 -0
- data/docs/react-server-components/inside-client-components.md +332 -0
- data/docs/react-server-components/purpose-and-benefits.md +243 -0
- data/docs/react-server-components/rendering-flow.md +86 -0
- data/docs/react-server-components/selective-hydration-in-streamed-components.md +75 -0
- data/docs/react-server-components/server-side-rendering.md +72 -0
- data/docs/react-server-components/tutorial.md +19 -0
- data/docs/release-notes/4.0.md +94 -0
- data/docs/release-notes/v4-react-server-components.md +66 -0
- data/docs/ruby-api.md +11 -0
- data/docs/streaming-server-rendering.md +210 -0
- data/docs/troubleshooting.md +24 -0
- data/docs/updating.md +219 -0
- data/eslint.config.mjs +220 -0
- data/lib/react_on_rails_pro/assets_precompile.rb +230 -0
- data/lib/react_on_rails_pro/cache.rb +88 -0
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +38 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +103 -0
- data/lib/react_on_rails_pro/configuration.rb +228 -0
- data/lib/react_on_rails_pro/constants.rb +8 -0
- data/lib/react_on_rails_pro/engine.rb +24 -0
- data/lib/react_on_rails_pro/error.rb +14 -0
- data/lib/react_on_rails_pro/license_public_key.rb +30 -0
- data/lib/react_on_rails_pro/license_validator.rb +188 -0
- data/lib/react_on_rails_pro/prepare_node_renderer_bundles.rb +40 -0
- data/lib/react_on_rails_pro/rendering_error.rb +5 -0
- data/lib/react_on_rails_pro/request.rb +318 -0
- data/lib/react_on_rails_pro/routes.rb +13 -0
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +102 -0
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +133 -0
- data/lib/react_on_rails_pro/server_rendering_pool/pro_rendering.rb +117 -0
- data/lib/react_on_rails_pro/stream_cache.rb +61 -0
- data/lib/react_on_rails_pro/stream_request.rb +170 -0
- data/lib/react_on_rails_pro/utils.rb +222 -0
- data/lib/react_on_rails_pro/v8_log_processor.rb +50 -0
- data/lib/react_on_rails_pro/version.rb +6 -0
- data/lib/react_on_rails_pro.rb +23 -0
- data/package-scripts.yml +109 -0
- data/package.json +159 -0
- data/rakelib/dummy_apps.rake +22 -0
- data/rakelib/lint.rake +32 -0
- data/rakelib/public_key_management.rake +155 -0
- data/rakelib/rbs.rake +47 -0
- data/rakelib/run_rspec.rake +81 -0
- data/rakelib/task_helpers.rb +45 -0
- data/rakelib/yard.rake +20 -0
- data/react_on_rails_pro.gemspec +47 -0
- data/readme-gen-docs.md +1 -0
- data/script/bootstrap +33 -0
- data/script/preinstall.js +31 -0
- data/script/setup +23 -0
- data/script/test +38 -0
- data/sig/react_on_rails_pro/cache.rbs +13 -0
- data/sig/react_on_rails_pro/configuration.rbs +100 -0
- data/sig/react_on_rails_pro/error.rbs +4 -0
- data/sig/react_on_rails_pro/utils.rbs +7 -0
- data/sig/react_on_rails_pro.rbs +5 -0
- data/yarn.lock +7599 -0
- metadata +319 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Heroku Deployment
|
|
2
|
+
|
|
3
|
+
Most React on Rails Pro installations of the Node SSR Renderer will deploy the Rails and Renderer
|
|
4
|
+
instances on the same server. This technique results in better performance since it avoids network
|
|
5
|
+
latency.
|
|
6
|
+
|
|
7
|
+
Scroll down if you want to have different servers.
|
|
8
|
+
|
|
9
|
+
## Deploying to the Same Server As Your App Server
|
|
10
|
+
|
|
11
|
+
[buildpack for runit](https://github.com/danp/heroku-buildpack-runit)
|
|
12
|
+
|
|
13
|
+
### Procfile
|
|
14
|
+
|
|
15
|
+
`/Procfile`
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
web: bin/runsvdir-dyno
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Procfile.web
|
|
22
|
+
|
|
23
|
+
`/Procfile.web`
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
puma: bundle exec puma -C config/puma.rb
|
|
27
|
+
renderer: bin/node-renderer
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### bin/node-renderer
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
#!/bin/bash
|
|
34
|
+
cd client
|
|
35
|
+
yarn run node-renderer
|
|
36
|
+
```
|
|
37
|
+
Be sure your script to run the node-renderer sets some port, like 3800 which is also set as the
|
|
38
|
+
config.renderer_url for your Rails server.
|
|
39
|
+
|
|
40
|
+
### node-renderer
|
|
41
|
+
Any task in client/package.json that starts the node-renderer
|
|
42
|
+
|
|
43
|
+
### Modifying Precompile Task
|
|
44
|
+
|
|
45
|
+
_Not necessary if you are using [bundle caching](../bundle-caching.md) as doing so will result in the below being done automatically._
|
|
46
|
+
|
|
47
|
+
To avoid the initial round trip to get a bundle on the renderer, you can do something like this to copy the file during precompile.
|
|
48
|
+
|
|
49
|
+
See [lib/tasks/assets.rake](https://github.com/shakacode/react_on_rails_pro/blob/master/lib/tasks/assets.rake) for a couple tasks that you can use.
|
|
50
|
+
|
|
51
|
+
If you're using the default tmp/bundles subdirectory for the node-renderer, you don't need to set the
|
|
52
|
+
ENV value for `RENDERER_BUNDLE_PATH`. Otherwise, please set this ENV value so the files get copied
|
|
53
|
+
to the right place.
|
|
54
|
+
|
|
55
|
+
Then you can use the rake task: `react_on_rails_pro:pre_stage_bundle_for_node_renderer`.
|
|
56
|
+
|
|
57
|
+
You might do something like this:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
Rake::Task["assets:precompile"]
|
|
61
|
+
.clear_prerequisites
|
|
62
|
+
.enhance([:environment, "react_on_rails:assets:compile_environment"])
|
|
63
|
+
.enhance do
|
|
64
|
+
Rake::Task["react_on_rails_pro:pre_stage_bundle_for_node_renderer"].invoke
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Troubleshooting
|
|
69
|
+
|
|
70
|
+
If you get this sort of error, then you're forgetting to configure the PORT on the node-renderer and
|
|
71
|
+
setting the config.renderer_url on the Rails App.
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
bundler: failed to load command: puma (/app/vendor/bundle/ruby/2.6.0/bin/puma)
|
|
75
|
+
Errno::EADDRINUSE: Address already in use - bind(2) for "0.0.0.0" port 21752
|
|
76
|
+
/app/vendor/bundle/ruby/2.6.0/gems/puma-4.3.3/lib/puma/binder.rb:229:in `initialize'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Separate Rails and Node Render Instances
|
|
81
|
+
|
|
82
|
+
### Deploy Node renderer to Heroku
|
|
83
|
+
|
|
84
|
+
1. Create your **Heroku** app with **Node.js** buildpack, say `renderer-test.herokuapp.com`.
|
|
85
|
+
2. In your JS configuration file or
|
|
86
|
+
1. If setting the port, ensure the port uses `process.env.PORT` so it will use port number provided by **Heroku** environment. The default is to use the env value RENDERER_PORT if available. (*TODO: Need to check on this*)
|
|
87
|
+
2. Set password in your configuration to something like `process.env.RENDERER_PASSWORD` and configure the corresponding **ENV variable** on your **Heroku** dyno so the `config/initializers/react_on_rails_pro.rb` uses this value.
|
|
88
|
+
3. Run deployment process (usually by pushing changes to **Git** repo associated with created **Heroku** app).
|
|
89
|
+
4. Once deployment process is finished, renderer should start listening from something like `renderer-test.herokuapp.com` host.
|
|
90
|
+
|
|
91
|
+
### Deploy react_on_rails application to Heroku
|
|
92
|
+
|
|
93
|
+
1. Create your **Heroku** app for `react_on_rails`.
|
|
94
|
+
2. Configure your app to communicate with renderer app you've created above. Put the following to your `initializers/react_on_rails_pro` (assuming you have **SSL** certificate uploaded to your renderer **Heroku** app or you use **Heroku** wildcard certificate under `*.herokuapp.com`) and configure corresponding **ENV variable** for the render_url and/or password on your **Heroku** dyno.
|
|
95
|
+
3. Run deployment process (usually by pushing changes to **Git** repo associated with created **Heroku** app).
|
|
96
|
+
4. Once deployment process is finished, all rendering requests form your `react_on_rails` app should be served by `<your-heroku-app>.herokuapp.com` app via **HTTPS**.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## References
|
|
101
|
+
|
|
102
|
+
* [Heroku Node Settings](https://github.com/damianmr/heroku-node-settings)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Node Renderer JavaScript Configuration
|
|
2
|
+
|
|
3
|
+
You can configure the node-renderer with only ENV values using the provided bin file `node-renderer`.
|
|
4
|
+
|
|
5
|
+
You can also create a custom configuration file to setup and launch the node-renderer.
|
|
6
|
+
|
|
7
|
+
The values in this file must be kept in sync with with the `config/initializers/react_on_rails_pro.rb` file, as documented in [Configuration](../configuration.md).
|
|
8
|
+
|
|
9
|
+
Here are the options available for the JavaScript renderer configuration object, as well as the available default ENV values if using the command line program node-renderer.
|
|
10
|
+
|
|
11
|
+
[//]: # 'If you change text here, you may want to update comments in packages/node-renderer/src/shared/configBuilder.ts as well.'
|
|
12
|
+
|
|
13
|
+
1. **port** (default: `process.env.RENDERER_PORT || 3800`) - The port the renderer should listen to.
|
|
14
|
+
[On Heroku](https://devcenter.heroku.com/articles/dyno-startup-behavior#port-binding-of-web-dynos) or [ControlPlane](https://docs.controlplane.com/reference/workload/containers#port-variable) you may want to use `process.env.PORT`.
|
|
15
|
+
1. **logLevel** (default: `process.env.RENDERER_LOG_LEVEL || 'info'`) - The renderer log level. Set it to `silent` to turn logging off.
|
|
16
|
+
[Available levels](https://getpino.io/#/docs/api?id=levels): `{ fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 }`. `silent` can be used as well.
|
|
17
|
+
1. **logHttpLevel** (default: `process.env.RENDERER_LOG_HTTP_LEVEL || 'error'`) - The HTTP server log level (same allowed values as `logLevel`).
|
|
18
|
+
1. **fastifyServerOptions** (default: `{}`) - Additional options to pass to the Fastify server factory. See [Fastify documentation](https://fastify.dev/docs/latest/Reference/Server/#factory).
|
|
19
|
+
1. **serverBundleCachePath** (default: `process.env.RENDERER_SERVER_BUNDLE_CACHE_PATH || process.env.RENDERER_BUNDLE_PATH || '/tmp/react-on-rails-pro-node-renderer-bundles'` ) - Path to a cache directory where uploaded server bundle files will be stored. This is distinct from Shakapacker's public asset directory. 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: `process.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`.
|
|
21
|
+
1. **password** (default: `env.RENDERER_PASSWORD`) - The password expected to receive from the **Rails client** to authenticate rendering requests.
|
|
22
|
+
If no password is set, no authentication will be required.
|
|
23
|
+
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.
|
|
24
|
+
1. **delayBetweenIndividualWorkerRestarts** (default: `env.RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS`) - Interval in minutes between individual worker restarts (when cluster restart is triggered). By default restarts are not enabled. If restarts are enabled, `allWorkersRestartInterval` should also be set.
|
|
25
|
+
1. **gracefulWorkerRestartTimeout**: (default: `env.GRACEFUL_WORKER_RESTART_TIMEOUT`) - Time in seconds that the master waits for a worker to gracefully restart (after serving all active requests) before killing it. Use this when you want to avoid situations where a worker gets stuck in an infinite loop and never restarts. This config is only usable if worker restart is enabled. The timeout starts when the worker should restart; if it elapses without a restart, the worker is killed.
|
|
26
|
+
1. **maxDebugSnippetLength** (default: 1000) - If the rendering request is longer than this, it will be truncated in exception and logging messages.
|
|
27
|
+
1. **supportModules** - (default: `env.RENDERER_SUPPORT_MODULES || null`) - If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS global objects and functions that get added to the VM context:
|
|
28
|
+
`{ Buffer, TextDecoder, TextEncoder, URLSearchParams, ReadableStream, process, setTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate, queueMicrotask }`.
|
|
29
|
+
This option is required to equal `true` if you want to use loadable components.
|
|
30
|
+
Setting this value to false causes the NodeRenderer to behave like ExecJS.
|
|
31
|
+
See also `stubTimers`.
|
|
32
|
+
1. **additionalContext** - (default: `null`) - additionalContext enables you to specify additional NodeJS objects (usually from https://nodejs.org/api/globals.html) to add to the VM context in addition to our `supportModules` defaults.
|
|
33
|
+
Object shorthand notation may be used, but is not required.
|
|
34
|
+
Example: `{ URL, Crypto }`
|
|
35
|
+
1. **stubTimers** - (default: `env.RENDERER_STUB_TIMERS` if that environment variable is set, `true` otherwise) - With this option set, use of functions `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, and `queueMicrotask` will do nothing during server-rendering.
|
|
36
|
+
This is useful when using dependencies like [react-virtuoso](https://github.com/petyosi/react-virtuoso) that use these functions during hydration.
|
|
37
|
+
In RORP, hydration typically is synchronous and single-task (unless you use streaming) and thus callbacks passed to task-scheduling functions should never run during server-side rendering.
|
|
38
|
+
Because these functions are valid client-side, they are ignored on server-side rendering without errors or warnings.
|
|
39
|
+
See also `supportModules`.
|
|
40
|
+
|
|
41
|
+
Deprecated options:
|
|
42
|
+
|
|
43
|
+
1. **bundlePath** - Renamed to `serverBundleCachePath`. The old name will continue to work but will log a deprecation warning.
|
|
44
|
+
1. **honeybadgerApiKey**, **sentryDsn**, **sentryTracing**, **sentryTracesSampleRate** - Deprecated and have no effect.
|
|
45
|
+
If you have any of them set, see [Error Reporting and Tracing](./error-reporting-and-tracing.md) for the new way to set up error reporting and tracing.
|
|
46
|
+
1. **includeTimerPolyfills** - Renamed to `stubTimers`.
|
|
47
|
+
|
|
48
|
+
## Example Launch Files
|
|
49
|
+
|
|
50
|
+
### Testing example:
|
|
51
|
+
|
|
52
|
+
[spec/dummy/client/node-renderer.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/node-renderer.js)
|
|
53
|
+
|
|
54
|
+
### Simple example:
|
|
55
|
+
|
|
56
|
+
Create a file './node-renderer.js'
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import path from 'path';
|
|
60
|
+
import { reactOnRailsProNodeRenderer } from 'react-on-rails-pro-node-renderer';
|
|
61
|
+
|
|
62
|
+
const config = {
|
|
63
|
+
// Save bundles to relative "./.node-renderer-bundles" dir of our app
|
|
64
|
+
serverBundleCachePath: path.resolve(__dirname, './.node-renderer-bundles'),
|
|
65
|
+
|
|
66
|
+
// All other values are the defaults, as described above
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// For debugging, run in single process mode
|
|
70
|
+
if (process.env.NODE_ENV === 'development') {
|
|
71
|
+
config.workersCount = 0;
|
|
72
|
+
}
|
|
73
|
+
// Renderer detects a total number of CPUs on virtual hostings like Heroku or CircleCI instead
|
|
74
|
+
// of CPUs number allocated for current container. This results in spawning many workers while
|
|
75
|
+
// only 1-2 of them really needed.
|
|
76
|
+
else if (process.env.CI) {
|
|
77
|
+
config.workersCount = 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
reactOnRailsProNodeRenderer(config);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
And add this line to your `scripts` section of `package.json`
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
"scripts": {
|
|
87
|
+
"start": "echo 'Starting React on Rails Pro Node Renderer.' && node ./node-renderer.js"
|
|
88
|
+
},
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`yarn start` will run the renderer.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Node Renderer Troubleshooting
|
|
2
|
+
|
|
3
|
+
- If you enabled restarts (having `allWorkersRestartInterval` and `delayBetweenIndividualWorkerRestarts`), you should set it with a high number to avoid the app from crashing because all Node renderer workers are stopped/killed.
|
|
4
|
+
|
|
5
|
+
- If your app contains streamed pages that take too much time to be streamed to the client, ensure to not set the `gracefulWorkerRestartTimeout` parameter or set to a high number, so the worker is not killed while it's still serving an active request.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Profiling Server-Side Renderer In RORP
|
|
2
|
+
|
|
3
|
+
This guide helps you profile the server-side code running in RORP node-renderer. It may help you find slow parts or bottlenecks in code.
|
|
4
|
+
|
|
5
|
+
This guide uses the RORP dummy app in profiling the server-side code.
|
|
6
|
+
|
|
7
|
+
## Profiling Server-Side Code Running On Node Renderer
|
|
8
|
+
|
|
9
|
+
1. Run node-renderer using the `--inspect` node option.
|
|
10
|
+
|
|
11
|
+
Open the `spec/dummy/Procfile.dev` file and update the `node-renderer` process to run the renderer using `node --inspect` command. Change the following line
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node-renderer: RENDERER_LOG_LEVEL=debug yarn run node-renderer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node --inspect client/node-renderer.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
1. Run the App
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bin/dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
1. Visit `chrome://inspect` on Chrome browser and you should see something like this:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
1. Click the `inspect` link. This should open a developer tools window. Open the performance tab there
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
1. Click the `record` button
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
1. Open the web app you want to test and refresh it multiple times. We use the React on Rails Pro dummy app for this tutorial. So, we will open it in the browser by going to [http://localhost:3000](http://localhost:3000)
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
1. If you get any `Timeout Error` while visiting the page, you may need to increase the `ssr_timeout` in the Ruby on Rails initializer file. **Running node-renderer** using the `--inspect` flag makes it slower. So, you can increase the `ssr_timeout` to `10 seconds` by adding the following line to `config/initializers/react_on_rails_pro.rb` file
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
config.ssr_timeout = 10
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
1. Stop performance recording
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
1. You should see something like this
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## Profile Analysis
|
|
60
|
+
|
|
61
|
+
You can see that there is much work done during the first request because it contains the process of uploading the component bundles and executing the server-side bundle code.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
By zooming into it, you can see the function that’s called `buildVM` (you can also search for it by clicking Ctrl+f and type the function name)
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
All code that runs later inside that code context calls the `runInContext` function. Like this code that calls `renderToString` to render a specific react component
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
To check the profile of other requests, zoom into any of the following spots of work
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
You should find a call to `serverRenderReactComponent`
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
**If you can’t find any requests coming to the renderer server, component caching may be the cause.** You can try to disable React on Rails caching by adding the following line to `config/initializers/react_on_rails_pro.rb` file
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
config.prerender_caching = false
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Profiling Renderer With High Loads
|
|
88
|
+
|
|
89
|
+
To see the renderer behavior while there are many requests coming to it, you can use the `ApacheBench (ab)` tool that lets you make many HTTP requests to a specific end points at the same time.
|
|
90
|
+
|
|
91
|
+
1. The `ApacheBench (ab)` is installed on macOS by default. You can install it on Linux by running the following command
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
sudo apt-get install apache2-utils
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
1. Do all steps in `Profiling Server-Side Code Running On Node Renderer` section except the step number 6. Instead of opening the page in the browser, let the `ab` tool make many HTTP requests for you by running the following command.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ab -n 100 -c 10 http://localhost:3000/
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
1. Now, we you open the noder-renderer profile, you will see it very busy responding to all requests
|
|
104
|
+
|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
1. Then, you can analyze the renderer behavior of each request as stated in `Profile Analysis` section.
|
|
108
|
+
|
|
109
|
+
### ExecJS
|
|
110
|
+
React on Rails Pro supports profiling with ExecJS starting from version **4.0.0**. You will need to do more work to profile ExecJS if you are using an older version.
|
|
111
|
+
|
|
112
|
+
If you are using **v4.0.0** or later, you can enable the profiler by setting the `profile_server_rendering_js_code` config by adding the following line to the ReactOnRails initializer.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
config.profile_server_rendering_js_code = true
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then, run the app you are profiling and open some pages in it.
|
|
119
|
+
You will find many log files with the name `isloate-0x*.log` in the root of your app. You can use the following command to analyze the log files.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
rake react_on_rails_pro:process_v8_logs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
You will find all log files are converted to `profile.v8log.json` file and moved to the **v8_profiles** directory.
|
|
126
|
+
|
|
127
|
+
You can use `speedscope` to analyze the `profile.v8log.json` file. You can install `speedscope` by running the following command
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm install -g speedscope
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then, you can analyze the profile by running the following command
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
speedscope /path/to/profile.v8log.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Profiling ExecJS with Older Versions of React on Rails Pro
|
|
140
|
+
|
|
141
|
+
If you are using an older version of React on Rails Pro, you need to do more work to profile the server-side code running in the ExecJS.
|
|
142
|
+
|
|
143
|
+
If you are using `node` as the runtime for ExecJS, you can enable the profiler by adding the following code on top of the `ReactOnRailsPro` initializer.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class CustomRuntime < ExecJS::ExternalRuntime
|
|
147
|
+
def initialize
|
|
148
|
+
super(
|
|
149
|
+
name: 'Custom Node.js (with --prof)',
|
|
150
|
+
command: ['node --prof'],
|
|
151
|
+
runner_path: ExecJS.root + '/support/node_runner.js'
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
ExecJS.runtime = CustomRuntime.new
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If you are using V8 as the runtime for ExecJS, you can enable the profiler by adding the following code on top of the `ReactOnRailsPro` initializer.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
class CustomRuntime < ExecJS::ExternalRuntime
|
|
163
|
+
def initialize
|
|
164
|
+
super(
|
|
165
|
+
name: 'Custom V8 (with --prof)',
|
|
166
|
+
command: ['d8 --prof'],
|
|
167
|
+
runner_path: ExecJS.root + '/support/v8_runner.js'
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
After adding the code, you can run the app and open some pages in it. You will find many log files with the name `isloate-0x*.log` in the root of your app. You can use the following command to analyze any log file.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
node --prof-process --preprocess -j isolate*.log > profile.v8log.json
|
|
177
|
+
npm install -g speedscope
|
|
178
|
+
speedscope /path/to/profile.v8log.json
|
|
179
|
+
```
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Add Streaming and Interactivity to RSC Page
|
|
2
|
+
|
|
3
|
+
Before reading this document, please read the [Create React Server Component without SSR](./create-without-ssr.md) document.
|
|
4
|
+
|
|
5
|
+
## Make the React Server Component Page Progressively Load
|
|
6
|
+
React Server Components support progressive loading, which means they can be built as asynchronous functions that resolve and render after the initial HTML is sent to the client. This enables a better user experience by:
|
|
7
|
+
|
|
8
|
+
1. Showing initial content quickly while async data loads;
|
|
9
|
+
2. Maintaining interactivity while loading;
|
|
10
|
+
3. Streaming updates to the page as server components resolve.
|
|
11
|
+
|
|
12
|
+
This progressive enhancement approach allows React Server Components to efficiently handle data fetching and rendering without blocking the initial page load.
|
|
13
|
+
|
|
14
|
+
Let's create an `async` React Server Component that will be progressively loaded.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
// app/javascript/components/Posts.jsx
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import fetch from 'node-fetch';
|
|
20
|
+
import _ from 'lodash';
|
|
21
|
+
import moment from 'moment';
|
|
22
|
+
|
|
23
|
+
const Posts = async () => {
|
|
24
|
+
// Add artificial delay to simulate network latency
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
26
|
+
|
|
27
|
+
const posts = await (await fetch(`http://localhost:3000/api/posts`)).json();
|
|
28
|
+
const postsByUser = _.groupBy(posts, 'user_id');
|
|
29
|
+
const onePostPerUser = _.map(postsByUser, (group) => group[0]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
{onePostPerUser.map((post) => (
|
|
34
|
+
<div style={{ border: '1px solid black', margin: '10px', padding: '10px' }}>
|
|
35
|
+
<h1>{post.title}</h1>
|
|
36
|
+
<p>{post.body}</p>
|
|
37
|
+
<p>
|
|
38
|
+
Created <span style={{ fontWeight: 'bold' }}>{moment(post.created_at).fromNow()}</span>
|
|
39
|
+
</p>
|
|
40
|
+
<img src="https://placehold.co/200" alt={post.title} />
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default Posts;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The async `Posts` component fetches and displays a list of posts, showing one post per user with title, body, timestamp and thumbnail image.
|
|
51
|
+
|
|
52
|
+
Let's add the Posts component to the React Server Component Page.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// app/javascript/packs/components/ReactServerComponentPage.jsx
|
|
56
|
+
import React from 'react';
|
|
57
|
+
import ReactServerComponent from '../../components/ReactServerComponent';
|
|
58
|
+
import Posts from '../../components/Posts';
|
|
59
|
+
|
|
60
|
+
const ReactServerComponentPage = () => {
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
<ReactServerComponent />
|
|
64
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
65
|
+
<Posts />
|
|
66
|
+
</Suspense>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default ReactServerComponentPage;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The `Suspense` component is used to wrap the Posts component to handle its loading state. The `fallback` prop is used to display a loading message while the Posts component is loading.
|
|
75
|
+
|
|
76
|
+
## Run the Development Server
|
|
77
|
+
|
|
78
|
+
Run the development server:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bin/dev
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Navigate to the React Server Component Page:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
http://localhost:3000/react_server_component_without_ssr
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
When you open the page, you'll see the React Server Component render immediately, followed by the "Loading..." fallback state from the Suspense component. After a 1-second delay, the Posts component will render with the fetched data. This artificial delay helps demonstrate how React Server Components handle asynchronous operations and streaming:
|
|
91
|
+
|
|
92
|
+
1. The page loads instantly with the ReactServerComponent.
|
|
93
|
+
2. The Suspense fallback shows "Loading..." where the Posts will appear.
|
|
94
|
+
3. After the delay, the Posts component streams in and replaces "Loading...".
|
|
95
|
+
|
|
96
|
+
## How The Streaming Works
|
|
97
|
+
|
|
98
|
+
The streaming happens through the `rsc_payload/ReactServerComponentPage` fetch request that React on Rails Pro initiates when loading the page. The server keeps this connection open and sends data in chunks:
|
|
99
|
+
|
|
100
|
+
1. The initial chunk contains the immediately available content (ReactServerComponent).
|
|
101
|
+
2. When the Posts component's async operation completes, the server sends another chunk with its rendered content.
|
|
102
|
+
3. The browser progressively receives and renders these chunks, updating the page seamlessly.
|
|
103
|
+
|
|
104
|
+
This streaming approach means users see content as soon as it's ready, rather than waiting for everything to load before seeing anything. The `Suspense` boundary ensures a smooth transition between the loading state and the final content.
|
|
105
|
+
|
|
106
|
+
You can observe this streaming behavior in your browser's network tab: the `rsc/ReactServerComponentPage` request will show multiple chunks arriving over time, each one adding more content to your page.
|
|
107
|
+
|
|
108
|
+
## Add Interactivity
|
|
109
|
+
|
|
110
|
+
Let's add interactivity to the Posts component. Only client components can be interactive, so we'll create a new client component that helps us to show or hide the post image and call it `ToggleContainer`. It can receive any component as a child and toggle the visibility of the child component.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// app/javascript/components/ToggleContainer.jsx
|
|
114
|
+
'use client';
|
|
115
|
+
|
|
116
|
+
import React, { useState } from 'react';
|
|
117
|
+
|
|
118
|
+
const ToggleContainer = ({ children }) => {
|
|
119
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<button onClick={() => setIsVisible((prev) => !prev)}>Toggle</button>
|
|
124
|
+
{isVisible && children}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default ToggleContainer;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Now, let's use the `ToggleContainer` component to wrap the post image.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
// app/javascript/components/Posts.jsx
|
|
136
|
+
import ToggleContainer from './ToggleContainer';
|
|
137
|
+
|
|
138
|
+
const Posts = () => {
|
|
139
|
+
// existing code..
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div>
|
|
143
|
+
{onePostPerUser.map((post) => (
|
|
144
|
+
<div>
|
|
145
|
+
{/* existing code.. */}
|
|
146
|
+
<ToggleContainer>
|
|
147
|
+
<img src="https://placehold.co/200" alt={post.title} />
|
|
148
|
+
</ToggleContainer>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default Posts;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Now when you visit the page, you'll see a "Toggle" button for each post. Clicking the button will show/hide that post's image. This demonstrates how we can add client-side interactivity to a React Server Component by creating a client component (`ToggleContainer`) that manages its own state.
|
|
159
|
+
|
|
160
|
+
The `ToggleContainer` is marked with [`'use client'`](https://react.dev/reference/rsc/use-client) directive, indicating it runs on the client-side and can handle user interactions. It uses the `useState` hook to maintain the visibility state of its children. Meanwhile, the parent `Posts` component remains a server component, fetching and rendering the initial posts data on the server.
|
|
161
|
+
|
|
162
|
+
It's important to note that while client components (like `ToggleContainer`) cannot directly import server components, they can receive server components as props (like children in this case). This is why we can pass the server-rendered image element as a child to our client-side `ToggleContainer` component. This pattern allows for flexible composition while maintaining the boundaries between server and client code.
|
|
163
|
+
|
|
164
|
+
This pattern allows us to optimize performance by keeping most of the component logic on the server while selectively adding interactivity where needed on the client.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## Checking The Network Requests
|
|
168
|
+
|
|
169
|
+
Let's check what bundles are being loaded for this page. By opening the browser's developer tools and going to the "Network" tab, you can see JavaScript bundles being loaded for this page.
|
|
170
|
+
|
|
171
|
+

|
|
172
|
+
|
|
173
|
+
Looking at the network requests, you'll notice two key JavaScript bundles:
|
|
174
|
+
|
|
175
|
+
1. The original `ReactServerComponentPage.js` bundle (1.4KB) - This contains the core server component code.
|
|
176
|
+
2. A new `client25.js` (can be different for you) bundle - This contains the client-side interactive code, specifically the `ToggleContainer` component and React hooks like `useState`.
|
|
177
|
+
|
|
178
|
+
The browser automatically knows to load this additional client bundle because of how React Server Components work:
|
|
179
|
+
|
|
180
|
+
1. When the server renders the RSC tree, it includes references to any client components used (in this case, `ToggleContainer`).
|
|
181
|
+
2. These references point to the specific JavaScript chunks needed to hydrate those client components.
|
|
182
|
+
3. The React runtime on the client then ensures those chunks are loaded before hydrating the interactive parts of the page.
|
|
183
|
+
|
|
184
|
+
This demonstrates one of the key benefits of React Server Components - automatic code splitting and loading of just the client-side JavaScript needed for interactivity, while keeping the bulk of the application logic on the server.
|
|
185
|
+
|
|
186
|
+
For more details on this architecture, see React's [Server Components documentation](https://react.dev/learn/thinking-in-react#how-react-server-components-work).
|
|
187
|
+
|
|
188
|
+
## Next Steps
|
|
189
|
+
|
|
190
|
+
Now that you understand how to add streaming and interactivity to React Server Components, you can proceed to the next article: [SSR React Server Components](./server-side-rendering.md) to learn how to enable server-side rendering (SSR) for your React Server Components.
|