PageSourceSearch

https://shakacode.com/component---src-template-doc-article-tsx-con…mance-benchmarks-md-62aeae342e68d54998e7.js

js shakacode.com collected 2026-10-04 00:25:57 UTC 68,097 bytes, 2 lines download raw bytes

1"use strict";(self.webpackChunkgatsby_starter_default=self.webpackChunkgatsby_starter_default||[]).push([[1462],{67191:function(e,n,t){t.r(n),t.d(n,{Head:function(){return g},default:function(){return u}});var a=t(28453),r=t(96540);function l(e){const n=Object.assign({h1:"h1",a:"a",span:"span",p:"p",strong:"strong",h2:"h2",ol:"ol",li:"li",em:"em",h3:"h3",ul:"ul",div:"div",svg:"svg",path:"path",table:"table",thead:"thead",tr:"tr",th:"th",tbody:"tbody",td:"td"},(0,a.R)(),e.components);return r.createElement(r.Fragment,null,r.createElement(n.h1,{id:"performance-benchmarks",style:{position:"relative"}},r.createElement(n.a,{href:"#performance-benchmarks","aria-label":"performance benchmarks permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Performance Benchmarks"),"\n",r.createElement(n.p,null,"The point of this page is not to admire numbers. It is to help you ",r.createElement(n.strong,null,"make a page measurably\nfaster and prove the win")," — increasingly with an AI agent doing the conversion while a benchmark\nharness keeps it honest."),"\n",r.createElement(n.p,null,"The React on Rails v17 story (React 19.2, ",r.createElement(n.a,{href:"/linked-assets/67be9e982a0c13953bd3d4e91f6d08f8/index.md"},"React Server Components"),",\n",r.createElement(n.a,{href:"/linked-assets/36f649125c45c80a71092e7283f882be/streaming-server-rendering.md"},"streaming SSR"),", and the\n",r.createElement(n.a,{href:"/linked-assets/b6490192988c8a036f273ba296fd689e/basics.md"},"Node renderer"),") is that an existing SSR or\nclient-rendered page can be converted to a faster rendering strategy — often by prompting a coding\nagent — and the result can be verified with a trustworthy A/B benchmark instead of a hopeful\nbefore/after. The rest of this page is the loop, the harness that proves it, the levers you pull,\nand how to read the result without fooling yourself."),"\n",r.createElement(n.h2,{id:"the-improve-and-prove-loop",style:{position:"relative"}},r.createElement(n.a,{href:"#the-improve-and-prove-loop","aria-label":"the improve and prove loop permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"The improve-and-prove loop"),"\n",r.createElement(n.p,null,"A modern performance change on React on Rails looks like this:"),"\n",r.createElement(n.ol,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Point an agent at a page."),' "Convert the ',r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">home</code>'}}),' page to React Server Components," or "move\nthis dashboard\'s formatting libraries server-side." The ',r.createElement(n.a,{href:"/linked-assets/622a61aa1ad6998352e5086c6bf2834f/rsc-performance-validation.md"},"RSC performance skill"),"\nand guides give the agent the conversion patterns."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"The agent converts the page")," on a branch, keeping the same data, routes, and visual output."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"A paired A/B benchmark proves the win")," — control (your default branch) versus experiment\n(the branch) — under production builds and mobile throttling, with a statistical test that\nsurvives a noisy laptop."),"\n"),"\n",r.createElement(n.p,null,"Step 3 is 
1what makes the loop real rather than vibes. An agent iterating in a tight loop is running\non the ",r.createElement(n.em,null,"same machine"),' it is benchmarking; conventional "run before, run after, compare the average"\nmeasurement is dominated by that noise and will happily report a phantom win or hide a real\nregression. The harness below is built to cancel that noise so the agent (and you) can trust the\nnumber after every single change.'),"\n",r.createElement(n.h2,{id:"proving-a-change-with-shakaperf",style:{position:"relative"}},r.createElement(n.a,{href:"#proving-a-change-with-shakaperf","aria-label":"proving a change with shakaperf permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Proving a change with ShakaPerf"),"\n",r.createElement(n.p,null,r.createElement(n.a,{href:"https://github.com/shakacode/shakaperf"},"ShakaPerf")," is the ShakaCode benchmarking toolkit we use to\nverify React on Rails performance work. It is source-available and public on\n",r.createElement(n.a,{href:"https://www.npmjs.com/package/shaka-perf"},"npm")," and ",r.createElement(n.a,{href:"https://github.com/shakacode/shakaperf"},"GitHub"),".\nThe methodology is what matters — any harness that runs two production builds side by side under\nidentical throttling with paired sampling and a significance test gives the same signal — but\nShakaPerf packages the whole thing, so it is the path of least resistance."),"\n",r.createElement(n.h3,{id:"twin-dockerized-servers-take-the-noise-out",style:{position:"relative"}},r.createElement(n.a,{href:"#twin-dockerized-servers-take-the-noise-out","aria-label":"twin dockerized servers take the noise out permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Twin dockerized servers take the noise out"),"\n",r.createElement(n.p,null,"ShakaPerf stands up ",r.createElement(n.strong,null,"two production-mode servers side by side")," in Docker:"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Control")," — built from your baseline branch (typically ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">main</code>'}}),"). Your reference point."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Experiment")," — built from your current branch. What you are measuring."),"\n"),"\n",r.createElement(n.p,null,"Both containers run the same data and configuration in production mode; the only difference is the\ncode on each branch (surfaced to the app as a ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PERF_EXPERIMENT</code>'}}),' environment variable). Because the\ntwo servers are real, isolated builds running at the same time, you get a genuine side-by-side\ncomparison instead of "run the old code, change branches, rebuild, hope nothing else drifted."'),"\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token comment"># In your app, after a one-time twin-servers setup:</span>\n<span class="token function">pnpm</span> <span class="token builtin class-name">exec</span> shaka-perf servers    <span class="token comment"># detect stale images → rebuild → start both containers → start servers</span>\n<span class="token comment"># Control:    http://localhost:3020</span>\n<span class="token comment"># Experiment: http://localhost:3030</span></code></pre></div>'}}),"\n",r.createElement(n.h3,{id:"paired-simultaneous-sampling--no-quiet-machine-required",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#paired-simultaneous-sampling--no-quiet-machine-required","aria-label":"paired simultaneous sampling  no quiet machine required permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Paired, simultaneous sampling — no quiet machine required"),"\n",r.createElement(n.p,null,"The reason ShakaPerf is trustworthy on a developer laptop (or a shared CI box) is that it does not\ntry to average noise away. It ",r.createElement(n.strong,null,"aligns the noise and subtracts it"),":"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,"Each iteration measures control and experiment ",r.createElement(n.strong,null,"at the same instant")," (",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Promise.all</code>'}}),"), so both\nsides experience the same CPU contention, thermal state, and background interference. Whatever\nnoise hits iteration ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">i</code>'}})," hits both samples equally."),"\n",r.createElement(n.li,null,"Analysis runs on the per-iteration ",r.createElement(n.strong,null,"paired difference")," ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">d[i] = control[i] − experiment[i]</code>'}}),",\nwhere the shared noise cancels. Significance is a ",r.createElement(n.strong,null,"Wilcoxon signed-rank test")," (with the exact\nnull distribution for small ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">n</code>'}}),", so an 8-sample run can still reach significance), and the point\nestimate is the paired ",r.createElement(n.strong,null,"Hodges-Lehmann")," estimator. Both are non-parametric and robust to the\nheavy-tailed outliers Lighthouse produces."),"\n"),"\n",r.createElement(n.p,null,"The practical payoff: you do ",r.createElement(n.strong,null,"not")," need a dedicated quiet machine or a silent CI runner. Shared\nnoise cancels inside each pair, which is exactly what lets an agent benchmark reliably on the same\nmachine it is coding on. (The full statistical justification, including why paired Wilcoxon beats\nMann-Whitney U and Welch's t-test here, is in ShakaPerf's\n",r.createElement(n.a,{href:"https://github.com/shakacode/shakaperf/blob/main/packages/shaka-perf/used_statistics.md"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">used_statistics.md</code>'}})),".)"),"\n",r.createElement(n.h3,{id:"what-it-measures",style:{position:"relative"}},r.createElement(n.a,{href:"#what-it-measures","aria-label":"what it measures permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"What it measures"),"\n",r.createElement(n.p,null,"A single Playwright-based ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">abTest</code>'}})," definition drives every check. From it, ShakaPerf collects:"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Core Web Vitals and load metrics:")," FCP, LCP, TBT, CLS, Speed Index, TTFB, and INP."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Network/byte metrics:")," total downloads and JavaScript bytes (with request counts and\nbefore-LCP variants), so a JavaScript-payload reduction shows up directly."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Custom timing marks:")," any ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">performance.mark()</code>'}})," you emit. ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">hydration-start</code>'}})," and ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">hydration-end</code>'}}),"\nare measured out of the box, which is how you see hydration cost move on an RSC conversion."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Visual regression:"),' screenshot diffs of control vs experiment across viewports, so "faster"\nis gated on "still the same page."'),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Accessibility (axe-core):")," violations, so a conversion does not silently regress a11y."),"\n"),"\n",r.createElement(n.h3,{id:"running-the-comparison",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#running-the-comparison","aria-label":"running the comparison permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Running the comparison"),"\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token comment"># Perf + visual regression across both branches:</span>\n<span class="token function">pnpm</span> <span class="token builtin class-name">exec</span> shaka-perf compare <span class="token parameter variable">--categories</span> perf,visreg <span class="token punctuation">\\</span>\n  <span class="token parameter variable">--controlURL</span> http://localhost:3020 <span class="token punctuation">\\</span>\n  <span class="token parameter variable">--experimentURL</span> http://localhost:3030 <span class="token punctuation">\\</span>\n  --full-report-zip</code></pre></div>'}}),"\n",r.createElement(n.p,null,"Results land in ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">compare-results/</code>'}})," — ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">self-contained-performance-report.html</code>'}})," (a single portable\nfile to attach to a PR), ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">full-report.html</code>'}}),", and ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">report.json</code>'}})," for machine parsing; add\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">--full-report-zip</code>'}})," to bundle everything into one archive. Key knobs live in an\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">abtests.config.ts</code>'}})," file — ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">numberOfMeasurements</code>'}})," (default 20), ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">regressionThreshold</code>'}})," (default\n50 ms), ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">pValueThreshold</code>'}})," (default 0.05), and ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">samplingMode</code>'}})," (default ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">simultaneous</code>'}}),"). See\nShakaPerf's ",r.createElement(n.a,{href:"https://github.com/shakacode/shakaperf/blob/main/packages/shaka-perf/README-twin-servers.md"},"twin-servers guide"),"\nfor the one-time Docker setup."),"\n",r.createElement(n.div,{className:"markdown-alert markdown-alert-note",dir:"auto"},"\n",r.createElement(n.p,{className:"markdown-alert-title",dir:"auto"},r.createElement(n.svg,{className:"octicon",viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true"},r.createElement(n.path,{d:"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"})),"NOTE"),"\n",r.createElement(n.p,null,"ShakaPerf is source-available under the ShakaPerf License (not MIT). It is ",r.createElement(n.strong,null,"free")," for reading\nand studying the source, a 45-day evaluation (agents welcome), education, personal projects,\nsupporting public open-source projects, and production use by small organizations (under 10\npeople ",r.createElement(n.strong,null,"and")," under $1M revenue ",r.createElement(n.strong,null,"and")," under $1M raised). Larger or funded organizations need a\npaid subscription. See ",r.createElement(n.a,{href:"https://shakaperf.com"},"shakaperf.com")," for terms and pricing. The\nmethodology on this page applies to any equivalent paired-A/B harness."),"\n"),"\n",r.createElement(n.h3,{id:"in-repo-example",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#in-repo-example","aria-label":"in repo example permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"In-repo example"),"\n",r.createElement(n.p,null,"React on Rails uses ShakaPerf as a release gate for the RSC ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:"<code class=\"language-text\">'use client'</code>"}})," CSS fix — a real, small\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">abTest</code>'}})," you can copy from. See\n",r.createElement(n.a,{href:"https://github.com/shakacode/react_on_rails/tree/main/test/shakaperf/rsc-fouc"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">test/shakaperf/rsc-fouc/</code>'}})),"\nfor the config and the ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">.abtest.ts</code>'}})," definition."),"\n",r.createElement(n.h2,{id:"the-performance-levers",style:{position:"relative"}},r.createElement(n.a,{href:"#the-performance-levers","aria-label":"the performance levers permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"The performance levers"),"\n",r.createElement(n.p,null,"Once you can prove a change, these are the levers with the largest payoff, roughly in order of\nimpact for a typical server-rendered React on Rails page. Each links to its deep dive."),"\n",r.createElement(n.h3,{id:"ssr-engine-execjs-vs-the-node-renderer",style:{position:"relative"}},r.createElement(n.a,{href:"#ssr-engine-execjs-vs-the-node-renderer","aria-label":"ssr engine execjs vs the node renderer permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"SSR engine: ExecJS vs the Node Renderer"),"\n",r.createElement(n.p,null,"The default ExecJS renderer evaluates JavaScript synchronously inside a single-threaded pool. The\n",r.createElement(n.a,{href:"/linked-assets/b6490192988c8a036f273ba296fd689e/basics.md"},"Node Renderer")," (React on Rails Pro) runs a dedicated\nNode.js master process with worker processes (default: one per CPU minus one), providing\ndramatically better throughput."),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"Metric"),r.createElement(n.th,null,"ExecJS (mini_racer)"),r.createElement(n.th,null,"ExecJS (Node.js runtime)"),r.createElement(n.th,null,"Node Renderer (Pro)"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,"Architecture"),r.createElement(n.td,null,"V8 isolate in Ruby process"),r.createElement(n.td,null,"New process per eval call"),r.createElement(n.td,null,"Persistent Node.js workers")),r.createElement(n.tr,null,r.createElement(n.td,null,"Concurrency (MRI)"),r.createElement(n.td,null,"Single-threaded (pool size 1)"),r.createElement(n.td,null,"Single-threaded (pool size 1)"),r.createElement(n.td,null,"Multi-worker")),r.createElement(n.tr,null,r.createElement(n.td,null,"Async support"),r.createElement(n.td,null,"None"),r.createElement(n.td,null,"None"),r.createElement(n.td,null,"Full (Promises, timers)")),r.createElement(n.tr,null,r.createElement(n.td,null,"Streaming SSR"),r.createElement(n.td,null,"Not supported"),r.createElement(n.td,null,"Not supported"),r.createElement(n.td,null,"Supported")),r.createElement(n.tr,null,r.createElement(n.td,null,"RSC support"),r.createElement(n.td,null,"Not supported"),r.createElement(n.td,null,"Not supported"),r.createElement(n.td,null,"Supported")),r.createElement(n.tr,null,r.createElement(n.td,null,"Relative speed"),r.createElement(n.td,null,"Baseline"),r.createElement(n.td,null,"Slower (process spawn per eval)"),r.createElement(n.td,null,"Substantially faster")))),"\n",r.createElement(n.p,null,"On MRI Ruby, ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">server_renderer_pool_size</code>'}})," must stay at 1 to avoid deadlocks (see\n",r.createElement(n.a,{href:"/linked-assets/b2801cee1881740cf9936f565c6d7f0e/execjs-limitations.md"},"ExecJS Limitations"),"); JRuby can raise it. The Node Renderer's persistent\nworkers support full async rendering and multi-worker concurrency, which are the primary sources of\nthe difference — the gap widens for pages with many async data sources or large component trees.\
1n",r.createElement(n.a,{href:"#popmenu"},"Popmenu measured a 73% response-time reduction")," in production after switching to Pro;\nthe exact multiple is workload-dependent, so benchmark your own pages rather than quoting a headline\nnumber."),"\n",r.createElement(n.h3,{id:"react-server-components-ship-less-javascript",style:{position:"relative"}},r.createElement(n.a,{href:"#react-server-components-ship-less-javascript","aria-label":"react server components ship less javascript permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"React Server Components: ship less JavaScript"),"\n",r.createElement(n.p,null,r.createElement(n.a,{href:"/linked-assets/67be9e982a0c13953bd3d4e91f6d08f8/index.md"},"React Server Components")," (Pro) exclude server components\nand their dependencies from the client bundle entirely:"),"\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="jsx"><pre class="language-jsx"><code class="language-jsx"><span class="token comment">// These imports stay server-side — zero client cost</span>\n<span class="token keyword">import</span> <span class="token punctuation">{</span> format <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">\'date-fns\'</span><span class="token punctuation">;</span> <span class="token comment">// ~30KB</span>\n<span class="token keyword">import</span> <span class="token punctuation">{</span> marked <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">\'marked\'</span><span class="token punctuation">;</span> <span class="token comment">// ~35KB</span>\n<span class="token keyword">import</span> numeral <span class="token keyword">from</span> <span class="token string">\'numeral\'</span><span class="token punctuation">;</span> <span class="token comment">// ~25KB</span></code></pre></div>'}}),"\n",r.createElement(n.p,null,"Server components produce HTML that needs ",r.createElement(n.strong,null,"no hydration")," — only client components (",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:"<code class=\"language-text\">'use client'</code>"}}),")\nhydrate — which cuts Total Blocking Time and improves Time to Interactive. Applications that lean on\nheavy formatting, parsing, or data-processing libraries on the server see the largest gains.\n",r.createElement(n.a,{href:"https://frigade.com/blog/bundle-size-reduction-with-rsc-and-frigade"},"Frigade reported a 62% reduction in client-side bundle size"),"\nafter migrating to RSC. This is the conversion the improve-and-prove loop most often targets; see the\n",r.createElement(n.a,{href:"/linked-assets/622a61aa1ad6998352e5086c6bf2834f/rsc-performance-validation.md"},"RSC Performance Validation Playbook"),"."),"\n",r.createElement(n.h3,{id:"streaming-ssr-and-selective-hydration",style:{position:"relative"}},r.createElement(n.a,{href:"#streaming-ssr-and-selective-hydration","aria-label":"streaming ssr and selective hydration permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Streaming SSR and selective hydration"),"\n",r.createElement(n.p,null,r.createElement(n.a,{href:"/linked-assets/36f649125c45c80a71092e7283f882be/streaming-server-rendering.md"},"Streaming SSR")," (Pro) uses React's\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToPipeableStream</code>'}})," to send HTML progressively as components resolve:"),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"Rendering Strategy"),r.createElement(n.th,null,"TTFB"),r.createElement(n.th,null,"Full Page Load"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,"Client-side only"),r.createElement(n.td,null,"Fast (empty shell)"),r.createElement(n.td,null,"Slow (fetch + render)")),r.createElement(n.tr,null,r.createElement(n.td,null,"Traditional SSR (",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToString</code>'}}),")"),r.createElement(n.td,null,"Slow (waits for all data)"),r.createElement(n.td,null,"Fast (complete HTML)")),r.createElement(n.tr,null,r.createElement(n.td,null,"Streaming SSR"),r.createElement(n.td,null,"Fast (shell immediately)"),r.createElement(n.td,null,"Progressive (chunks arrive)")))),"\n",r.createElement(n.p,null,'The browser gets the initial HTML 
1shell immediately (fast TTFB) while data-dependent sections stream\nin as they resolve — especially valuable for pages with multiple independent data sources. With\nstreaming and React 18+, components can hydrate independently as their JavaScript loads: navigation\ncan become interactive while main content is still streaming, and user interactions prioritize\nhydration of the clicked component, so there is no single "hydration wall."'),"\n",r.createElement(n.div,{className:"markdown-alert markdown-alert-note",dir:"auto"},"\n",r.createElement(n.p,{className:"markdown-alert-title",dir:"auto"},r.createElement(n.svg,{className:"octicon",viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true"},r.createElement(n.path,{d:"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"})),"NOTE"),"\n",r.createElement(n.p,null,"Selective hydration requires ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:async</code>'}})," script loading. In ",r.createElement(n.strong,null,"React on Rails Pro")," apps on\nShakapacker ≥ 8.2.0 this is already the default when the setting is unset — no initializer change\nis needed. Non-Pro apps default to ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:defer</code>'}})," (which delays all hydration until streaming finishes),\nand ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:async</code>'}})," is a Pro capability there. On Shakapacker < 8.2.0, script loading falls back to\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:sync</code>'}}),". See ",r.createElement(n.a,{href:"/linked-assets/f6da3f2541ea92609c5b74342ee350b0/selective-hydration-in-streamed-components.md"},"Selective Hydration in Streamed Components"),"\nand ",r.createElement(n.a,{href:"/linked-assets/dcb9e6d7071f36807a60930712b7ea7b/hydration-scheduling.md"},"Hydration Scheduling")," for control over when\nindividual islands hydrate."),"\n"),"\n",r.createElement(n.h3,{id:"caching-code-splitting-and-the-smaller-levers",style:{position:"relative"}},r.createElement(n.a,{href:"#caching-code-splitting-and-the-smaller-levers","aria-label":"caching code splitting and the smaller levers permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Caching, code splitting, and the smaller levers"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/89f8149b6e4cb4f30723a2af5ce2cc27/caching.md"},"Caching"),":")," prerender caching and fragment caching\n(",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cached_react_component_hash</code>'}}),") can skip prop assembly, JSON serialization, and JavaScript\nevaluation on a warm hit. This is often the highest-leverage change for mostly static pages — and,\nas the next section explains, it sets the fair baseline any RSC conversion must beat."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/f63a6ea5a4d60d9e6a089f3fb5262827/code-splitting.md"},"Code splitting"))," (Pro) and\n",r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/f510a0cc0d0a89ff64eeae37ca0b63da/bundle-caching.md"},"bundle caching"))," (Pro) reduce initial and rebuilt\nJavaScript. Gains scale with how independent your routes are."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/31e5254e110f8026877c55d4bd7605c9/react-compiler.md"},"React Compiler"))," adds build-time memoization with no\nsource changes."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/8f0beb8c
1d24363ce4dd117293aceca70/critical-resource-hints.md"},"Critical resource hints")),"\n(",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">preload</code>'}}),", ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">preinit</code>'}}),", ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">preconnect</code>'}}),") let React itself prioritize the LCP resource, CSS, and fonts\n— a React-native alternative to hand-rolled ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">preload_pack_asset</code>'}})," tags."),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"JSON serialization:")," see ",r.createElement(n.a,{href:"#json-serialization-performance"},"JSON serialization performance")," below."),"\n"),"\n",r.createElement(n.h2,{id:"reading-the-result-honestly",style:{position:"relative"}},r.createElement(n.a,{href:"#reading-the-result-honestly","aria-label":"reading the result honestly permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Reading the result honestly"),"\n",r.createElement(n.p,null,"A faster number is only a real win if it is the same page, measured against the right baseline, and\nyou understand ",r.createElement(n.em,null,"which")," metric moved."),"\n",r.createElement(n.h3,{id:"decompose-the-metrics--do-not-just-stare-at-lcp",style:{position:"relative"}},r.createElement(n.a,{href:"#decompose-the-metrics--do-not-just-stare-at-lcp","aria-label":"decompose the metrics  do not just stare at lcp permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Decompose the metrics — do not just stare at LCP"),"\n",r.createElement(n.p,null,"The fix depends on which signals moved together:"),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"Signal"),r.createElement(n.th,null,"Likely cause"),r.createElement(n.th,null,"Where to look"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"FCP and TBT both high")),r.createElement(n.td,null,"JS-bundle / hydration bound — the ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:"<code class=\"language-text\">'use client'</code>"}})," tail ships too much"),r.createElement(n.td,null,"Reduce client boundaries (",r.createElement(n.a,{href:"../migrating/rsc-troubleshooting.md#chunk-contamination"},"chunk contamination"),")")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"LCP high while FCP is also high")),r.createElement(n.td,null,"LCP is gated on a late FCP; the element is healthy but cannot paint yet"),r.createElement(n.td,null,"Fix FCP first by reducing client JS; LCP usually follows")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"LCP high while FCP is healthy")),r.createElement(n.td,null,"The LCP element or its asset delivery is slow"),r.createElement(n.td,null,"Inspect the hero/image resource, CDN headers, preload/fetch priority")),r.createElement(n.tr,null,r.createElement(n.td,null,r.createElement(n.strong,null,"INP high in RUM")),r.createElement(n.td,null,"Long client tasks delay input, often the same JS tail that raises TBT"),r.createElement(n.td,null,"Follow the FCP/TBT path, verify with RUM or traces")))),"\n",r.createElement(n.p,null,"A high FCP dragging LCP behind it is the common RSC-conversion pattern: the largest element is\nhealthy, it just cannot paint until a late first render lets it. Fix FCP first."),"\n",r.createElement(n.h3,{id:"benchmarking-rsc-against-warm-ssr-caches",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#benchmarking-rsc-against-warm-ssr-caches","aria-label":"benchmarking rsc against warm ssr caches permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Benchmarking RSC against warm SSR caches"),"\n",r.createElement("span",{id:"benchmarking-rsc-against-warm-ssr-caches"}),"\n",r.createElement(n.p,null,"React Server Components reduce client JavaScript, hydration work, and duplicated data-fetching\npaths. They do ",r.createElement(n.strong,null,"not")," automatically beat an already-warm SSR cache on every first-paint metric. If\nthe existing page uses ",r.createElement(n.a,{href:"../building-features/caching.md#level-2-fragment-caching"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cached_react_component</code>'}})," or ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cached_react_component_hash</code>'}})),",\nthe fair baseline is a warm fragment-cache hit. If it uses\n",r.createElement(n.a,{href:"../building-features/caching.md#level-1-prerender-caching"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.prerender_caching = true</code>'}})),"\nwithout fragment caching, the fair baseline is a warm prerender-cache hit. Do not compare RSC\nagainst an uncached SSR request unless that uncached state is the production baseline."),"\n",r.createElement(n.p,null,"This matters most for mostly static public pages. On a fragment-cache hit, React on Rails Pro can\nskip prop assembly, JSON serialization, and JavaScript evaluation. On a prerender-cache hit, props\nare still assembled and serialized, but the JavaScript render result is reused. Either warm path can\nbe very hard for an RSC conversion to beat on TTFB, FCP, or LCP because there is little\nserver-rendering work left to remove."),"\n",r.createElement(n.p,null,"A cache-optimized landing page often looks like this:"),"\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="erb"><pre class="language-erb"><code class="language-erb"><span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%</span><span class="token ruby language-ruby">\n  cache_key <span class="token operator">=</span> <span class="token punctuation">[</span>\n    <span class="token string-literal"><span class="token string">"welcome-page"</span></span><span class="token punctuation">,</span>\n    current_user<span class="token punctuation">,</span>\n    device_type_cache_key<span class="token punctuation">,</span>\n    release_cache_key<span class="token punctuation">,</span>\n    cms_last_updated_at<span class="token punctuation">,</span>\n    I18n<span class="token punctuation">.</span>locale\n  <span class="token punctuation">]</span>\n\n  rendered <span class="token operator">=</span> cached_react_component_hash<span class="token punctuation">(</span>\n    <span class="token string-literal"><span class="token string">"WelcomePage"</span></span><span class="token punctuation">,</span>\n    <span class="token symbol">cache_key</span><span class="token operator">:</span><span class="token punctuation">,</span>\n    <span class="token symbol">auto_load_bundle</span><span class="token operator">:</span> <span class="token boolean">false</span>\n  <span class="token punctuation">)</span> <span class="token keyword">do</span>\n    build_welcome_page_props\n  <span class="token keyword">end</span>\n</span><span class="token delimiter punctuation">%></span></span>\n\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%</span><span class="token ruby language-ruby"> append_javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"generated/WelcomePage"</span></span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%</span><span class="token ruby language-ruby"> append_stylesheet_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"generated/WelcomePage"</span></span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> preload_pack_asset<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"generated/WelcomePage.css"</span></span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> rendered<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"componentHtml"</span></span><span class="token punctuation">]</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",r.createElement(n.p,null,"An RSC experiment must keep the same data, release, device, locale, CMS state, CSS delivery, font\npreloads, and hero/image priority before claiming that RSC changed rendering performance. Otherwise\nthe comparison is apples to oranges: a lower JavaScript payload can coexist with worse LCP if the\nconversion delays CSS, fonts, or the LCP resource. This is exactly why the ShakaPerf harness gates\nperformance on ",r.createElement(n.strong,null,"visual regression")," in the same run — a faster page has to still be the same page."),"\n",r.createElement(n.p,null,"Measure each cache state intentionally and label it in the report:"),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"Variant"),r.createElement(n.th,null,"How to prepare it"),r.createElement(n.th,null,"What it answers"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,"Cold uncached SSR"),r.createElement(n.td,null,"Clear fragment/prerender caches; measure the first request"),r.createElement(n.td,null,"Cost of prop assembly, serialization, and JavaScript rendering before caching")),r.createElement(n.tr,null,r.createElement(n.td,null,"Warm cached SSR"),r.createElement(n.td,null,"Prime the exact cache key, then measure repeated hits; log ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RORP_CACHE_HIT</code>'}})," when available"),r.createElement(n.td,null,"Repeat-visitor steady state for the existing cached implementation")),r.createElement(n.tr,null,r.createElement(n.td,null,"RSC cold"),r.createElement(n.td,null,"Clear relevant Rails/browser/renderer caches; measure the first RSC request"),r.createElement(n.td,null,"First-hit cost of the RSC render, Flight payload generation, and asset discovery")),r.createElement(n.tr,null,r.createElement(n.td,null,"RSC warm"),r.createElement(n.td,null,"Prime the RSC route under the same data and cache policy; measure repeated navigations"),r.createElement(n.td,null,"Steady state for RSC rendering, hydration, Flight payload, and cached assets")))),"\n",r.createElement(n.p,null,"For RSC pages, also measure what moved into the HTML response. The initial Flight payload is usually\nembedded in the HTML stream rather than fetched as a separate ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/rsc_payload/*</code>'}})," resource, so you may\nsee fewer JavaScript requests while the navigation response grows. Pair JavaScript byte counts with\nHTML transfer size, Flight payload bytes, FCP/LCP, TBT, and server/renderer timing before drawing\nconclusions."),"\n",r.createElement(n.p,null,"It is valid for an RSC migration to be a win on TBT and JavaScript bytes, a wash on LCP, and still\nworth doing for maintainability or lower browser work. Say that plainly rather than framing a\nwarm-cache first-paint tie as a failure. For the full PR evidence checklist — same-machine\ncontrol/experiment, stable package stack, one variable per run, archived output — use the\n",r.createElement(n.a,{href:"/linked-assets/622a61aa1ad6998352e5086c6bf2834f/rsc-performance-validation.md"},"RSC Performance Validation Playbook"),"."),"\n",r.createElement(n.h2,{id:"real-world-results",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#real-world-results","aria-label":"real world results permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Real-world results"),"\n",r.createElement(n.div,{className:"markdown-alert markdown-alert-note",dir:"auto"},"\n",r.createElement(n.p,{className:"markdown-alert-title",dir:"auto"},r.createElement(n.svg,{className:"octicon",viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true"},r.createElement(n.path,{d:"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"})),"NOTE"),"\n",r.createElement(n.p,null,"Start with the public live demo, then the production case study. For a fully inspectable,\nside-by-side comparison of rendering strategies, use the Marketplace demo."),"\n"),"\n",r.createElement(n.h3,{id:"marketplace-demo",style:{position:"relative"}},r.createElement(n.a,{href:"#marketplace-demo","aria-label":"marketplace demo permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Marketplace demo"),"\n",r.createElement("span",{id:"public-marketplace-rsc-demo"}),"\n",r.createElement(n.p,null,"The ",r.createElement(n.a,{href:"https://rsc.reactonrails.com/"},"Marketplace demo")," is a public, inspectable React on Rails Pro +\nRSC demo showing the same page families rendered with traditional SSR, client rendering, and React\nServer Components. See the ",r.createElement(n.a,{href:"../../pro/react-server-components/index.md#live-demo-and-evidence"},"Live Demo and Evidence"),"\nsection for the canonical link inventory: performance showcase, raw Lighthouse reports, bundle-size\nbreakdowns, the ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/why-rsc</code>'}})," walkthrough, and the demo source."),"\n",r.createElement(n.h3,{id:"production-case-study-popmenu",style:{position:"relative"}},r.createElement(n.a,{href:"#production-case-study-popmenu","aria-label":"production case study popmenu permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Production case study: Popmenu"),"\n",r.createElement("span",{id:"popmenu"}),"\n",r.createElement(n.p,null,"Popmenu, a restaurant platform serving tens of millions of SSR requests daily, adopted React on\nRails Pro and reported:"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"73% decrease")," in average response times"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"20-25% lower")," Heroku hosting costs"),"\n",r.createElement(n.li,null,"Stable performance under high traffic with the Node Renderer's worker pool"),"\n"),"\n",r.createElement(n.p,null,"See the ",r.createElement(n.a,{href:"https://www.shakacode.com/recent-work/popmenu/"},"full case study"),"."),"\n",r.createElement(n.h3,{id:"earlier-directional-experiment-gumroad-style-rsc-demo",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#earlier-directional-experiment-gumroad-style-rsc-demo","aria-label":"earlier directional experiment gumroad style rsc demo permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Earlier directional experiment: Gumroad-style RSC demo"),"\n",r.createElement(n.p,null,"An earlier local A/B (April 2026) compared an Inertia-style control route against an RSC route in the\npublic ",r.createElement(n.a,{href:"https://github.com/shakacode/react-on-rails-demo-gumroad-rsc"},"Gumroad-style demo repo"),". It\nproduced a directional RSC-favored signal on navigation duration and LCP, but the run predated the\npaired-A/B methodology above — it used a single warmup, ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">n=8</code>'}}),", unrecorded environment metadata, and\nshowed a ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">responseEnd</code>'}})," p95 counter-signal — so it is not a stable baseline. It is preserved only as\nan early data point; run a fresh paired comparison with ShakaPerf for any current claim."),"\n",r.createElement(n.h2,{id:"measuring-your-own-performance",style:{position:"relative"}},r.createElement(n.a,{href:"#measuring-your-own-performance","aria-label":"measuring your own performance permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Measuring your own performance"),"\n",r.createElement(n.h3,{id:"key-metrics-to-track",style:{position:"relative"}},r.createElement(n.a,{href:"#key-metrics-to-track","aria-label":"key metrics to track permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Key metrics to track"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Time to First Byte (TTFB):")," how quickly the server begins sending HTML"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Largest Contentful Paint (LCP):")," when the main content becomes visible"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Total Blocking Time (TBT):")," time the main thread is blocked during load"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Client bundle size / JavaScript bytes:")," total JavaScript downloaded"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Server render time:")," not logged by default — measure wall-clock time in Ruby around the render\ncall; on Pro, enable ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.tracing = true</code>'}})," in ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">
1config/initializers/react_on_rails_pro.rb</code>'}})," to log\nrender timings"),"\n"),"\n",r.createElement(n.h3,{id:"tools",style:{position:"relative"}},r.createElement(n.a,{href:"#tools","aria-label":"tools permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Tools"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"https://github.com/shakacode/shakaperf"},"ShakaPerf"),":")," paired twin-server A/B for a trustworthy\nbranch-vs-branch verdict (the harness above)"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/bbc8e06d6ce4e0c72ec550468a0d8596/performance-tracks-and-profiling.md"},"Performance tracks and profiling"),":"),"\nthe React on Rails-specific profiling workflow and metric clusters"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.a,{href:"/linked-assets/924c7e4fdc7c0a51f3de6e6a3caba0b0/web-vitals-and-rum.md"},"Web Vitals and RUM"),":")," field data from real users"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Chrome DevTools Performance tab / Lighthouse:")," ad-hoc profiling and Core Web Vitals scoring"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">webpack-bundle-analyzer</code>'}}),":")," visualize bundle composition and large dependencies"),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Rails server logs:")," server-side console messages replayed to ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rails.logger</code>'}})," when\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.logging_on_server = true</code>'}})),"\n",r.createElement(n.li,null,r.createElement(n.strong,null,"Node Renderer logs:")," lifecycle and error detail via ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">RENDERER_LOG_LEVEL</code>'}})," (Pro)"),"\n"),"\n",r.createElement(n.h2,{id:"json-serialization-performance",style:{position:"relative"}},r.createElement(n.a,{href:"#json-serialization-performance","aria-label":"json serialization performance permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"JSON serialization performance"),"\n",r.createElement(n.p,null,"React on Rails serializes component props to JSON before passing them to the JavaScript renderer.\nThe speed of that serialization depends on which version of Ruby's ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">json</code>'}})," gem you have."),"\n",r.createElement(n.p,null,"Ruby's bundled ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">json</code>'}})," gem (2.5–2.7, as shipped with Ruby 3.3) is slower than it needs to be. Version\n2.8.0 introduced a performance rewrite that makes it roughly ",r.createElement(n.strong,null,"2x faster")," for large payloads:"),"\n",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby">gem <span class="token string-literal"><span class="token string">\'json\'</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">\'>= 2.8\'</span></span></code></pre></div>'}}),"\n",r.createElement(n.p,null,"For a ~3 MB props payload on Ruby 3.3:"),"\n",r.createElement(n.table,null,r.createElement(n.thead,null,r.createElement(n.tr,null,r.createElement(n.th,null,"JSON Gem Version"),r.createElement(n.th,null,"Serialization Time"),r.createElement(n.th,null,"Improvement"))),r.createElement(n.tbody,null,r.createElement(n.tr,null,r.createElement(n.td,null,"2.7.2 (bundled)"),r.createElement(n.td,null,"21.5 ms"),r.createElement(n.td,null,"Baseline")),r.createElement(n.tr,null,r.createElement(n.td,null,"2.19.8 (upgraded)"),r.createElement(n.td,null,"10.2 ms"),r.createElement(n.td,null,r.createElement(n.strong,null,"2.1x faster"))))),"\n",r.createElement(n.p,null,"The improvement scales with payload size — roughly ",r.createElement(n.strong,null,"3-4 ms saved per MB")," of props. It is most\nimpactful for large component trees, data-heavy pages (dashboards, tables, lists), and traditional\nSSR where props are serialized before rendering. For typical pages with small props (under 100 KB),\nthe difference is negligible (< 1 ms)."),"\n",r.createElement(n.p,null,"React on Rails v17 requires Ruby ≥ 3.3, whose bundled ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">json</code>'}})," predates 2.8; on Ruby 3.4+ the bundled\ngem is already 2.9+, so the explicit upgrade is unnecessary. The upgraded ",r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">json</code>'}})," gem supports all\nRuby versions React on Rails supports."),"\n",r.createElement(n.h2,{id:"related-documentation",style:{position:"relative"}}
1,r.createElement(n.a,{href:"#related-documentation","aria-label":"related documentation permalink",className:"anchor before"},r.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Related documentation"),"\n",r.createElement(n.ul,null,"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/622a61aa1ad6998352e5086c6bf2834f/rsc-performance-validation.md"},"RSC Performance Validation Playbook")," — the full PR evidence workflow"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/bbc8e06d6ce4e0c72ec550468a0d8596/performance-tracks-and-profiling.md"},"Performance Tracks and Profiling")," — profiling workflow"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/924c7e4fdc7c0a51f3de6e6a3caba0b0/web-vitals-and-rum.md"},"Web Vitals and RUM")," — field data from real users"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/89f8149b6e4cb4f30723a2af5ce2cc27/caching.md"},"Caching")," — prerender and fragment caching (the warm-cache baseline)"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/dcb9e6d7071f36807a60930712b7ea7b/hydration-scheduling.md"},"Hydration Scheduling")," — control when islands hydrate"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/36f649125c45c80a71092e7283f882be/streaming-server-rendering.md"},"Streaming Server Rendering")," — setup and best practices"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/f63a6ea5a4d60d9e6a089f3fb5262827/code-splitting.md"},"Code Splitting")," and ",r.createElement(n.a,{href:"/linked-assets/f510a0cc0d0a89ff64eeae37ca0b63da/bundle-caching.md"},"Bundle Caching")," — JavaScript reduction (Pro)"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/31e5254e110f8026877c55d4bd7605c9/react-compiler.md"},"React Compiler")," — build-time memoization"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/b6490192988c8a036f273ba296fd689e/basics.md"},"Node Renderer Basics")," — Pro Node.js renderer setup"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/b2801cee1881740cf9936f565c6d7f0e/execjs-limitations.md"},"ExecJS Limitations")," — constraints of the default rendering engine"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/0f5411ae9a04472945df8ca7fd255e66/oss-vs-pro.md"},"OSS vs Pro")," — feature comparison"),"\n",r.createElement(n.li,null,r.createElement(n.a,{href:"/linked-assets/67be9e982a0c13953bd3d4e91f6d08f8/index.md"},"React Server Components")," — RSC overview and guides"),"\n"))}var s=function(e){void 0===e&&(e={});const{wrapper:n}=Object.assign({},(0,a.R)(),e.components);return n?r.createElement(n,e,r.createElement(l,e)):l(e)},c=t(63679),o=t(95197),i=t(47388),d=t(23690),h=t(14487);const m=c.default.p.withConfig({displayName:"doc-article__RedirectFallback",componentId:"sc-tic2w5-0"})(["padding:2rem;"]),p=e=>{let{data:n,location:t,children:a}=e;const{headings:l}=n.article.childMdx,s=(0,o.X9)(t.pathname),[c,d]=r.useState(null),p=t.pathname.split("/"),u=p.indexOf("docs"),g=-1===u?t.pathname.length:p.slice(0,u+1).join("/").length,E=t.pathname.slice(0,g),f=t.pathname.slice(g+1).replace(/\/$/,"").split("/").filter((e=>""!==e)),v=f.length>1?f.slice(0,-1).join("/"):"";r.useEffect((()=>{if(!s)return;const{hash:e}=window.location;d(e);const n=""+s+e;window.location.href!==n&&window.location.replace(n)}),[s]);const y=null===s||null===c?null:""+s+c;return r.createElement(r.Fragment,null,null!==y?r.createElement(m,{role:"status","aria-live":"polite"},"Redirecting..."," ",r.createElement("a",{href:y,target:"_blank",rel:"noopener noreferrer"},"Click here if not redirected.")):r.createElement(i.A,{main:r.createElement(h.A,{body:a,headings:l,transformUrl:e=>{if(e.startsWith("http")||e.startsWith("#"))return e;const n=e.replace(/\.md$/,"/");
1if(n.startsWith("./"))return E+(""===v?"":"/"+v)+n.slice(1);if(n.startsWith("../")){if(""===v)return""+E+n.slice(2);const e=v.split("/").slice(0,-1).join("/");return E+(""===e?"":"/"+e)+n.slice(2)}return E+"/"+(""===v?"":v+"/")+n}})}))};function u(e){return r.createElement(p,e,r.createElement(s,e))}const g=e=>{let{data:n,location:t,pageContext:a}=e;const{headings:l}=n.article.childMdx,{siteUrl:s}=n.site.siteMetadata,{repoDisplayName:c}=a,i=l.find((e=>1===e.depth)),h=(0,o.X9)(t.pathname)||""+s.replace(/\/+$/,"")+t.pathname;return r.createElement(d.A,{title:i?i.value:c,description:i?i.value:c+" documentation",isArticle:!0,canonical:h,pathname:t.pathname})}}}]);
2//# sourceMappingURL=component---src-template-doc-article-tsx-content-file-path-cache-gatsby-source-git-react-on-rails-docs-oss-core-concepts-performance-benchmarks-md-62aeae342e68d54998e7.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.