Skip to main content

Profiling Server-Side Rendering Code

This guide helps you profile server-side JavaScript running through React on Rails Pro so you can find slow paths and bottlenecks.

Use this page when you need a CPU profile of the Pro Node Renderer or an ExecJS/V8 log. For breakpoints and renderer logs, see Node Renderer Debugging. For React 19.2 Performance Tracks, browser traces, and a profiling decision guide, see React Performance Tracks and Profiling.

The examples below use the sample app in react_on_rails_pro/spec/dummy.

Prerequisite: This guide assumes you have Overmind installed. On macOS, you can install it with brew install overmind.

Profiling the Pro Node Renderer

  1. Start the sample app with Overmind.

    cd react_on_rails_pro/spec/dummy
    overmind start -f Procfile.dev
  2. In a second terminal, stop only the managed node-renderer process.

    cd react_on_rails_pro/spec/dummy
    overmind stop node-renderer
  3. In that second terminal, restart the renderer manually with the Node inspector enabled.

    cd react_on_rails_pro/spec/dummy
    RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node --inspect renderer/node-renderer.js

    Keep this terminal open while you profile. In the repository dummy app you can also use pnpm run node-renderer:debug, which runs the same renderer entry point with --inspect. In another app, use the same package script with your package manager, such as npm run node-renderer:debug or yarn node-renderer:debug.

  4. Visit chrome://inspect in Chrome. You should see the Node renderer process:

    Chrome Inspect Tab

  5. Click the inspect link. This opens a dedicated DevTools window for the Node process. Open the Performance tab there.

    Chrome Performance Tab

  6. Click the record button.

    Chrome Performance Tab

  7. Open the web app you want to test and refresh it multiple times. In the sample app, that means visiting http://localhost:3000.

    RORP Dummy App

  8. If the page raises a Timeout Error, temporarily increase ssr_timeout in config/initializers/react_on_rails_pro.rb. Running the renderer with --inspect slows SSR enough that a normal development timeout can be too short.

    config.ssr_timeout = 10
  9. Stop performance recording.

    Running profiler at the performance tab

  10. Inspect the recorded profile.

    Recorded Node JS profile

Profile Analysis

The first request usually includes extra work because Rails uploads component bundles and the renderer executes the server-side bundle code.

Recorded Node JS profile

Zoom into that first request and search for buildVM with Ctrl+F or Cmd+F.

NodeJS startup code profile

Code that runs inside that VM context appears under runInContext. For example, server-rendered components that use renderToString show up below that frame.

runInContext function profile

For later requests, zoom into another request-sized block of work.

Recorded Node JS profile

You should find a call to serverRenderReactComponent.

serverRenderReactComponent function profile

If you cannot 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:

config.prerender_caching = false

If the slow path includes client hydration, browser layout, or React Server Components timing, collect a separate browser trace using React Performance Tracks and Profiling. The Node profile explains renderer CPU time; the browser trace explains what happened after the response reached the browser.

Profiling Renderer With High Loads

To see renderer behavior under concurrent local traffic, use ApacheBench (ab) to make many HTTP requests to the same endpoint.

  1. ApacheBench (ab) is installed on macOS by default. On Linux, install it with:

    sudo apt-get install apache2-utils
  2. Follow the steps in Profiling the Pro Node Renderer, but instead of opening the page in the browser, let ab drive the traffic:

    ab -n 100 -c 10 http://localhost:3000/
  3. The Node profile should show the renderer responding to concurrent requests.

    Busy renderer profile

  4. Analyze each request-sized block as described in Profile Analysis.

Profiling ExecJS

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.

If you are using v4.0.0 or later, enable the profiler by setting profile_server_rendering_js_code in the React on Rails Pro initializer:

config.profile_server_rendering_js_code = true

Then, run the app you are profiling and open some pages in it. You will find log files named isolate-0x*.log in the root of your app. Use the following command to analyze the log files:

rake react_on_rails_pro:process_v8_logs

The task converts the logs to profile.v8log.json files and moves them to the v8_profiles directory.

You can analyze the profile.v8log.json file with speedscope:

pnpm dlx speedscope /path/to/profile.v8log.json
# or with npm:
npx speedscope /path/to/profile.v8log.json
# or with Yarn:
yarn dlx speedscope /path/to/profile.v8log.json

Profiling ExecJS with Older Versions of React on Rails Pro

If you are using an older version of React on Rails Pro, you need to configure the ExecJS runtime manually.

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.

class CustomRuntime < ExecJS::ExternalRuntime
def initialize
super(
name: 'Custom Node.js (with --prof)',
command: ['node --prof'],
runner_path: ExecJS.root + '/support/node_runner.js'
)
end
end

ExecJS.runtime = CustomRuntime.new

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.

class CustomRuntime < ExecJS::ExternalRuntime
def initialize
super(
name: 'Custom V8 (with --prof)',
command: ['d8 --prof'],
runner_path: ExecJS.root + '/support/v8_runner.js'
)
end
end

After adding the code, run the app and open the pages you want to profile. You will find log files named isolate-0x*.log in the root of your app. Use these commands to analyze a log:

node --prof-process --preprocess -j isolate-0x*.log > profile.v8log.json
pnpm dlx speedscope /path/to/profile.v8log.json
# or with npm:
npx speedscope /path/to/profile.v8log.json
# or with Yarn:
yarn dlx speedscope /path/to/profile.v8log.json