docs/oss/core-concepts/performance-benchmarks.md
This page covers the performance characteristics of React on Rails across different rendering strategies, with data from real-world deployments and comparative benchmarks.
The default ExecJS renderer evaluates JavaScript synchronously inside a single-threaded pool. The Node Renderer (React on Rails Pro) runs a dedicated Node.js master process with worker processes (via cluster.fork()), providing dramatically better throughput.
| Metric | ExecJS (mini_racer) | ExecJS (Node.js runtime) | Node Renderer (Pro) |
|---|---|---|---|
| Architecture | V8 isolate in Ruby process | New process per eval call | Persistent Node.js workers |
| Concurrency (MRI) | Single-threaded (pool size 1) | Single-threaded (pool size 1) | Multi-worker |
| Async support | None | None | Full (Promises, timers) |
| Streaming SSR | Not supported | Not supported | Supported |
| RSC support | Not supported | Not supported | Supported |
| Typical speedup | Baseline | Comparable | 3-10x over ExecJS |
The Node Renderer's persistent process supports full async rendering and multi-worker concurrency, which are the primary sources of the performance difference. Popmenu reported a 73% decrease in response times after switching to Pro. ExecJS is limited to synchronous rendering within a single-threaded pool, so the gap widens for pages with many async data sources or large component trees.
React on Rails supports code splitting (Pro feature) to reduce the amount of JavaScript sent to the browser. The impact depends on your application's structure:
Code splitting with dynamic import() breaks your application into smaller chunks loaded on demand:
For applications with many routes and components, code splitting can substantially reduce initial bundle size. The actual reduction depends on how much code is shared across routes — apps with mostly independent route content typically see larger gains.
Server bundles are not typically split because the server can load the full bundle once at startup. However, with React Server Components, server-only dependencies are excluded from the client bundle entirely, which compounds the benefits of code splitting.
Streaming SSR (Pro feature) uses React's renderToPipeableStream to send HTML progressively as components resolve:
| Rendering Strategy | TTFB | Full Page Load |
|---|---|---|
| Client-side only | Fast (empty shell) | Slow (fetch + render) |
Traditional SSR (renderToString) | Slow (waits for all data) | Fast (complete HTML) |
| Streaming SSR | Fast (shell immediately) | Progressive (chunks arrive) |
Streaming SSR provides the best of both approaches: the browser receives the initial HTML shell immediately (fast TTFB) while data-dependent sections stream in as they resolve. This is especially valuable for pages with multiple independent data sources.
With streaming SSR and React 18+, components can hydrate independently as their JavaScript loads:
Note: By default, React on Rails uses defer scripts which delay all hydration until the page finishes streaming. To enable selective hydration, configure your initializer:
config.generated_component_packs_loading_strategy = :async
See Selective Hydration in Streamed Components for complete details.
React Server Components (Pro feature) provide additional performance benefits on top of streaming SSR:
Server components and their dependencies are excluded from the client bundle. In practice, this means:
// These imports stay server-side — zero client cost
import { format } from 'date-fns'; // ~30KB
import { marked } from 'marked'; // ~35KB
import numeral from 'numeral'; // ~25KB
Applications that use heavy formatting, parsing, or data-processing libraries on the server side see the largest gains. Frigade reported a 62% reduction in client-side bundle size after migrating to RSC.
Server components produce HTML that does not need hydration — they have no client-side JavaScript. Only client components (those with 'use client') require hydration. This reduces Total Blocking Time and improves Time to Interactive.
Popmenu, a restaurant platform serving tens of millions of SSR requests daily, adopted React on Rails Pro and reported:
See the full case study.
config.tracing = true in config/initializers/react_on_rails_pro.rb to log render timings)webpack-bundle-analyzer: Visualize bundle composition and identify large dependenciesRails.logger when config.logging_on_server = trueRENDERER_LOG_LEVEL (Pro)