The React on Rails Pro Node Renderer replaces ExecJS with a dedicated Node.js server for server-side rendering. It eliminates the limitations of embedded JavaScript execution and provides significant performance improvements for production applications.
Note
Summary for AI agents: Use this page when the user asks about the Node renderer, ExecJS alternatives, or SSR performance. This is the Pro-level overview; for technical setup, see Node Renderer basics and JS configuration. The Node renderer is required for RSC.
Route map: Start at React on Rails Pro if you're choosing a path. This page is the canonical Node Renderer overview; use the linked install and technical docs below for the deeper implementation details.
ExecJS embeds a JavaScript runtime (mini_racer/V8) inside the Ruby process. This works for small apps but creates problems at scale:
- Memory pressure — V8 contexts consume memory inside each Ruby process, competing with Rails for resources
- No Node tooling — You cannot use standard Node.js profiling, debugging, or memory leak detection tools with ExecJS
- Process crashes — JavaScript memory leaks can crash your Ruby server
- Limited concurrency — ExecJS renders synchronously within the Ruby request cycle
The Pro Node Renderer solves all of these by running a standalone Node.js server that handles rendering requests from Rails over HTTP.
| Metric | ExecJS | Node Renderer |
|---|---|---|
| SSR throughput | Baseline | 10-100x faster |
| Memory isolation | Shared with Ruby | Separate process |
| Worker concurrency | Single-threaded per request | Configurable worker pool |
| Profiling | Not available | Full Node.js tooling |
| Memory leak recovery | Crashes Ruby | Rolling worker restarts |
At Popmenu (a ShakaCode client), switching to the Node Renderer contributed to a 73% decrease in average response times and 20-25% lower Heroku costs across tens of millions of daily SSR requests.
- Rails sends a rendering request (component name, props, and JavaScript bundle reference) to the Node Renderer over HTTP
- The Node Renderer evaluates the server bundle in a Node.js worker
- The rendered HTML is returned to Rails and inserted into the view
- Workers are pooled and can be automatically restarted to mitigate memory leaks
- Worker pool — Configurable number of workers (defaults to CPU count minus 1)
- Rolling restarts — Automatic worker recycling to prevent memory leak buildup
- Bundle caching — Server bundles are cached on the Node side for fast re-renders
- Shared secret authentication — Secure communication between Rails and Node
- Prerender caching — Combined with prerender caching, rendering results are cached across requests
The fastest way to set up the Node Renderer is with the Pro generator:
bundle exec rails generate react_on_rails:proThis creates the Node Renderer entry point, configures webpack, and adds the renderer to Procfile.dev.
For fine-grained control, see the Node Renderer installation section in the installation guide.
Configure Rails to use the Node Renderer:
# config/initializers/react_on_rails_pro.rb
ReactOnRailsPro.configure do |config|
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
endThe renderer password secures communication between Rails and the Node Renderer. React on Rails Pro enforces secure defaults by environment:
| Environment | Password Required? | Behavior |
|---|---|---|
development |
No | Optional — no authentication if unset |
test |
No | Optional — no authentication if unset |
(neither set) |
Yes | Treated as production-like; RENDERER_PASSWORD required |
staging |
Yes | Raises error on boot if RENDERER_PASSWORD is missing |
production |
Yes | Raises error on boot if RENDERER_PASSWORD is missing |
qa, preview, etc. |
Yes | Raises error on boot if RENDERER_PASSWORD is missing |
In production-like environments (anything other than development or test), both the Rails app and the Node Renderer will refuse to start without a non-empty password. Set the same RENDERER_PASSWORD for both sides:
# Set for both Rails and Node Renderer
export RENDERER_PASSWORD="your-secure-password"The Node Renderer reads RENDERER_PASSWORD directly from process.env. On the Ruby side, React on Rails Pro
resolves the password in this order:
config.renderer_password(blank values fall through to the next step)- Password embedded in
config.renderer_url(for example,https://:password@localhost:3800) ENV["RENDERER_PASSWORD"]
So setting RENDERER_PASSWORD in the environment is enough unless you intentionally override it in
the initializer or URL.
If neither NODE_ENV nor RAILS_ENV is set, the Node Renderer treats the environment as
production-like and still requires RENDERER_PASSWORD.
For local development, you can either omit the password entirely (no authentication) or set a convenience default:
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")When a new container starts, the Node Renderer has an empty bundle cache. The first SSR request triggers a costly 410→retry round-trip where Rails sends the full bundle over HTTP, adding 200ms–1s+ of latency depending on bundle size. In rolling deploys, this affects every new pod.
The pre_seed_renderer_cache rake task stages compiled server bundles directly into the renderer's cache directory, so the renderer finds them immediately on startup.
It supports two modes, both producing the same on-disk cache layout (<cache>/<bundleHash>/<bundleHash>.js):
MODE=copy(default) — copies files. Use in Docker/image builds so the cache is baked into an immutable artifact.MODE=symlink— creates relative symlinks. For same-filesystem workflows (local dev, CI, Heroku-style same-dyno deploys, bundle-caching restores).
# After webpack/assets build step (Docker image build)
ENV RENDERER_SERVER_BUNDLE_CACHE_PATH=/app/.node-renderer-bundles
RUN bundle exec rake react_on_rails_pro:pre_seed_renderer_cacheBoth modes stage the server bundle, any configured assets_to_copy, and (when RSC is enabled) the RSC bundle and its companion manifests.
The pre_seed_renderer_cache task is also invoked automatically at the end of assets:precompile with MODE=symlink, so the local/CI/Heroku path has zero new configuration.
Note
The older react_on_rails_pro:pre_stage_bundle_for_node_renderer rake task and ReactOnRailsPro::PrepareNodeRenderBundles class are deprecated in favor of the unified API. Both remain available as thin shims that emit a deprecation warning and delegate to MODE=symlink. react_on_rails:doctor flags deploy scripts that still reference the deprecated task.
The task follows the same environment-variable precedence as the Node Renderer, while the default fallback can differ between Ruby and standalone Node environments:
RENDERER_SERVER_BUNDLE_CACHE_PATHenvironment variable (preferred)RENDERER_BUNDLE_PATHenvironment variable (deprecated — emits a warning)Rails.root.join(".node-renderer-bundles")(Rails-side default when env vars are unset, only accepted forMODE=symlinkand in dev/test)
In MODE=copy (Docker image builds) the task requires one of the env vars above to be set in non-dev/test environments. "Non-dev/test" means any RAILS_ENV other than development or test — including custom environments like staging, review, or ci — so set RENDERER_SERVER_BUNDLE_CACHE_PATH wherever you run MODE=copy outside of local/CI-test runs. Because the Node renderer's own default can differ (e.g., falling back to /tmp/react-on-rails-pro-node-renderer-bundles when its cwd sits outside the app tree), relying on the silent fallback risks pre-seeded bundles landing in a directory the renderer never reads. The task raises a clear error if the env var is missing:
ENV RENDERER_SERVER_BUNDLE_CACHE_PATH=/app/.node-renderer-bundles
RUN bundle exec rake react_on_rails_pro:pre_seed_renderer_cache| Scenario | Before | After |
|---|---|---|
| First request on fresh deploy | 410→retry: 200ms–1s+ | Direct render: <50ms |
| Thundering herd on new pod | N requests queue behind per-bundle lock | All requests served immediately |
During a rolling deploy, new renderer instances can receive requests for both the current deployed bundle hash and the previous hash while old Rails instances drain. Treat this as a two-hash cache-seeding problem, not a single-file problem.
At startup, aim to have the cache contain:
- the current server bundle hash
- the previous server bundle hash
- the current and previous RSC bundle hashes as well, if RSC support is enabled
- any required copied assets and RSC manifests in each seeded hash directory
pre_seed_renderer_cache seeds the current locally built bundle outputs. For the previous deployed hash, the most practical approach is to publish bundle artifacts keyed by hash after each successful deploy, then fetch the previous hash artifact during the next build and place it into the same <cache>/<bundleHash>/... layout before boot.
- Node Renderer basics — Architecture and core concepts
- JavaScript configuration — Node-side config options
- Error reporting and tracing — Monitoring in production
- Heroku deployment — Deploy the renderer on Heroku
- Debugging — Troubleshooting renderer issues
- Troubleshooting — Common problems and solutions