Skip to content

Latest commit

 

History

History
184 lines (125 loc) · 11.3 KB

File metadata and controls

184 lines (125 loc) · 11.3 KB

Node Renderer

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.

Why Use the Node Renderer?

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.

Performance Benefits

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.

How It Works

  1. Rails sends a rendering request (component name, props, and JavaScript bundle reference) to the Node Renderer over HTTP
  2. The Node Renderer evaluates the server bundle in a Node.js worker
  3. The rendered HTML is returned to Rails and inserted into the view
  4. Workers are pooled and can be automatically restarted to mitigate memory leaks

Key Features

  • 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

Getting Started

Quick Setup (Generator)

The fastest way to set up the Node Renderer is with the Pro generator:

bundle exec rails generate react_on_rails:pro

This creates the Node Renderer entry point, configures webpack, and adds the renderer to Procfile.dev.

Manual Setup

For fine-grained control, see the Node Renderer installation section in the installation guide.

Configuration

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")
end

Renderer Password Security

The 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:

  1. config.renderer_password (blank values fall through to the next step)
  2. Password embedded in config.renderer_url (for example, https://:password@localhost:3800)
  3. 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")

Eliminating Cold-Start Latency in Docker Deployments

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.

Pre-seeding the bundle cache

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_cache

Both 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.

Configuration

The task follows the same environment-variable precedence as the Node Renderer, while the default fallback can differ between Ruby and standalone Node environments:

  1. RENDERER_SERVER_BUNDLE_CACHE_PATH environment variable (preferred)
  2. RENDERER_BUNDLE_PATH environment variable (deprecated — emits a warning)
  3. Rails.root.join(".node-renderer-bundles") (Rails-side default when env vars are unset, only accepted for MODE=symlink and 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

Impact

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

Rolling deploys: seed current and previous bundle hashes

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.

Further Reading