PageSourceSearch

https://shakacode.com/ae81b3cc-4a04abaf4288922d8b72.js

js shakacode.com collected 2026-10-04 00:26:22 UTC 196,618 bytes, 2 lines download raw bytes

1"use strict";(self.webpackChunkgatsby_starter_default=self.webpackChunkgatsby_starter_default||[]).push([[1751],{78183:function(e,n,a){var s=a(28453),t=a(96540);function l(e){const n=Object.assign({h1:"h1",a:"a",span:"span",p:"p",blockquote:"blockquote",strong:"strong",h2:"h2",ul:"ul",li:"li",em:"em",ol:"ol",h3:"h3",table:"table",thead:"thead",tr:"tr",th:"th",tbody:"tbody",td:"td",div:"div",svg:"svg",path:"path",h4:"h4"},(0,s.R)(),e.components);return t.createElement(t.Fragment,null,t.createElement(n.h1,{id:"streaming-server-side-rendering",style:{position:"relative"}},t.createElement(n.a,{href:"#streaming-server-side-rendering","aria-label":"streaming server side rendering permalink",className:"anchor before"},t.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 Server-Side Rendering"),"\n",t.createElement(n.p,null,"React on Rails Pro supports streaming server rendering using React 18/19's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToPipeableStream</code>'}})," API. Instead of waiting for the entire page to render before sending any HTML, streaming SSR sends HTML to the browser progressively as each part of the page becomes ready."),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Route map"),": Start at ",t.createElement(n.a,{href:"/linked-assets/5f3eacdd52b97cebdce257aa7836a6e1/react-on-rails-pro.md"},"React on Rails Pro")," if you're choosing a path. This page is the canonical streaming SSR overview; for the technical implementation guide, see ",t.createElement(n.a,{href:"/linked-assets/36f649125c45c80a71092e7283f882be/streaming-server-rendering.md"},"Streaming Server Rendering"),"."),"\n"),"\n",t.createElement(n.h2,{id:"why-streaming-ssr",style:{position:"relative"}},t.createElement(n.a,{href:"#why-streaming-ssr","aria-label":"why streaming ssr permalink",className:"anchor before"},t.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>'}})),"Why Streaming SSR?"),"\n",t.createElement(n.p,null,"Traditional SSR renders the full page on the server, then sends the complete HTML in one response. This means the user sees nothing until the slowest component finishes rendering. Streaming SSR changes this:"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Faster Time to First Byte (TTFB)")," — The browser receives the page shell immediately"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Progressive rendering")," — Content appears as it becomes ready, not all at once"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Suspense integration")," — React's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundaries define which parts can stream independently"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Selective Hydration")," — Components become interactive as soon as their JavaScript loads, even while other parts are still streaming"),"\n"),"\n",t.createElement(n.p,null,"With traditional SSR the first paint waits for the slowest query; with streaming the shell paints first and each slow section streams in afterward:"),"\n",t.createElement("p",null,t.createElement("img",{src:"images/traditional-vs-streaming.svg",alt:"Two side-by-side timelines on the same scale. With traditional SSR the first paint happens only after the slowest data fetch and the HTML render finish (about 1000 ms). With streaming SSR plus async props the shell paints almost immediately (about 50 ms) and each slow section streams in afterward.",width:"840"})),"\n",t.createElement(n.p,null,t.createElement(n.em,null,"Timings are illustrative, not benchmarks — the point is ",t.createElement(n.strong,null,"when")," the first paint happens: only after all data is ready for traditional SSR, but right after the shell for streaming.")),"\n",t.createElement(n.h2,{id:"how-it-works",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#how-it-works","aria-label":"how it works permalink",className:"anchor before"},t.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>'}})),"How It Works"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,"Rails starts the response immediately, sending the HTML shell (layout, static content, loading placeholders)"),"\n",t.createElement(n.li,null,"The Node Renderer uses ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToPipeableStream</code>'}})," to render React components"),"\n",t.createElement(n.li,null,"As each ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundary resolves (e.g., an async data fetch completes), the rendered HTML chunk is streamed to the browser"),"\n",t.createElement(n.li,null,"The browser replaces placeholders with real content — no full-page reload needed"),"\n"),"\n",t.createElement("p",null,t.createElement("img",{src:"images/streaming-sequence.svg",alt:"Sequence diagram across three lanes — Browser, Rails, and the Node Renderer. The browser asks Rails for a page, Rails asks the renderer to start, the renderer streams the first HTML piece back through Rails so the browser can paint right away, then a loop streams each further HTML piece as more of the page becomes ready.",width:"840"})),"\n",t.createElement(n.h2,{id:"prerequisites",style:{position:"relative"}},t.createElement(n.a,{href:"#prerequisites","aria-label":"prerequisites permalink",className:"anchor before"},t.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>'}})),"Prerequisites"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"React on Rails Pro"),"\n",t.createElement(n.li,null,"React 18 or 19 for progressive ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}}),"; React 16/17 use synchronous SSR fallback"),"\n",t.createElement(n.li,null,"React 19.2.x with patch 19.2.7 or newer for async props and React Server Components"),"\n",t.createElement(n.li,null,"React on Rails v16.0.0 or higher"),"\n",t.createElement(n.li,null,"Node Renderer running (streaming requires Node.js, not ExecJS)"),"\n",t.createElement(n.li,null,"For async props (streaming each slow prop independently): React Server Components enabled — ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.enable_rsc_support = true</code>'}})),"\n"),"\n",t.createElement(n.h3,{id:"compatibility-by-react-version",style:{position:"relative"}},t.createElement(n.a,{href:"#compatibility-by-react-version","aria-label":"compatibility by react version permalink",className:"anchor before"},t.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>'}})),"Compatibility by React Version"),"\n",t.createElement(n.table,null,t.createElement(n.thead,null,t.createElement(n.tr,null,t.createElement(n.th,null,"React version"),t.createElement(n.th,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}})," behavior"),t.createElement(n.th,null,"Async props and RSC"))),t.createElement(n.tbody,null,t.createElement(n.tr,null,t.createElement(n.td,null,"React 16/17"),t.createElement(n.td,null,"Synchronous SSR fallback for trees that do not suspend. If a child actually suspends, ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToString</code>'}})," cannot complete SSR; the React 18 example below will not work unchanged."),t.createElement(n.td,null,"Not available")),t.createElement(n.tr,null,t.createElement(n.td,null,"React 18"),t.createElement(n.td,null,"Progressive streaming for ordinary, non-RSC components with synchronous props. Suspense boundaries can reveal independently through ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToPipeableStream</code>'}}),"."),t.createElement(n.td,null,"Not available; do not install ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails-rsc</code>'}})," or enable RSC support")),t.createElement(n.tr,null,t.createElement(n.td,null,"React 19"),t.createElement(n.td,null,"Progressive streaming for ordinary components, including the same non-RSC path as React 18."),t.createElement(n.td,null,"Async props and RSC require React/React DOM 19.2.x with patch 19.2.7+ and compatible ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails-rsc</code>'}})," 19.2.x")))),"\n",t.createElement(n.p,null,"React on Rails detects whether the installed React DOM server provides ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToPipeableStream</code>'}}),". This keeps\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}})," on the synchronous path for React 16 and 17 while selecting progressive streaming\nautomatically on React 18 and 19. The React 16/17 path is suitable only when the rendered tree does not suspend."),"\n",t.createElement(n.h3,{id:"react-192-suspense-reveal-batching",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#react-192-suspense-reveal-batching","aria-label":"react 192 suspense reveal batching permalink",className:"anchor before"},t.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 19.2 Suspense reveal batching"),"\n",t.createElement(n.p,null,t.createElement(n.a,{href:"https://react.dev/blog/2025/10/01/react-19-2#batching-suspense-boundaries-for-ssr"},"React 19.2 briefly batches some server-rendered Suspense boundary reveals"),"\nso boundaries that finish near one another can become visible together. React controls this with heuristics and backs\noff when batching could hurt Core Web Vitals. React on Rails Pro does not add or expose a batching-delay setting."),"\n",t.createElement(n.p,null,"Keep these two checkpoints separate when observing a streamed page:"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Server chunk delivery:")," React decides when resolved boundary markup enters its server stream. React on Rails Pro\nforwards those emitted chunks in order. For RSC streams, each combined flush preserves the required order: payload\ninitialization and stylesheet prerequisites, then the HTML, then the matching payload-push scripts."),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Browser-visible reveal:")," Network buffering, compression, HTML parsing, and React's reveal batching can make DOM\nupdates appear later or in larger groups than the chunks observed by Rails or the Node Renderer. A server chunk is\ntherefore not a guaranteed paint boundary."),"\n"),"\n",t.createElement(n.p,null,"Make streaming tests state-based rather than timer-based. Assert the shell and fallbacks while data is unresolved,\nthen resolve one or more sources and use retrying assertions for the final DOM state, element order, and the rule that\nresolved content does not return to its fallback. Do not assume one resolved boundary equals one output chunk, use an\narbitrary sleep, or depend on a fixed batching delay such as 300 ms; React's timing is deliberately heuristic."),"\n",t.createElement(n.h2,{id:"react-18-streaming-without-rsc",style:{position:"relative"}},t.createElement(n.a,{href:"#react-18-streaming-without-rsc","aria-label":"react 18 streaming without rsc permalink",className:"anchor before"},t.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 18 Streaming Without RSC"),"\n",t.createElement(n.p,null,"React 18 applications can use progressive SSR before adopting React Server Components. Keep all Rails props\nsynchronous, register an ordinary React component, and render it with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}}),". You do not need\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails-rsc</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.enable_rsc_support</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">registerServerComponent</code>'}}),", or an RSC bundle."),"\n",t.createElement(n.p,null,"For a Suspense boundary to reveal progressively, something below it must suspend during the server render. One\nstraightforward React 18 case is a lazily loaded component:"),"\n",t.createElement(n.p,null,":::note Node Renderer module support\nThis example assumes the server bundle can resolve its dynamic import. If your bundler's lazy or loadable component\nruntime needs Node.js globals, enable ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">supportModules: true</code>'}})," in the Node Renderer launcher. Because that option changes\nthe renderer VM's runtime capabilities, review the\n",t.createElement(n.a,{href:"../oss/building-features/node-renderer/js-configuration.md#node-renderer-javascript-configuration"},"supportModules configuration and security notes"),"\nbefore enabling it.\n:::"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="jsx"><pre class="language-jsx"><code class="language-jsx"><span class="token comment">// app/javascript/components/ProductPage.jsx</span>\n<span class="token keyword">
1import</span> React<span class="token punctuation">,</span> <span class="token punctuation">{</span> lazy<span class="token punctuation">,</span> Suspense <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">\'react\'</span><span class="token punctuation">;</span>\n\n<span class="token keyword">const</span> ProductReviews <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">\'./ProductReviews\'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>\n\n<span class="token keyword">const</span> <span class="token function-variable function">ProductPage</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> product<span class="token punctuation">,</span> reviews <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>product<span class="token punctuation">.</span>name<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>product<span class="token punctuation">.</span>description<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><span class="token plain-text">\n\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><span class="token plain-text">Loading reviews...</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">ProductReviews</span></span> <span class="token attr-name">reviews</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>reviews<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span>\n<span class="token punctuation">)</span><span class="token punctuation">;</span>\n\n<span class="token keyword">export</span> <span class="token keyword">default</span>
1 ProductPage<span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The lazy module itself remains an ordinary synchronous React component:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="jsx"><pre class="language-jsx"><code class="language-jsx"><span class="token comment">// app/javascript/components/ProductReviews.jsx</span>\n<span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">\'react\'</span><span class="token punctuation">;</span>\n\n<span class="token keyword">const</span> <span class="token function-variable function">ProductReviews</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> reviews <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token punctuation">{</span>reviews<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">review</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span>\n      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">key</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>review<span class="token punctuation">.</span>id<span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token punctuation">{</span>review<span class="token punctuation">.</span>summary<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>\n    <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span>\n<span class="token punctuation">)</span><span class="token punctuation">;</span>\n\n<span class="token keyword">export</span> <span class="token keyword">default</span> ProductReviews<span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Register it through the normal, non-RSC API. With manual bundling, import this shared registration module from both\nthe browser and server bundle entrypoints so the browser can hydrate the streamed component:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js"><span class="token comment">// app/javascript/packs/react-on-rails-components.js</span>\n<span class="token keyword">import</span> ReactOnRails <span class="token keyword">from</span> <span class="token string">\'react-on-rails-pro\'</span><span class="token punctuation">;</span>\n<span class="token keyword">import</span> ProductPage <span class="token keyword">from</span> <span class="token string">\'../components/ProductPage\'</span><span class="token punctuation">;</span>\n\nReactOnRails<span class="token punctuation">.</span><span class="token function">register</span><span class="token punctuation">(</span><span class="token punctuation">{</span> ProductPage <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js"><span class="token comment">// app/javascript/packs/application.js</span>\n<span class="token keyword">
1import</span> <span class="token string">\'./react-on-rails-components\'</span><span class="token punctuation">;</span>\n\n<span class="token comment">// app/javascript/packs/server-bundle.js</span>\n<span class="token keyword">import</span> <span class="token string">\'./react-on-rails-components\'</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"If you use auto-bundling, its generated client and server packs perform the equivalent ordinary component\nregistration."),"\n",t.createElement(n.p,null,"Pass the already-loaded Rails data as ordinary props:"),"\n",t.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"> stream_react_component<span class="token punctuation">(</span>\n      <span class="token string-literal"><span class="token string">\'ProductPage\'</span></span><span class="token punctuation">,</span>\n      <span class="token symbol">props</span><span class="token operator">:</span> <span class="token punctuation">{</span>\n        <span class="token symbol">product</span><span class="token operator">:</span> <span class="token variable">@product</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">%i[id name description]</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span>\n        <span class="token symbol">reviews</span><span class="token operator">:</span> <span class="token variable">@reviews</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">%i[id summary]</span></span><span class="token punctuation">)</span>\n      <span class="token punctuation">}</span>\n    <span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Render the containing view through the streaming controller concern:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">ProductsController</span> <span class="token operator">&lt;</span> ApplicationController\n  <span class="token keyword">include</span> ActionController<span class="token double-colon punctuation">::</span>Live\n  <span class="token keyword">include</span> ReactOnRails<span class="token double-colon punctuation">::</span>Controller\n  <span class="token keyword">include</span> ReactOnRailsPro<span class="token double-colon punctuation">::</span>Stream\n\n  <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span>\n    <span class="token variable">@product</span> <span class="token operator">=</span> Product<span class="token punctuation">.</span>find<span class="token punctuation">(</span>params<span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">]</span><span class="token punctuation">)</span>\n    <span class="token variable">@reviews</span> <span class="token operator">=</span> <span class="token variable">@product</span><span class="token punctuation">.</span>reviews\n    stream_view_containing_react_components<span class="token punctuation">(</span><span class="token symbol">template</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"products/show"</span></span><span class="token punctuation">)</span>\n  <span class="token keyword">end</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The browser receives the product shell and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Loading reviews...</code>'}})," fallback first. When the lazy module resolves on the\nserver, React streams the reviews HTML and replaces that fallback. E
1xisting React 18 data libraries that suspend during\nSSR can use the same boundary pattern."),"\n",t.createElement(n.p,null,"This path provides progressive ",t.createElement(n.strong,null,"HTML streaming"),", but all Rails props are still loaded before React begins rendering.\nTo stream Rails data as each slow query finishes, use ",t.createElement(n.a,{href:"#progressive-data-with-async-props"},"async props"),", which require\nReact/React DOM 19.2.x with patch 19.2.7 or newer and RSC."),"\n",t.createElement(n.h2,{id:"progressive-data-with-async-props",style:{position:"relative"}},t.createElement(n.a,{href:"#progressive-data-with-async-props","aria-label":"progressive data with async props permalink",className:"anchor before"},t.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>'}})),"Progressive Data with Async Props"),"\n",t.createElement(n.p,null,"Streaming SSR sends HTML as React renders it. ",t.createElement(n.strong,null,"Async props")," (a React Server Components feature, so it requires ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">enable_rsc_support</code>'}}),") go one step further: Rails emits each prop ",t.createElement(n.em,null,"as its data becomes ready")," and forwards the matching Suspense boundary to the browser the moment it resolves."),"\n",t.createElement(n.p,null,'This is the recommended answer to "my component needs Rails data during render": ',t.createElement(n.strong,null,"Rails owns the data and supplies it as an async prop"),", preserving your controller / model / authorization / caching layers. Rails can push known props as work finishes, or React can pull named props on demand; the renderer never needs a separate Rails application API request."),"\n",t.createElement(n.div,{className:"markdown-alert markdown-alert-note",dir:"auto"},"\n",t.createElement(n.p,{className:"markdown-alert-title",dir:"auto"},t.createElement(n.svg,{className:"octicon",viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true"},t.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",t.createElement(n.p,null,"A Server Component ",t.createElement(n.em,null,"can")," ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">fetch</code>'}})," a Rails API directly, but the renderer's VM has no ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">fetch</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Headers</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Request</code>'}}),", or ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Response</code>'}})," by default — you must bundle an HTTP client or inject them via ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">additionalContext</code>'}}),". That call leaves the active Rails controller request and must re-establish or explicitly forward auth, tenancy, caching, and tracing context through the API endpoint. ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:"<code class=\"language-text\">'use server'</code>"}})," Server Actions are ",t.createElement(n.strong,null,"not")," supported (the renderer has no DB/session/cookie access). Prefer props / async props. See ",t.createElement(n.a,{href:"/linked-assets/ac508aaab32d6164befba4ceae1e5634/rsc-data-fetching.md"},"RSC data fetching patterns"),"."),"\n"),"\n",t.createElement(n.p,null,"Under the hood, Rails opens a bidirectional NDJSON stream to the renderer over h2c by default, or direct HTTP/1.1 when both sides are configured for it, and feeds props in as it resolves them. Eac
1h update runs in that request's isolated ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sharedExecutionContext</code>'}})," and resolves a Promise, which lets React flush the corresponding HTML back to Rails for forwarding to the browser:"),"\n",t.createElement("p",null,t.createElement("img",{src:"images/async-props-sequence.svg",alt:"Sequence diagram across three lanes — Browser, Rails, and the Node Renderer. The browser asks for a page, Rails starts the page with placeholders, the renderer returns an HTML shell with loading states, and the browser shows the shell. Then a loop runs for each slow data value: Rails sends the value, the renderer returns the HTML for that section, and the browser replaces the loading state.",width:"840"})),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.prerender_caching</code>'}})," does not cache async-props streams. React on Rails Pro bypasses prerender caching for\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component_with_async_props</code>'}})," because the async output can depend on request-specific data outside the\nprerender cache key. Use explicit fragment cache keys only for async-props content that is safe to reuse across\nrequests."),"\n",t.createElement(n.p,null,"The view helper is ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component_with_async_props</code>'}}),", which yields an emitter:"),"\n",t.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"> stream_react_component_with_async_props<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"Dashboard"</span></span><span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>emit<span class="token operator">|</span>\n      <span class="token comment"># Sequential: both values are emitted asynchronously, one after the other.</span>\n      <span class="token comment"># Put fast values in `props:`; use emit for values you want to stream.</span>\n      emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"users"</span></span><span class="token punctuation">,</span> User<span class="token punctuation">.</span>active<span class="token punctuation">.</span>limit<span class="token punctuation">(</span><span class="token number">50</span><span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">,</span> <span class="token symbol">:name</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>\n      emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"posts"</span></span><span class="token punctuation">,</span> Post<span class="token punctuation">.</span>recent<span class="token punctuation">.</span>limit<span class="token punctuation">(</span><span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">,</span> <span class="token symbol">:title</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>\n    <span class="token keyword">end</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"When several slow sources are independent, use the ",t.createElement(n.a,{href:"#loading-multiple-slow-sources-in-parallel"},"parallel fan-out pattern below")," so one query does not block the next."),"\n",t.createElement(n.h3,{id:"push-known-props-or-pull-them-on-demand",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#push-known-props-or-pull-them-on-demand","aria-label":"push known props or pull them on demand permalink",className:"anchor before"},t.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>'}})),"Push known props or pull them on demand"),"\n",t.createElement(n.p,null,"The default async-props flow is ",t.createElement(n.strong,null,"push mode"),": the block starts its work and calls ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.call</code>'}})," for every async prop it\nplans to provide. This is best when the page always needs those values and Rails can start them immediately."),"\n",t.createElement(n.div,{className:"markdown-alert markdown-alert-note",dir:"auto"},"\n",t.createElement(n.p,{className:"markdown-alert-title",dir:"auto"},t.createElement(n.svg,{className:"octicon",viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true"},t.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",t.createElement(n.p,null,"Pull and mixed async-props modes (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">push_props:</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.pull_requests</code>'}}),") require React on Rails and React on Rails\nPro 17.0.0 or newer. React on Rails 16 async-props releases support push mode only."),"\n"),"\n",t.createElement(n.p,null,"Set ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">push_props:</code>'}})," to enable bidirectional ",t.createElement(n.strong,null,"pull mode"),". The block should emit names listed in ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">push_props:</code>'}})," eagerly;\nthe renderer will not request those names. Any other name is queued in ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.pull_requests</code>'}})," only when the rendered\nReact tree calls ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">getReactOnRailsAsyncProp</code>'}})," for it. An empty list enables pure pull mode:"),"\n",t.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"> stream_react_component_with_async_props<span class="token punctuation">(</span>\n      <span class="token string-literal"><span class="token string">"Dashboard"</span></span><span class="token punctuation">,</span>\n      <span class="token symbol">props</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token symbol">title</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"Dashboard"</span></span> <span class="token punctuation">}</span><span class="token punctuation">,</span>\n      <span class="token symbol">push_props</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>\n    <span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>emit<span class="token operator">|</span>\n  <span class="token keyword">while</span> <span class="token punctuation">(</span>
1prop_name <span class="token operator">=</span> emit<span class="token punctuation">.</span>pull_requests<span class="token punctuation">.</span>dequeue<span class="token punctuation">)</span>\n    <span class="token keyword">case</span> prop_name\n    <span class="token keyword">when</span> <span class="token string-literal"><span class="token string">"recommendations"</span></span>\n      emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span>prop_name<span class="token punctuation">,</span> RecommendationsQuery<span class="token punctuation">.</span>call<span class="token punctuation">(</span>current_user<span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">)</span>\n    <span class="token keyword">when</span> <span class="token string-literal"><span class="token string">"recent_orders"</span></span>\n      emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span>prop_name<span class="token punctuation">,</span> Order<span class="token punctuation">.</span>recent_for<span class="token punctuation">(</span>current_user<span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">)</span>\n    <span class="token keyword">else</span>\n      emit<span class="token punctuation">.</span>reject<span class="token punctuation">(</span>prop_name<span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"Unknown async prop"</span></span><span class="token punctuation">)</span>\n    <span class="token keyword">end</span>\n  <span class="token keyword">end</span>\n<span class="token keyword">end</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Pull mode is useful when synchronous props determine which branch renders and some Rails queries may never be needed.\nThe pull queue closes when the React render completes. Always allowlist prop names and reject unknown requests; do not\nturn a prop name into an arbitrary constant, method call, or SQL fragment."),"\n",t.createElement(n.p,null,"For the full data-fetching guidance — synchronous props, parallelizing independent queries, and React Query / SWR interop — see ",t.createElement(n.a,{href:"/linked-assets/ac508aaab32d6164befba4ceae1e5634/rsc-data-fetching.md"},"RSC data fetching patterns"),"."),"\n",t.createElement(n.h3,{id:"the-discouraged-alternative-direct-fetch-from-the-renderer",style:{position:"relative"}},t.createElement(n.a,{href:"#the-discouraged-alternative-direct-fetch-from-the-renderer","aria-label":"the discouraged alternative direct fetch from the renderer permalink",className:"anchor before"},t.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 discouraged alternative: direct ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">fetch</code>'}})," from the renderer"),"\n",t.createElement(n.p,null,"For contrast, a Server Component ",t.createElement(n.em,null,"can")," reach Rails by calling ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">fetch</code>'}})," itself. This is a plain ",t.createElement(n.strong,null,"network round-trip")," — the renderer's VM has no in-process access to Rails models, sessions, or cookies — and it gives up what async props provide for free, so prefer async props for Rails-owned data:"),"\n",t.createElement("p",null,t.createElement("img",{src:"images/discouraged-direct-fetch.svg",alt:"Sequence diagram across three lanes — Node Renderer, Rails API, and DB / Models. A warning banner notes this is discouraged. The renderer asks the Rails API for data, Rails loads and authorizes it from the database, returns the data to Rails, Rails returns JSON to the renderer, and the renderer continues rendering. Prefer async props so the active Rails page request keeps its existing auth and caching context.",width:"840"})),"\n",t.createElement(n.p,null,"Caveats: ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">fetch</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Headers</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Request</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Response</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">AbortController</code>'}}),", and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">AbortSignal</code>'}})," are ",t.createElement(n.strong,null,"not")," in the VM by default (bundle an HTTP client or inject them via ",t.createElement(n.a,{href:"../oss/building-features/node-renderer/js-configuration.md#runtime-globals-for-ssr-and-rsc"},"runtime globals"),"); cookies, auth, session, and CSRF are ",t.createElement(n.strong,null,"not")," forwarded automatically; and the API call must re-establish or explicitly forward the authorization, tenancy, caching, and tracing context already available in the active Rails page request."),"\n",t.createElement(n.h2,{id:"implementation-steps",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#implementation-steps","aria-label":"implementation steps permalink",className:"anchor before"},t.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>'}})),"Implementation Steps"),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"This example uses async props"),", which build on React Server Components. Enable RSC before you start: set ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.enable_rsc_support = true</code>'}})," in your React on Rails Pro configuration (see the ",t.createElement(n.a,{href:"/linked-assets/37d1c89f8266fd0f03818d68f8999302/tutorial.md"},"RSC tutorial"),"). Without it, the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async function</code>'}})," server component won't render and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component_with_async_props</code>'}})," raises ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRailsPro::Error</code>'}}),". If all your data is fast and you don't need progressive streaming, you can skip RSC and use the synchronous ",t.createElement(n.a,{href:"../oss/migrating/rsc-data-fetching.md#data-fetching-in-react-on-rails-pro"},t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}}))," with all props passed at once (no ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">enable_rsc_support</code>'}})," required)."),"\n"),"\n",t.createElement(n.h3,{id:"1-use-react-192",style:{position:"relative"}},t.createElement(n.a,{href:"#1-use-react-192","aria-label":"1 use react 192 permalink",className:"anchor before"},t.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>'}})),"1. Use React 19.2"),"\n",t.createElement(n.p,null,"Ensure you're using the coordinated React 19.2.x line in your ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">package.json</code>'}}),":"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">{</span>\n  <span class="token property">"react"</span><span class="token operator">:</span> <span class="token string">"19.2.7"</span><span class="token punctuation">,</span>\n  <span class="token property">"react-dom"</span><span class="token operator">:</span> <span class="token string">"19.2.7"</span>\n<span class="token punctuation">}</span></code></pre></div>'}}),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,"Note: React on Rails Pro 17 RSC requires React/React DOM 19.2.x with patch ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">>= 19.2.7</code>'}}),". Keep these versions coordinated with stable ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails-rsc</code>'}})," 19.2.x with patch ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">>= 19.2.1</code>'}}),"."),"\n"),"\n",t.createElement(n.h3,{id:"2-prepare-your-react-components",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#2-prepare-your-react-components","aria-label":"2 prepare your react components permalink",className:"anchor before"},t.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>'}})),"2. Prepare Your React Components"),"\n",t.createElement(n.p,null,"For a Suspense boundary to actually stream — show a fallback first, then swap in real content — something inside it must suspend on a Promise. In React on Rails, that data still comes from Rails: with ",t.createElement(n.strong,null,"async props"),", Rails sends the fast props immediately and emits each slow prop as it resolves. React on Rails Pro injects a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">getReactOnRailsAsyncProp</code>'}})," helper into the root component's props (alongside the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">props:</code>'}})," you pass) — you don't import or create it. Calling it returns a Promise for the named async prop; an async child ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">await</code>'}}),"s that Promise inside a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundary."),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="jsx"><pre class="language-jsx"><code class="language-jsx"><span class="token comment">// app/javascript/components/MyStreamingComponent.jsx</span>\n<span class="token keyword">import</span> React<span class="token punctuation">,</span> <span class="token punctuation">{</span> Suspense <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">\'react\'</span><span class="token punctuation">;</span>\n\n<span class="token comment">// Async Server Component (requires RSC — see callout above). Awaits the</span>\n<span class="token comment">// streamed `posts` prop, then renders.</span>\n<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">PostList</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> posts <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>\n  <span class="token keyword">const</span> resolved <span class="token operator">=</span> <span class="token keyword">await</span> posts<span class="token punctuation">;</span>\n  <span class="token keyword">return</span> <span class="token punctuation">(</span>\n    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span><span class="token plain-text">\n      </span><span class="token punctuation">{</span>resolved<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">post</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span>\n        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">key</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>post<span class="token punctuation">.</span>id<span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token punctuation">{</span>post<span class="token punctuation">.</span>title<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>
1\n      <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span>\n  <span class="token punctuation">)</span><span class="token punctuation">;</span>\n<span class="token punctuation">}</span>\n\n<span class="token keyword">const</span> <span class="token function-variable function">MyStreamingComponent</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> greeting<span class="token punctuation">,</span> getReactOnRailsAsyncProp <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>\n  <span class="token comment">// Returns a Promise that resolves when Rails emits the `posts` prop.</span>\n  <span class="token keyword">const</span> postsPromise <span class="token operator">=</span> <span class="token function">getReactOnRailsAsyncProp</span><span class="token punctuation">(</span><span class="token string">\'posts\'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>\n\n  <span class="token keyword">return</span> <span class="token punctuation">(</span>\n    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span><span class="token punctuation">></span></span><span class="token plain-text">\n        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>greeting<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text">\n      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span><span class="token plain-text">\n      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><span class="token plain-text">Loading posts...</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">PostList</span></span> <span class="token attr-name">posts</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>postsPromise<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span></span><span class="token punctuation">></span></span>\n  <span class="token punctuation">)</span><span class="token punctuation">;</span>\n<span class="token punctuation">}</span><span class="token punctuation">;</span>\n\n<span class="token keyword">export</span> <span class="token keyword">default</span> MyStreamingComponent<span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"React on Rails note:")," Database queries, authentication, authorization, and caching all stay on the Rails side — the controller and view own them. Async props just let Rails emit each result the moment it has it, instead of blocking the whole render on the slowest source. The component never fetches data directly. In TypeScript, type the root component's props with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">WithAsyncProps&lt;AsyncProps, SyncProps></code>'}})," from ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails-pro</code>'}}),", and type each async child's prop as a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Promise</code>'}})," (e.g. ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">posts: Promise&lt;Post[]></code>'}}),") — ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">getReactOnRailsAsyncProp</code>'}})," returns a Promise, not the resolved value. See ",t.createElement(n.a,{href:"../oss/migrating/rsc-data-fetching.md#data-fetching-in-react-on-rails-pro"},"Data Fetching in React on Rails Pro")," for the prop-based data model this builds on."),"\n"),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Handle failures:")," if a prop's query rejects, the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">await</code>'}})," throws inside the async component and React propagates it up the tree. In production, wrap each ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," in an error boundary so a single failed source degrades to a fallback instead of breaking the whole render. React does not ship an ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ErrorBoundary</code>'}})," — use the ",t.createElement(n.a,{href:"https://github.com/bvaughn/react-error-boundary"},t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-error-boundary</code>'}}))," package (or your own class component):"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="jsx"><pre class="language-jsx"><code class="language-jsx"><span class="token keyword">
1import</span> <span class="token punctuation">{</span> ErrorBoundary <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">\'react-error-boundary\'</span><span class="token punctuation">;</span>\n\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">ErrorBoundary</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><span class="token plain-text">Posts unavailable</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><span class="token plain-text">Loading posts...</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">PostList</span></span> <span class="token attr-name">posts</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>postsPromise<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">ErrorBoundary</span></span><span class="token punctuation">></span></span><span class="token punctuation">;</span></code></pre></div>'}}),"\n"),"\n",t.createElement(n.p,null,"With ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">auto_load_bundle</code>'}})," enabled (recommended), ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">MyStreamingComponent</code>'}})," is registered automatically. Otherwise, register it as a server component — server components use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">registerServerComponent</code>'}}),", ",t.createElement(n.strong,null,"not")," ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRails.register</code>'}}),":"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js"><span class="token comment">// Server bundle — bundles the actual component code</span>\n<span class="token keyword">import</span> registerServerComponent <span class="token keyword">from</span> <span class="token string">\'react-on-rails-pro/registerServerComponent/server\'</span><span class="token punctuation">;</span>\n<span class="token keyword">
1import</span> MyStreamingComponent <span class="token keyword">from</span> <span class="token string">\'../components/MyStreamingComponent\'</span><span class="token punctuation">;</span>\n\n<span class="token function">registerServerComponent</span><span class="token punctuation">(</span><span class="token punctuation">{</span> MyStreamingComponent <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js"><span class="token comment">// Client bundle — registers by name; the component code stays out of the client bundle</span>\n<span class="token keyword">import</span> registerServerComponent <span class="token keyword">from</span> <span class="token string">\'react-on-rails-pro/registerServerComponent/client\'</span><span class="token punctuation">;</span>\n\n<span class="token function">registerServerComponent</span><span class="token punctuation">(</span><span class="token string">\'MyStreamingComponent\'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"See ",t.createElement(n.a,{href:"./react-server-components/create-without-ssr.md#register-the-react-server-component-page"},"Create a React Server Component")," for the full registration and RSC bundle setup."),"\n",t.createElement(n.h3,{id:"3-add-the-component-to-your-rails-view",style:{position:"relative"}},t.createElement(n.a,{href:"#3-add-the-component-to-your-rails-view","aria-label":"3 add the component to your rails view permalink",className:"anchor before"},t.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>'}})),"3. Add The Component To Your Rails View"),"\n",t.createElement(n.p,null,"Use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component_with_async_props</code>'}}),". Fast props go in ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">props:</code>'}}),"; each ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.call(name, value)</code>'}})," streams one more prop to the browser as soon as it resolves. Run the slow query ",t.createElement(n.strong,null,"inside the block")," — that's what lets the shell render first and the prop stream in afterward. (Loading it in the controller before this helper would block the shell until the query finished, defeating the point of async props.) Keep the block thin: call a model scope or query object, not business logic."),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="erb"><pre class="language-erb"><code class="language-erb"><span class="token comment">&lt;!-- app/views/example/show.html.erb --\x3e</span>\n\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> stream_react_component_with_async_props<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'MyStreamingComponent\'</span></span><span class="token punctuation">,</span>\n      <span class="token symbol">props</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token symbol">greeting</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">\'Hello, Streaming World!\'</span></span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>emit<span class="token operator">|</span>\n  <span class="token comment"># Runs in the streaming flow: the shell (greeting + Suspense fallback) is</span>\n  <span class="token comment"># sent first, then this emits `posts` once the query resolves. Rails still</span>\n  <span class="token comment"># owns the query, auth, and caching — `Post.recent` is a model scope.</span>\n  emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'posts\'</span></span><span class="token punctuation">,</span> Post<span class="token punctuation">.</span>recent<span class="token punctuation">.</span>limit<span class="token punctuation">(</span><span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">,</span> <span class="token symbol">:title</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>\n<span class="token keyword">end</span> </span><span class="token delimiter punctuation">%></span></span>\n\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>footer</span><span class="token punctuation">></span></span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Footer content<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>footer</span><span class="token punctuation">></span></span></code></pre></div>'}}),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,"If every data source is fast, use the simpler ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}})," and pass all props synchronously — React still streams the rendered HTML to the browser as it walks the tree. Reach for async props when one or more sources are slow. See ",t.createElement(n.a,{href:"../oss/migrating/rsc-data-fetching.md#data-fetching-in-react-on-rails-pro"},"Data Fetching in React on Rails Pro"),"."),"\n"),"\n",t.createElement(n.h3,{id:"4-render-the-view-using-the-stream_view_containing_react_components-helper",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#4-render-the-view-using-the-stream_view_containing_react_components-helper","aria-label":"4 render the view using the stream_view_containing_react_components helper permalink",className:"anchor before"},t.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>'}})),"4. Render The View Using The ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_view_containing_react_components</code>'}})," Helper"),"\n",t.createElement(n.p,null,"Ensure you have a controller that renders the view containing the React components. The controller must include the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRails::Controller</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRailsPro::Stream</code>'}})," modules."),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># app/controllers/example_controller.rb</span>\n\n<span class="token keyword">class</span> <span class="token class-name">ExampleController</span> <span class="token operator">&lt;</span> ApplicationController\n  <span class="token keyword">include</span> ReactOnRails<span class="token double-colon punctuation">::</span>Controller\n  <span class="token keyword">include</span> ReactOnRailsPro<span class="token double-colon punctuation">::</span>Stream\n  <span class="token comment"># Note: ActionController::Live is already mixed in by ReactOnRailsPro::Stream,</span>\n  <span class="token comment"># but you can include it explicitly if you prefer.</span>\n\n  <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span>\n    stream_view_containing_react_components<span class="token punctuation">(</span><span class="token symbol">template</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">\'example/show\'</span></span><span class="token punctuation">)</span>\n  <span class="token keyword">end</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The controller just renders — the async prop's query runs in the view's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit</code>'}})," block (Step 3) so the shell can stream before the query finishes. The controller still owns request-level concerns: authentication, authorization, and any fast props you pass synchronously."),"\n",t.createElement(n.h3,{id:"buffered-static-rsc-responses",style:{position:"relative"}},t.createElement(n.a,{href:"#buffered-static-rsc-responses","aria-label":"buffered static rsc responses permalink",className:"anchor before"},t.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>'}})),"Buffered Static RSC Responses"),"\n",t.createElement(n.p,null,"For public, static, or cacheable pages where every prop is available before render, use the buffered helper instead of ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_view_containing_react_components</code>'}}),". It still uses the Pro streaming/RSC renderer internally, so server components and embedded Flight payloads work, but Rails receives the complete HTML string before committing the response:"),"\n",t.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"> buffered_stream_react_component<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"MarketingPage"</span></span><span class="token punctuation">,</span>
1\n      <span class="token symbol">props</span><span class="token operator">:</span> <span class="token variable">@marketing_page_props</span><span class="token punctuation">,</span>\n      <span class="token symbol">id</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"marketing-page"</span></span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cached_buffered_stream_react_component</code>'}})," when the complete static response should be cached with the same cache-key and tag semantics as other Pro cached helpers:"),"\n",t.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  cached_buffered_stream_react_component<span class="token punctuation">(</span>\n    <span class="token string-literal"><span class="token string">"MarketingPage"</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><span class="token string-literal"><span class="token string">"marketing-page"</span></span><span class="token punctuation">,</span> I18n<span class="token punctuation">.</span>locale<span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">cache_tags</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"marketing-page"</span></span><span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">cache_options</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token symbol">expires_in</span><span class="token operator">:</span> <span class="token number">30.</span>minutes <span class="token punctuation">}</span><span class="token punctuation">,</span>\n    <span class="token symbol">id</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"marketing-page"</span></span>\n  <span class="token punctuation">)</span> <span class="token keyword">do</span>\n    <span class="token variable">@marketing_page_props</span>\n  <span class="token keyword">end</span>\n</span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"For static public RSC pages that do not hydrate the generated page pack, use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cached_static_rsc_component</code>'}}),". It buffers through the same renderer, strips embedded ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">REACT_ON_RAILS_RSC_PAYLOADS</code>'}})," bootstrap scripts before caching, and respects an explicit ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">auto_load_bundle: false</code>'}})," so the view can append only a small sidecar pack:"),"\n",t.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  cached_static_rsc_component<span class="token punctuation">(</span>\n    <span class="token string-literal"><span class="token string">"MarketingPage"</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><span class="token string-literal"><span class="token string">"marketing-page"</span></span><span class="token punctuation">,</span> I18n<span class="token punctuation">.</span>locale<span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">cache_tags</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"marketing-page"</span></span><span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">cache_options</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token symbol">expires_in</span><span class="token operator">:</span> <span class="token number">30.</span>minutes <span class="token punctuation">}</span><span class="token punctuation">,</span>\n    <span class="token symbol">auto_load_bundle</span><span class="token operator">:</span> <span class="token boolean">
1false</span><span class="token punctuation">,</span>\n    <span class="token symbol">rsc_diagnostic_packs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"generated/PublicPageClientEffects"</span></span><span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">id</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"marketing-page"</span></span>\n  <span class="token punctuation">)</span> <span class="token keyword">do</span>\n    <span class="token variable">@marketing_page_props</span>\n  <span class="token keyword">end</span>\n</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"> javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"generated/PublicPageClientEffects"</span></span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Buffered rendering is not progressive: the browser receives the page only after every RSC/SSR chunk is available. Prefer ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}})," or ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component_with_async_props</code>'}})," when early shell flush, slow async props, or progressive Suspense boundaries matter."),"\n",t.createElement(n.h4,{id:"static-rsc-render-diagnostics",style:{position:"relative"}},t.createElement(n.a,{href:"#static-rsc-render-diagnostics","aria-label":"static rsc render diagnostics permalink",className:"anchor before"},t.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>'}})),"Static RSC Render Diagnostics"),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cached_static_rsc_component</code>'}})," emits a redacted render summary in development and when ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRailsPro.configuration.tracing</code>'}})," is enabled. You can also enable it per render with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rsc_render_diagnostics: true</code>'}})," or capture it directly with a callback:"),"\n",t.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  cached_static_rsc_component<span class="token punctuation">(</span>\n    <span class="token string-literal"><span class="token string">"MarketingPage"</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><span class="token string-literal"><span class="token string">"marketing-page"</span></span><span class="token punctuation">,</span> I18n<span class="token punctuation">.</span>locale<span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">auto_load_bundle</span><span class="token operator">:</span> <span class="token boolean">
1false</span><span class="token punctuation">,</span>\n    <span class="token symbol">rsc_diagnostic_packs</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"generated/PublicPageClientEffects"</span></span><span class="token punctuation">]</span><span class="token punctuation">,</span>\n    <span class="token symbol">rsc_render_diagnostics</span><span class="token operator">:</span> <span class="token operator">-</span><span class="token operator">></span><span class="token punctuation">(</span>summary<span class="token punctuation">)</span> <span class="token punctuation">{</span> Rails<span class="token punctuation">.</span>logger<span class="token punctuation">.</span>info<span class="token punctuation">(</span>summary<span class="token punctuation">.</span>to_json<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>\n    <span class="token symbol">id</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"marketing-page"</span></span>\n  <span class="token punctuation">)</span> <span class="token keyword">do</span>\n    <span class="token variable">@marketing_page_props</span>\n  <span class="token keyword">end</span>\n</span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The same payload is published as an ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActiveSupport::Notifications</code>'}})," event so benchmark harnesses can archive it with ShakaPerf or Lighthouse output:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby">ActiveSupport<span class="token double-colon punctuation">::</span>Notifications<span class="token punctuation">.</span>subscribe<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"render_static_rsc_component.react_on_rails_pro"</span></span><span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>_name<span class="token punctuation">,</span> _start<span class="token punctuation">,</span> _finish<span class="token punctuation">,</span> _id<span class="token punctuation">,</span> summary<span class="token operator">|</span>\n  Rails<span class="token punctuation">.</span>logger<span class="token punctuation">.</span>info<span class="token punctuation">(</span>summary<span class="token punctuation">.</span>to_json<span class="token punctuation">)</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The summary includes:"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cache.enabled</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cache.hit</code>'}}),", and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">cache.key_digest</code>'}})," so public-page runs can distinguish cold renders from warm cached output without logging raw cache keys."),"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">auto_load_bundle</code>'}}),", which shows whether the generated page pack was intentionally skipped."),"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">html.raw_bytes</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">html.cached_bytes</code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rsc_payload.bootstrap_script_count</code>'}}),", and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rsc_payload.bootstrap_script_bytes</code>'}}
1),", which show how much Flight/bootstrap script markup was removed before caching."),"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emitted_assets.js</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emitted_assets.css</code>'}}),", populated from the Shakapacker manifest for the generated component pack when ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">auto_load_bundle</code>'}})," is true and for any explicit ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rsc_diagnostic_packs</code>'}}),"."),"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">client_references.entries</code>'}}),", populated from the RSC client manifest when it is available on disk. An empty array is useful evidence for static public pages that should avoid eager RSC client references."),"\n"),"\n",t.createElement(n.p,null,"Manifest-derived fields are best-effort. When a dev server, missing file, or custom deployment makes a manifest unavailable, the summary includes an ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">unavailable_reason</code>'}})," instead of failing the render. The summary never includes serialized props, Flight payload contents, or the raw expanded cache key."),"\n",t.createElement(n.h3,{id:"browser-observable-rsc-stream-marks",style:{position:"relative"}},t.createElement(n.a,{href:"#browser-observable-rsc-stream-marks","aria-label":"browser observable rsc stream marks permalink",className:"anchor before"},t.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>'}})),"Browser-Observable RSC Stream Marks"),"\n",t.createElement(n.p,null,"For streamed React Server Component pages, the initial Flight payload is embedded in the HTML stream. It is not a separate browser resource, so ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PerformanceResourceTiming</code>'}})," will not show a named RSC payload request for the initial page load. To inspect streamed payload bytes and flush timing from browser tooling without changing that transport, opt in to inline performance marks:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby">stream_view_containing_react_components<span class="token punctuation">(</span>\n  <span class="token symbol">template</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"example/show"</span></span><span class="token punctuation">,</span>\n  <span class="token symbol">rsc_stream_observability</span><span class="token operator">:</span> <span class="token boolean">true</span>\n<span class="token punctuation">)</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"When enabled, React on Rails Pro appends small inline scripts to the streamed response and enables matching client-side hydration marks for RSC island roots. Each mark first tries:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js">performance<span class="token punctuation">.</span><span class="token function">mark</span><span class="token punctuation">(</span><span class="token string">
1\'react-on-rails:rsc:payload\'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> detail <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"If the browser does not support mark details, or if ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">performance.mark</code>'}})," is unavailable, the same entry is appended to:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js">self<span class="token punctuation">.</span><span class="token constant">REACT_ON_RAILS_PERFORMANCE_MARKS</span><span class="token punctuation">;</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Use a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PerformanceObserver</code>'}})," for the normal path and read the queue as a fallback:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="js"><pre class="language-js"><code class="language-js"><span class="token keyword">const</span> observer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PerformanceObserver</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">list</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>\n  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> entry <span class="token keyword">of</span> list<span class="token punctuation">.</span><span class="token function">getEntries</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>\n    <span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">\'react-on-rails:rsc:\'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>\n      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>entry<span class="token punctuation">.</span>name<span class="token punctuation">,</span> entry<span class="token punctuation">.</span>detail<span class="token punctuation">)</span><span class="token punctuation">;</span>\n    <span class="token punctuation">}</span>\n  <span class="token punctuation">}</span>\n<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>\n\nobserver<span class="token punctuation">.</span><span class="token function">observe</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">\'mark\'</span><span class="token punctuation">,</span> <span class="token literal-property property">buffered</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>\n\n<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> entry <span class="token keyword">of</span> self<span class="token punctuation">.</span><span class="token constant">REACT_ON_RAILS_PERFORMANCE_MARKS</span> <span class="token operator">||</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>\n  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>entry<span class="token punctuation">.</span>name<span class="token punctuation">,</span> entry<span class="token punctuation">.</span>detail<span class="token punctuation">,</span> entry<span class="token punctuation">.</span>fallback<span class="token punctuation">)</span><span class="token punctuation">;</span>\n<span class="token punctuation">}</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The emitted mark names are:"),"\n",t.createElement(n.table,null,t.createElement(n.thead,null,t.createElement(n.tr,null,t.createElement(n.th,null,"Mark name"),t.createElement(n.th,null,"What it records"))),t.createElement(n.tbody,null,t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:stream</code>'}})),t.createElement(n.td,null,"Rails-side stream completion, emitted as a final safe HTML chunk with initial template bytes, ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderDurationMs</code>'}}),", and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sinceStreamStartMs</code>'}}),". ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sinceStreamStartMs</code>'}})," is total wall-clock through component stream drain.")),t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:payload</code>'}})),t.createElement(n.td,null,"Each inline Flight payload chunk, including component name, chunk index, matching flush index, raw Flight bytes, and RSC payload script bytes.")),t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:flush</code>'}}
1)),t.createElement(n.td,null,"Each Node-side HTML/RSC combined flush, including HTML bytes, payload script bytes, payload mark script bytes, stylesheet bytes, and index. ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">streamChunkBytesBeforeFlushMark</code>'}})," excludes the flush mark script's own bytes.")),t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:hydration:start</code>'}})),t.createElement(n.td,null,"Client-side start of an RSC island root hydration or client render, including component name, DOM id, and mode.")),t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:hydration:interactive</code>'}})),t.createElement(n.td,null,"Browser task scheduled after root creation for that RSC island root, a practical interactivity boundary for browser benchmarks without changing the React tree shape.")))),"\n",t.createElement(n.p,null,"The details intentionally include byte counts, phase names, component names, DOM ids, hydrate/render mode, chunk indexes, and timing offsets. They do not include serialized props or Flight payload contents."),"\n",t.createElement(n.p,null,"For flush marks, ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">streamChunkBytesBeforeFlushMark</code>'}})," is the sum of the flush detail subfields and excludes the flush mark's own inline script bytes. ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">payloadMarkScriptBytes</code>'}})," counts only payload-mark inline scripts co-flushed with that chunk."),"\n",t.createElement(n.p,null,"Hydration start marks are emitted immediately before root creation. Interactive marks are scheduled outside the React element tree after root creation so observability does not add client-only wrapper components that could affect ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">useId()</code>'}})," hydration parity. If ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">hydrateRoot</code>'}})," or ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">createRoot</code>'}})," throws synchronously before returning a root, if root creation or hydration throws before mounting, or if the root is torn down before the scheduled mark runs, the paired ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:hydration:interactive</code>'}})," mark will be absent. A hydration error that surfaces after root creation may still leave the scheduled interactive mark in place; treat it as a root-created browser task boundary, not proof that every nested Suspense boundary resolved."),"\n",t.createElement(n.p,null,"The initial RSC payload remains inline in the navigation response by design. A separate ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">/rsc_payload/*</code>'}})," request exists for client-side RSC payload generation, but the streamed initial page does not switch to that fetch path unless your application deliberately renders that route separately."),"\n",t.createElement(n.h4,{id:"cdn-and-trailer-caveats",style:{position:"relative"}},t.createElement(n.a,{href:"#cdn-and-trailer-caveats","aria-label":"cdn and trailer caveats permalink",className:"anchor before"},t.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>'}})),"CDN And Trailer Caveats"),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Server-Timing</code>'}})," headers are useful for non-streamed responses, but ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActionController::Live</code>'}})," commits headers on the first stream write. HTTP trailers can theoretically carry late timing data, but browser, proxy, and CDN support varies, and intermediaries may buffer, strip, rewrite, or hide trailer/header timing values. React on Rails Pro therefore does not make HTTP trailers the default streamed observability path. Inline marks travel in the response body, so they survive the same CDN path as the streamed HTML and are visible to ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PerformanceObserver</code>'}})," or the fallback queue after the browser parses them."),"\n",t.createElement(n.h4,{id:"server-timing-attribution",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#server-timing-attribution","aria-label":"server timing attribution permalink",className:"anchor before"},t.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>'}})),"Server-Timing Attribution"),"\n",t.createElement(n.p,null,"The browser-observable marks above close the client loop. To attribute the server/renderer side of a streamed RSC response's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">responseEnd</code>'}})," tail (see ",t.createElement(n.a,{href:"https://github.com/shakacode/react_on_rails/issues/4239"},"issue #4239"),"), ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rsc_stream_observability: true</code>'}})," also emits a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Server-Timing</code>'}})," ",t.createElement(n.strong,null,"response header"),". Because the header is set in the narrow window before the first stream write — the only point at which ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActionController::Live</code>'}})," will still accept header changes — it can only carry timing known before the first byte is flushed:"),"\n",t.createElement(n.table,null,t.createElement(n.thead,null,t.createElement(n.tr,null,t.createElement(n.th,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Server-Timing</code>'}})," metric"),t.createElement(n.th,null,"Emitted by"),t.createElement(n.th,null,"What it records"))),t.createElement(n.tbody,null,t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ror_stream_shell</code>'}})),t.createElement(n.td,null,"Rails (gem)"),t.createElement(n.td,null,"Duration of the shell ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">render_to_string</code>'}}),", which blocks on the first renderer chunk for each streamed component — the wait tail.")),t.createElement(n.tr,null,t.createElement(n.td,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ror_renderer_prepare</code>'}})),t.createElement(n.td,null,"Node renderer (response)"),t.createElement(n.td,null,"Execution-context build (cold or cached) plus the synchronous render that produces the stream object, before the first chunk.")))),"\n",t.createElement(n.p,null,"Both ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ror_stream_shell</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ror_renderer_prepare</code>'}})," appear on the navigation response your browser sees, visible in the Network panel and in ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PerformanceResourceTiming.serverTiming</code>'}}),". Rails captures the renderer response's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Server-Timing</code>'}})," value while waiting for the first streamed chunk, then appends it to the browser-facing response before the first stream write commits headers."),"\n",t.createElement(n.p,null,"Both metrics are emitted only when ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rsc_stream_observability: true</code>'}})," is set for the streamed render."),"\n",t.createElement(n.p,null,"One figure is intentionally ",t.createElement(n.strong,null,"not")," on the browser-facing header:"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Total / stream-complete renderer time")," is only known after the body is flushed, and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActionController::Live</code>'}})," does not support HTTP trailers (see the caveat above). It remains available via the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-on-rails:rsc:stream</code>'}})," mark's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sinceStreamStartMs</code>'}}),"."),"\n"),"\n",t.createElement(n.p,null,"Benchmark harnesses should read both ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ror_stream_shell</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ror_renderer_prepare</code>'}})," from the streamed navigation entry's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PerformanceResourceTiming.serverTiming</code>'}})," collection. The browser-facing header may include upstream renderer ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Server-Timing</code>'}})," entries if your renderer response already sets them; React on Rails Pro appends rather than replaces those values."),"\n",t.createElement(n.p,null,"Existing ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Server-Timing</code>'}})," entries set by your app or middleware (for example route-level ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">action_total</code>'}}),") are preserved — the gem appends to them rather than replacing."),"\n",t.createElement(n.h4,{id:"loading-multiple-slow-sources-in-parallel",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#loading-multiple-slow-sources-in-parallel","aria-label":"loading multiple slow sources in parallel permalink",className:"anchor before"},t.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>'}})),"Loading Multiple Slow Sources in Parallel"),"\n",t.createElement(n.p,null,"When several sources are slow and independent, run them concurrently with the ",t.createElement(n.a,{href:"https://github.com/socketry/async"},t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async</code>'}})," gem")," (already a dependency of React on Rails Pro and loaded by the streaming helper). Pro's streaming stack is fiber-based, so fan out with fibers from the running reactor rather than introducing a separate Ruby-thread concurrency model. The emit block runs inside an ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Async</code>'}})," task — capture it with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Async::Task.current</code>'}})," and spawn one child task per source with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">parent.async</code>'}}),", so each prop is emitted the moment its own query resolves:"),"\n",t.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"> stream_react_component_with_async_props<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'Dashboard\'</span></span><span class="token punctuation">,</span>\n      <span class="token symbol">props</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token symbol">title</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">\'Dashboard\'</span></span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>emit<span class="token operator">|</span>\n  parent  <span class="token operator">=</span> Async<span class="token double-colon punctuation">::</span>Task<span class="token punctuation">.</span>current\n  user_id <span class="token operator">=</span> current_user<span class="token punctuation">.</span>id <span class="token comment"># capture request state before fanning out</span>\n\n  <span class="token comment"># Each task runs its query concurrently and emits its prop as soon as that</span>\n  <span class="token comment"># query resolves — the slowest source no longer blocks the others.</span>\n  tasks <span class="token operator">=</span> <span class="token punctuation">[</span>\n    parent<span class="token punctuation">.</span>async <span class="token punctuation">{</span> emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'posts\'</span></span><span class="token punctuation">,</span> Post<span class="token punctuation">.</span>for_user<span class="token punctuation">(</span>user_id<span class="token punctuation">)</span><span class="token punctuation">.</span>recent<span class="token punctuation">.</span>limit<span class="token punctuation">(</span><span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">,</span> <span class="token symbol">:title</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>\n    parent<span class="token punctuation">.</span>async <span class="token punctuation">{</span> emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'stats\'</span></span><span class="token punctuation">,</span> DashboardStats<span class="token punctuation">.</span><span class="token keyword">
1for</span><span class="token punctuation">(</span>user_id<span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token symbol">:metric</span><span class="token punctuation">,</span> <span class="token symbol">:value</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>\n    parent<span class="token punctuation">.</span>async <span class="token punctuation">{</span> emit<span class="token punctuation">.</span>call<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'feed\'</span></span><span class="token punctuation">,</span>  FeedService<span class="token punctuation">.</span><span class="token keyword">for</span><span class="token punctuation">(</span>user_id<span class="token punctuation">)</span><span class="token punctuation">.</span>as_json<span class="token punctuation">(</span><span class="token symbol">only</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">,</span> <span class="token symbol">:title</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>\n  <span class="token punctuation">]</span>\n  tasks<span class="token punctuation">.</span><span class="token keyword">each</span><span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token symbol">:wait</span><span class="token punctuation">)</span> <span class="token comment"># block stays open until each task finishes</span>\n<span class="token keyword">end</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"On the React side, give each async prop its own ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundary (as in Step 2) so each section streams in independently as its query finishes."),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Error handling — two distinct failure modes:")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"An error raised in the task ",t.createElement(n.strong,null,"body before ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.call</code>'}}))," (e.g. a query raising ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActiveRecord::StatementInvalid</code>'}}),") propagates through ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">wait</code>'}})," and closes the stream, so the remaining props may not be sent. If one source failing shouldn't cut off the others, wrap each task body in a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rescue</code>'}})," and emit a fallback (or skip that prop)."),"\n",t.createElement(n.li,null,"An error raised ",t.createElement(n.strong,null,"inside ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.call</code>'}}))," (a non-serializable value, a write failure) may be swallowed by the emitter — logged rather than re-raised — so the task completes normally and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">wait</code>'}})," sees nothing, yet the prop is never delivered and its ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundary never resolves. Pass only JSON-serializable values (e.g. ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">.as_json(only: [...])</code>'}}),") to ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit.call</code>'}})," to avoid a silently hanging boundary."),"\n"),"\n"),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Requirements and footguns for concurrent async props:")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">config.active_support.isolation_level = :fiber</code>'}}
1))," (Rails 7.1+) — without it, fibers share one connection and concurrent queries corrupt each other; with it, each fiber checks out its own pooled connection. (This setting exists in Rails 7.0 but the connection pool only respects fiber identity starting in Rails 7.1.)"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Connection pool size")," — must cover the total DB-using fibers in flight across ",t.createElement(n.em,null,"all")," concurrent requests, not just this one fan-out, or extra fibers block on checkout."),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Driver matters")," — queries only truly overlap on a fiber-scheduler-aware driver such as ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">pg</code>'}})," (PostgreSQL). Blocking drivers like ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">mysql2</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sqlite3</code>'}})," serialize on the reactor, so the fan-out gives no speedup."),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Capture request state before fanning out")," — read ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">current_user.id</code>'}})," (and any other ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">CurrentAttributes</code>'}}),") into locals ",t.createElement(n.em,null,"before")," ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">parent.async</code>'}}),". Per-fiber isolation does not copy ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">CurrentAttributes</code>'}})," into child tasks, and this fails silently as stale/missing data rather than an error."),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Reactor-only")," — ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Async::Task.current</code>'}})," only works inside the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">emit</code>'}})," block (which runs in Pro's reactor). Don't copy this fan-out into a plain controller action, background job, or test that hasn't started an Async reactor — it raises ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Async::Error</code>'}}),"."),"\n"),"\n",t.createElement(n.blockquote,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Note:")," Running queries concurrently this way relies on ActiveRecord and your database driver supporting fiber scheduling (non-blocking I/O under a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Fiber.scheduler</code>'}}),"). See ",t.createElement(n.a,{href:"/linked-assets/98f9061f8396f36af9d98f9d2223fe22/async-props-database-queries.md"},"Database Queries in Async Props Blocks")," for the complete configuration guide — including isolation level, pool sizing, driver compatibility, connection lifecycle, and troubleshooting. If fiber scheduling isn't in place, the queries still run correctly, just serialized rather than in parallel."),"\n"),"\n",t.createElement(n.h3,{id:"5-test-your-application",style:{position:"relative"}},t.createElement(n.a,{href:"#5-test-your-application","aria-label":"5 test your application permalink",className:"anchor before"},t.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>'}})),"5. Test Your Application"),"\n",t.createElement(n.p,null,"You can test your application by running ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rails server</code>'}})," and navigating to the appropriate route."),"\n",t.createElement(n.h3,{id:"error-handling-note-for-react-on-rails-pro-167",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#error-handling-note-for-react-on-rails-pro-167","aria-label":"error handling note for react on rails pro 167 permalink",className:"anchor before"},t.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>'}})),"Error Handling Note for React on Rails Pro 16.7"),"\n",t.createElement(n.p,null,"React on Rails Pro 16.7 reports streaming renderer failures as ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ReactOnRailsPro::Error</code>'}})," during chunk iteration.\nThat includes unreadable response statuses and readable HTTP error statuses delivered as streaming bodies. If\nyour code directly iterates a stream from ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">render_code_as_stream</code>'}}),", wrap ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">each_chunk</code>'}})," in normal error handling and\ntreat that exception as a renderer failure. Older releases could return no chunks for these responses, so custom\ncallers should not use an empty stream as a success signal."),"\n",t.createElement(n.h3,{id:"6-what-happens-during-streaming",style:{position:"relative"}},t.createElement(n.a,{href:"#6-what-happens-during-streaming","aria-label":"6 what happens during streaming permalink",className:"anchor before"},t.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>'}})),"6. What Happens During Streaming"),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component_with_async_props</code>'}})," uses React's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToPipeableStream</code>'}})," to stream the shell first, then streams each async prop as Rails emits it:"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,"React renders everything outside the unresolved ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundaries and streams that shell immediately — the header and footer appear, with the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Loading posts...</code>'}})," fallback in place"),"\n",t.createElement(n.li,null,"When Rails runs ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:"<code class=\"language-text\">emit.call('posts', ...)</code>"}}),", the resolved value streams to the renderer and the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">posts</code>'}})," Promise resolves"),"\n",t.createElement(n.li,null,"The async ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PostList</code>'}})," awaits that Promise, renders, and React streams the post-list HTML chunk that replaces the fallback"),"\n"),"\n",t.createElement(n.p,null,"For example, with our ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">MyStreamingComponent</code>'}}),", the sequence is:"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,"The browser receives the shell immediately, including the Suspense fallback:"),"\n"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="html"><pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span><span class="token punctuation">></span></span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>Hello, Streaming World!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>s0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span>Loading posts...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>footer</span><span class="token punctuation">></span></span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Footer content<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>footer</span><span class="token punctuation">></span></span></code></pre></div>'}}),"\n",t.createElement(n.ol,{start:"2"},"\n",t.createElement(n.li,null,"Once Rails emits ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">posts</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">PostList</code>'}})," resolves, its rendered HTML streams to the browser:"),"\n"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="html"><pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span> <span class="token attr-name">hidden</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>b0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span>\n    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>First Post<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>\n    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span>Second Post<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span>\n  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">></span></span>\n\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">\n  <span class="token comment">// This implementation is slightly simplified</span>\n  document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">\'s0\'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">replaceChildren</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">\'b0\'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>\n</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"To render more of the page progressively, add an async prop and a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundary for each slow section — emit each one from the block as Rails resolves it, and every boundary streams in independently. This keeps the whole page in a single component tree (shared layout, context, and props) rather than splitting it across multiple ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">stream_react_component</code>'}})," calls."),"\n",t.createElement(n.p,null,"Extending the example with a second slow section (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">users</code>'}}),"), the page fills in stage by stage from the browser's perspective — visible from the first paint and interactive as each part hydrates, with each ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;Suspense></code>'}})," boundary swapping its fallback for real content as its prop arrives:"),"\n",t.createElement("p",null,t.createElement("img",{src:"images/progressive-stages.svg",alt:"Three browser snapshots. Stage 1 is the shell at about 50 ms: the header is done while users and posts show loading states, but the page is already visible and becomes interactive as JS loads. Stage 2: the users section fills in while posts still load. Stage 3: the page is complete with header, users, and posts all done. Each Suspense boundary swaps its fallback for real content as its prop arrives.",width:"840"})),"\n",t.createElement(n.h2,{id:"compression-for-streamed-rsc-responses",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#compression-for-streamed-rsc-responses","aria-label":"compression for streamed rsc responses permalink",className:"anchor before"},t.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>'}})),"Compression for Streamed RSC Responses"),"\n",t.createElement("span",{id:"compression-middleware-compatibility"}),"\n",t.createElement(n.p,null,"Streaming RSC renders HTML directly into the document instead of serializing a JSON data payload, so the ",t.createElement(n.strong,null,"raw")," (uncompressed) HTML transfer is larger than the equivalent Inertia/JSON response — but rendered HTML compresses far better. To make the transfer-bytes comparison fair, compress the streamed response end to end (see ",t.createElement(n.a,{href:"https://github.com/shakacode/react_on_rails/issues/4238"},"issue #4238"),"). The good news: streamed RSC responses compress with the ",t.createElement(n.strong,null,"same")," Rack middleware as any other response, with no special handling required."),"\n",t.createElement(n.h3,{id:"enabling-compression-in-rails",style:{position:"relative"}},t.createElement(n.a,{href:"#enabling-compression-in-rails","aria-label":"enabling compression in rails permalink",className:"anchor before"},t.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>'}})),"Enabling Compression in Rails"),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Deflater</code>'}})," (gzip/deflate) compresses streamed responses correctly out of the box:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># config/application.rb</span>\nconfig<span class="token punctuation">.</span>middleware<span class="token punctuation">.</span>use Rack<span class="token double-colon punctuation">::</span>Deflater</code></pre></div>'}}),"\n",t.createElement(n.p,null,"For Brotli (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">br</code>'}}),"), which typically beats gzip on HTML, add the ",t.createElement(n.a,{href:"https://github.com/marcotc/rack-brotli"},t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">rack-brotli</code>'}}))," gem and insert it the same way:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># Gemfile</span>\ngem <span class="token string-literal"><span class="token string">"rack-brotli"</span></span>\n\n<span class="token comment"># config/application.rb</span>\nconfig<span class="token punctuation">.</span>middleware<span class="token punctuation">.</span>use Rack<span class="token double-colon punctuation">::</span>Brotli</code></pre></div>'}}),"\n",t.createElement(n.p,null,"Either middleware negotiates the encoding from the request's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Accept-Encoding</code>'}})," header and sets ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Content-Encoding: gzip</code>'}})," / ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">br</code>'}})," on the response. The regression coverage in this repository verifies ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Deflater</code>'}})," end to end for gzip-compressed streaming responses. If your application intentionally runs both ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Deflater</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Brotli</code>'}}),", treat the combined middleware order as an application-level Rack stack choice: React on Rails Pro does not reorder 
1compression middleware, so verify the final stack order, ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Accept-Encoding</code>'}})," negotiation, and streamed flush behavior in your app before relying on both encodings in production."),"\n",t.createElement(n.h3,{id:"why-it-works-with-streaming",style:{position:"relative"}},t.createElement(n.a,{href:"#why-it-works-with-streaming","aria-label":"why it works with streaming permalink",className:"anchor before"},t.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>'}})),"Why It Works With Streaming"),"\n",t.createElement(n.p,null,"Streaming responses use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActionController::Live</code>'}}),", which writes chunks to a ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">SizedQueue</code>'}})," (a destructive, non-idempotent data structure). Standard Rack compression middleware works with streaming by wrapping the response body and compressing as Rack iterates the stream; this repository verifies that behavior with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Deflater</code>'}}),". Downstream proxy, CDN buffering, or stacked third-party compression middleware can still affect observed TTFB, so verify the full path with the checks below."),"\n",t.createElement(n.p,null,"However, if you pass an ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:if</code>'}})," condition that calls ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">body.each</code>'}})," to check the response size, ",t.createElement(n.strong,null,"streaming responses will deadlock"),". The ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:if</code>'}})," callback destructively consumes all chunks from the queue, leaving nothing for the compressor to read."),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># BAD — causes deadlocks with streaming responses</span>\nconfig<span class="token punctuation">.</span>middleware<span class="token punctuation">.</span>use Rack<span class="token double-colon punctuation">::</span>Deflater<span class="token punctuation">,</span> <span class="token symbol">if</span><span class="token operator">:</span> lambda <span class="token punctuation">{</span> <span class="token operator">|</span><span class="token operator">*</span><span class="token punctuation">,</span> body<span class="token operator">|</span>\n  sum <span class="token operator">=</span> <span class="token number">0</span>\n  body<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token punctuation">{</span> <span class="token operator">|</span>i<span class="token operator">|</span> sum <span class="token operator">+=</span> i<span class="token punctuation">.</span>length <span class="token punctuation">}</span>  <span class="token comment"># destructive — drains the queue</span>\n  sum <span class="token operator">></span> <span class="token number">512</span>\n<span class="token punctuation">}</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The ",t.createElement(n.a,{href:"https://github.com/rack/rack/blob/main/SPEC.rdoc"},"Rack SPEC")," states that ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">each</code>'}})," must only be called once and middleware must not call ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">each</code>'}})," directly unless the body responds to ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">to_ary</code>'}}),". Streaming bodies explicitly do not support ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">to_ary</code>'}}),"."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Correct pattern")," — check ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">to_ary</code>'}})," before iterating:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby">config<span class="token punctuation">.</span>middleware<span class="token punctuation">.</span>use Rack<span class="token double-colon punctuation">::</span>Deflater<span class="token punctuation">,</span> <span class="token symbol">if</span><span class="token operator">:</span> lambda <span class="token punctuation">{</span> <span class="token operator">|</span><span class="token operator">*</span><span class="token punctuation">,</span> body<span class="token operator">|</span>\n  <span class="token comment"># Streaming bodies don\'t support to_ary — always compress them.</span>\n  <span class="token comment"># Rack::Deflater handles streaming correctly when it owns body.each.</span>\n  <span class="token keyword">return</span> <span class="token boolean">true</span> <span class="token keyword">unless</span> body<span class="token punctuation">.</span>respond_to<span class="token operator">?</span><span class="token punctuation">(</span><span class="token symbol">:to_ary</span><span class="token punctuation">)</span>\n\n  body<span class="token punctuation">.</span>to_ary<span class="token punctuation">.</span>sum<span class="token punctuation">(</span><span class="token operator">&amp;</span><span class="token symbol">:bytesize</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">512</span>\n<span class="token punctuation">}</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"The same applies to ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Brotli</code>'}})," or any middleware that accepts an ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:if</code>'}})," callback."),"\n",t.createElement(n.h3,{id:"compression-at-the-reverse-proxy",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#compression-at-the-reverse-proxy","aria-label":"compression at the reverse proxy permalink",className:"anchor before"},t.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>'}})),"Compression at the Reverse Proxy"),"\n",t.createElement(n.p,null,"You can compress in Rails (above) ",t.createElement(n.strong,null,"or")," at the reverse proxy. nginx will not recompress a response whose ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Content-Encoding</code>'}})," is already set by Rails, but configuring compression at multiple layers adds complexity and can cause surprises with CDNs or older proxies. Pick one layer for predictable behavior. The critical constraint for streaming is that the proxy must ",t.createElement(n.strong,null,"not buffer the whole response")," before forwarding it, or you lose the streaming TTFB advantage."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"nginx.")," By default nginx buffers proxied responses (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">proxy_buffering on</code>'}}),"), which holds the stream until it is complete. Two options:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="nginx"><pre class="language-nginx"><code class="language-nginx"><span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span>\n  <span class="token directive"><span class="token keyword">proxy_pass</span> http://rails_app</span><span class="token punctuation">;</span>\n\n  <span class="token comment"># Option A — disable buffering for everything proxied here. Simplest; the</span>\n  <span class="token comment"># whole location streams. Pick Rails compression (Rack::Deflater /</span>\n  <span class="token comment"># Rack::Brotli) or nginx `gzip`/`brotli` directives so operators only need</span>\n  <span class="token comment"># to reason about one compression layer.</span>\n  <span class="token directive"><span class="token keyword">proxy_buffering</span> <span class="token boolean">off</span></span><span class="token punctuation">;</span>\n\n  <span class="token directive"><span class="token keyword">proxy_set_header</span> Host <span class="token variable">$host</span></span><span class="token punctuation">;</span>\n  <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-Proto <span class="token variable">$scheme</span></span><span class="token punctuation">;</span>\n  <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-For <span class="token variable">$proxy_add_x_forwarded_for</span></span><span class="token punctuation">;</span>\n<span class="token punctuation">}</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Option B — keep buffering on globally and let Rails opt specific streamed responses out")," with the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">X-Accel-Buffering: no</code>'}})," response header. nginx honors it per-response, so only streaming routes bypass the buffer while everything else keeps nginx buffering:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="nginx"><pre class="language-nginx"><code class="language-nginx"><span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span>\n  <span class="token directive"><span class="token keyword">proxy_pass</span> http://rails_app</span><span class="token punctuation">;</span>\n\n  <span class="token comment"># No proxy_buffering directive here; inherit the default `on`.</span>\n  <span class="token comment"># Rails sets X-Accel-Buffering: no on streaming responses that need it.</span>\n\n  <span class="token directive"><span class="token keyword">proxy_set_header</span>
1 Host <span class="token variable">$host</span></span><span class="token punctuation">;</span>\n  <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-Proto <span class="token variable">$scheme</span></span><span class="token punctuation">;</span>\n  <span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-For <span class="token variable">$proxy_add_x_forwarded_for</span></span><span class="token punctuation">;</span>\n<span class="token punctuation">}</span></code></pre></div>'}}),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby"><span class="token comment"># In the streaming controller action, before the first stream write:</span>\n<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span>\n  response<span class="token punctuation">.</span>headers<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"X-Accel-Buffering"</span></span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"no"</span></span>\n  stream_view_containing_react_components<span class="token punctuation">(</span><span class="token symbol">template</span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">"example/show"</span></span><span class="token punctuation">)</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">X-Accel-Buffering: no</code>'}})," only disables ",t.createElement(n.strong,null,"proxy")," buffering; it does not affect compression. If Rails already set ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Content-Encoding</code>'}}),", nginx will not recompress. If nginx itself compresses (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">gzip on</code>'}})," / the brotli module), chunk flushing depends on upstream flush signals and nginx's compression buffers. For guaranteed per-chunk TTFB with arbitrary streaming intervals, compress at the Rails layer (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Deflater</code>'}})," / ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Brotli</code>'}}),") rather than at nginx."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Cloudflare.")," Cloudflare can compress eligible responses automatically and should respect an existing ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Content-Encoding</code>'}})," from your origin rather than double-compressing. Do not assume any CDN preserves every application flush boundary: verify the production route with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">curl</code>'}}),", browser DevTools, and the inline RSC stream marks after Cloudflare rules, cache settings, and plan-specific features are applied. If your origin already sets ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Content-Encoding: br</code>'}}),", confirm that the header is still present at the browser."),"\n",t.createElement(n.h3,{id:"verifying-compression-end-to-end",style:{position:"relative"}},t.createElement(n.a,{href:"#verifying-compression-end-to-end","aria-label":"verifying compression end to end permalink",className:"anchor before"},t.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>'}})),"Verifying Compression End to End"),"\n",t.createElement(n.p,null,"Confirm the streamed route actually negotiates an encoding:"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-sSD</span> - <span class="token parameter variable">-o</span> /dev/null <span class="token parameter variable">-H</span> <span class="token string">\'Accept-Encoding: br, gzip\'</span> https://your-app.example/your-streaming-route\n<span class="token comment"># Look for:  content-encoding: br   (or gzip)</span>\n<span class="token comment">#            transfer-encoding: chunked   (no content-length — still streamed)</span>\n<span class="token comment">#            vary: Accept-Encoding   (CDNs must cache by negotiated encoding)</span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"In the browser, open DevTools → Network → select the navigation request → ",t.createElement(n.strong,null,"Response Headers")," should show ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">content-encoding</code>'}}),", and the ",t.createElement(n.strong,null,"Size")," column shows the transferred (compressed) bytes versus the larger uncompressed content size."),"\n",t.createElement(n.p,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Rack::Deflater</code>'}})," appends ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">Vary: Accept-Encoding</code>'}})," when it compresses a response. Keep that header intact through custom middleware and proxies so CDNs do not serve a cached gzip response to a client that did not advertise gzip support."),"\n",t.createElement(n.p,null,"The gem's own coverage of this path lives in ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react_on_rails_pro/spec/react_on_rails_pro/streaming_compression_spec.rb</code>'}}),", which asserts that a Rack body shaped like ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">ActionController::Live::Buffer</code>'}})," is gzip-compressed with the decompressed bytes unchanged."),"\n",t.createElement(n.h2,{id:"metadata-with-streaming",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#metadata-with-streaming","aria-label":"metadata with streaming permalink",className:"anchor before"},t.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>'}})),"Metadata with Streaming"),"\n",t.createElement(n.p,null,"Streaming SSR is fully compatible with React 19's native metadata tags. You can render ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;title></code>'}}),", ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;meta></code>'}}),", and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;link></code>'}})," anywhere in your component tree — including inside async components within Suspense boundaries — and React will hoist them into the document ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;head></code>'}}),"."),"\n",t.createElement(n.p,null,"This is a significant advantage over ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">react-helmet</code>'}}),", which requires ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">renderToString</code>'}})," and is incompatible with streaming. For details, see ",t.createElement(n.a,{href:"/linked-assets/925506244e705dff5ebbfeff5927a40e/react-19-native-metadata.md"},"React 19 Native Metadata"),"."),"\n",t.createElement(n.h2,{id:"when-to-use-streaming",style:{position:"relative"}},t.createElement(n.a,{href:"#when-to-use-streaming","aria-label":"when to use streaming permalink",className:"anchor before"},t.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>'}})),"When to Use Streaming"),"\n",t.createElement(n.p,null,"Streaming SSR is particularly valuable in specific scenarios. Here's when to consider it:"),"\n",t.createElement(n.h3,{id:"ideal-use-cases",style:{position:"relative"}},t.createElement(n.a,{href:"#ideal-use-cases","aria-label":"ideal use cases permalink",className:"anchor before"},t.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>'}})),"Ideal Use Cases"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Data-Heavy Pages")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"Pages that fetch data from multiple sources"),"\n",t.createElement(n.li,null,"Dashboard-style layouts where different sections can load independently"),"\n",t.createElement(n.li,null,"Content that requires heavy processing or computation"),"\n"),"\n"),"\n",t.createElement(n.li,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Progressive Enhancement")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"When you want users to see and interact with parts of the page while others load"),"\n",t.createElement(n.li,null,"For improving perceived performance on slower connections"),"\n",t.createElement(n.li,null,"When different parts of your page have different priority levels"),"\n"),"\n"),"\n",t.createElement(n.li,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Large, Complex Applications")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"Applications with multiple independent widgets or components"),"\n",t.createElement(n.li,null,"Pages where some content is critical and other content is supplementary"),"\n",t.createElement(n.li,null,"When you need to optimize Time to First Byte (TTFB)"),"\n"),"\n"),"\n"),"\n",t.createElement(n.h3,{id:"best-practices-for-streaming",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#best-practices-for-streaming","aria-label":"best practices for streaming permalink",className:"anchor before"},t.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>'}})),"Best Practices for Streaming"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Component Structure")),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="jsx"><pre class="language-jsx"><code class="language-jsx"><span class="token comment">// Good: Independent sections that can stream separately</span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Layout</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">HeaderSkeleton</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Header</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">MainContentSkeleton</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">MainContent</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">SidebarSkeleton</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Sidebar</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Layout</span></span><span class="token punctuation">></span></span>\n\n<span class="token comment">// Bad: Everything wrapped in a single Suspense boundary</span>\n<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Suspense</span></span> <span class="token attr-name">fallback</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">FullPageSkeleton</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Header</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">MainContent</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n  </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Sidebar</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">\n</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">Suspense</span></span><span class="token punctuation">></span></span></code></pre></div>'}}),"\n"),"\n",t.createElement(n.li,null,"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Data Loading Strategy")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"Prioritize critical data that should be included in the initial HTML"),"\n",t.createElement(n.li,null,"Use streaming for supplementary data that can load progressively"),"\n",t.createElement(n.li,null,"Consider implementing a waterfall strategy for dependent data"),"\n"),"\n"),"\n"),"\n",t.createElement(n.h2,{id:"script-loading-strategy-for-streaming",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#script-loading-strategy-for-streaming","aria-label":"script loading strategy for streaming permalink",className:"anchor before"},t.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>'}})),"Script Loading Strategy for Streaming"),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"IMPORTANT"),": When using streaming server rendering, you should NOT use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">defer: true</code>'}})," for your JavaScript pack tags. Here's why:"),"\n",t.createElement(n.h3,{id:"understanding-the-problem-with-defer",style:{position:"relative"}},t.createElement(n.a,{href:"#understanding-the-problem-with-defer","aria-label":"understanding the problem with defer permalink",className:"anchor before"},t.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>'}})),"Understanding the Problem with Defer"),"\n",t.createElement(n.p,null,"Deferred scripts (",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">defer: true</code>'}}),") only execute after the entire HTML document has finished parsing and streaming. This defeats the key benefit of React's Selective Hydration feature, which allows streamed components to hydrate as soon as they arrive—even while other parts of the page are still streaming."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Example Problem:")),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="erb"><pre class="language-erb"><code class="language-erb"><span class="token comment">&lt;!-- ❌ BAD: This delays hydration for ALL streamed components --\x3e</span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'client-bundle\'</span></span><span class="token punctuation">,</span> <span class="token symbol">defer</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"With ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">defer: true</code>'}}),", your streamed components will:"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,"Arrive progressively in the HTML stream"),"\n",t.createElement(n.li,null,"Be visible to users immediately"),"\n",t.createElement(n.li,null,"But remain non-interactive until the ENTIRE page finishes streaming"),"\n",t.createElement(n.li,null,"Only then will they hydrate"),"\n"),"\n",t.createElement(n.h3,{id:"recommended-approaches",style:{position:"relative"}},t.createElement(n.a,{href:"#recommended-approaches","aria-label":"recommended approaches permalink",className:"anchor before"},t.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>'}})),"Recommended Approaches"),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"For Pages WITH Streaming Components:")),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="erb"><pre class="language-erb"><code class="language-erb"><span class="token comment">&lt;!-- ✅ GOOD: No defer - allows Selective Hydration to work --\x3e</span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'client-bundle\'</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">\'data-turbo-track\'</span></span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">
1\'reload\'</span></span><span class="token punctuation">,</span> <span class="token symbol">defer</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span>\n\n<span class="token comment">&lt;!-- ✅ BEST: Use async for even faster hydration (requires Shakapacker ≥ 8.2.0) --\x3e</span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'client-bundle\'</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">\'data-turbo-track\'</span></span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">\'reload\'</span></span><span class="token punctuation">,</span> <span class="token symbol">async</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"For Pages WITHOUT Streaming Components:")),"\n",t.createElement(n.p,null,"With Shakapacker ≥ 8.2.0, ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}})," is recommended even for non-streaming pages to improve Time to Interactive (TTI):"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="erb"><pre class="language-erb"><code class="language-erb"><span class="token comment">&lt;!-- ✅ RECOMMENDED: Use async for optimal performance --\x3e</span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'client-bundle\'</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">\'data-turbo-track\'</span></span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">\'reload\'</span></span><span class="token punctuation">,</span> <span class="token symbol">async</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,"Note: With React on Rails Pro, ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}})," allows components to hydrate during page load, improving TTI even without streaming. See the Early Hydration section below for details."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"⚠️ Important: Redux Shared Store Caveat")),"\n",t.createElement(n.p,null,"If you are using Redux shared stores with the ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">redux_store</code>'}})," helper and ",t.createElement(n.strong,null,"inline script registration")," (registering components in view templates with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;script>ReactOnRails.register({ MyComponent })&lt;/script></code>'}}),"), you must use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">defer: true</code>'}})," instead of ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}}),":"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="erb"><pre class="language-erb"><code class="language-erb"><span class="token comment">&lt;!-- ⚠️ REQUIRED for Redux shared stores with inline registration --\x3e</span>\n<span class="token erb language-erb"><span class="token delimiter punctuation">&lt;%=</span><span class="token ruby language-ruby"> javascript_pack_tag<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">\'client-bundle\'</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">\'data-turbo-track\'</span></span><span class="token operator">:</span> <span class="token string-literal"><span class="token string">
1\'reload\'</span></span><span class="token punctuation">,</span> <span class="token symbol">defer</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span> </span><span class="token delimiter punctuation">%></span></span></code></pre></div>'}}),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Why?")," With ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}}),", the bundle executes immediately upon download, potentially ",t.createElement(n.strong,null,"before")," inline ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">&lt;script></code>'}})," tags in the HTML execute. This causes component registration failures when React on Rails tries to hydrate the component."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Solutions:")),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">defer: true</code>'}}))," - Ensures proper execution order (inline scripts run before bundle)"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Move registration to bundle")," - Register components in your JavaScript bundle instead of inline scripts (recommended)"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Use React on Rails Pro")," - Pro's ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">getOrWaitForStore</code>'}})," and ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">getOrWaitForStoreGenerator</code>'}})," can handle async loading with inline registration"),"\n"),"\n",t.createElement(n.p,null,"See the ",t.createElement(n.a,{href:"/linked-assets/bacc44f6c6f7f1a55ad64d8b36432b7f/redux-store-api.md"},"Redux Store API documentation")," for more details on Redux shared stores."),"\n",t.createElement(n.h3,{id:"why-async-is-better-than-no-defer",style:{position:"relative"}},t.createElement(n.a,{href:"#why-async-is-better-than-no-defer","aria-label":"why async is better than no defer permalink",className:"anchor before"},t.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>'}})),"Why Async is Better Than No Defer"),"\n",t.createElement(n.p,null,"With Shakapacker ≥ 8.2.0, using ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}})," provides the best performance:"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"No defer/async"),": Scripts block HTML parsing and streaming"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"defer: true"),": Scripts wait for complete page load (defeats Selective Hydration)"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"async: true"),": Scripts load in parallel and execute ASAP, enabling:","\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"Selective Hydration to work immediately"),"\n",t.createElement(n.li,null,"Components to become interactive as they stream in"),"\n",t.createElement(n.li,null,"Optimal Time to Interactive (TTI)"),"\n"),"\n"),"\n"),"\n",t.createElement(n.h3,{id:"migration-timeline",style:{position:"relative"}},t.createElement(n.a,{href:"#migration-timeline","aria-label":"migration timeline permalink",className:"anchor before"},t.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>'}})),"Migration Timeline"),"\n",t.createElement(n.ol,null,"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Before Shakapacker 8.2.0"),": Use ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">defer: false</code>'}})," for streaming pages"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Shakapacker ≥ 8.2.0"),": Migrate to ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}})," for all pages (streaming and non-streaming)"),"\n",t.createElement(n.li,null,t.createElement(n.strong,null,"Early Hydration (Pro)"),": Pro hydrates components early for optimal Time to Interactive — no additional configuration needed (see section below)"),"\n"),"\n",t.createElement(n.h2,{id:"early-hydration-pro",style:{position:"relative"}}
1,t.createElement(n.a,{href:"#early-hydration-pro","aria-label":"early hydration pro permalink",className:"anchor before"},t.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>'}})),"Early Hydration (Pro)"),"\n",t.createElement(n.p,null,"React on Rails Pro hydrates components during the page loading state (before ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">DOMContentLoaded</code>'}}),"), rather than waiting for the full page load. This works optimally with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}})," scripts and is always on for Pro — there is no configuration toggle."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Benefits of early hydration with ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">async: true</code>'}}),":")),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,"Components become interactive as soon as their JavaScript loads"),"\n",t.createElement(n.li,null,"No need to wait for ",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">DOMContentLoaded</code>'}})," or full-page load"),"\n",t.createElement(n.li,null,"Optimal Time to Interactive (TTI) for both streaming and non-streaming pages"),"\n",t.createElement(n.li,null,"Works seamlessly with React's Selective Hydration"),"\n"),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"Note:")," Early hydration requires a React on Rails Pro license. It is always enabled for Pro users and unavailable in OSS — no per-component toggle is exposed."),"\n",t.createElement(n.p,null,t.createElement(n.strong,null,"generated_component_packs_loading_strategy Option:")),"\n",t.createElement(n.p,null,"This configuration option sets the default loading strategy for auto-generated component packs:"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:async</code>'}})," (recommended for Shakapacker ≥ 8.2.0) - Scripts load asynchronously"),"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:defer</code>'}})," - Scripts defer until page load completes"),"\n",t.createElement(n.li,null,t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">:sync</code>'}})," - Scripts load synchronously (blocks page rendering)"),"\n"),"\n",t.createElement(n.span,{dangerouslySetInnerHTML:{__html:'<div class="gatsby-highlight" data-language="ruby"><pre class="language-ruby"><code class="language-ruby">ReactOnRails<span class="token punctuation">.</span>configure <span class="token keyword">do</span> <span class="token operator">|</span>config<span class="token operator">|</span>\n  config<span class="token punctuation">.</span>generated_component_packs_loading_strategy <span class="token operator">=</span> <span class="token symbol">:async</span>\n<span class="token keyword">end</span></code></pre></div>'}}),"\n",t.createElement(n.h2,{id:"further-reading",style:{position:"relative"}},t.createElement(n.a,{href:"#further-reading","aria-label":"further reading permalink",className:"anchor before"},t.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>'}})),"Further Reading"),"\n",t.createElement(n.ul,null,"\n",t.createElement(n.li,null,t.createElement(n.a,{href:"/linked-assets/5cfa7d251cd1b302804a29407c20001d/node-renderer.md"},"Node Renderer")," — Required for streaming SSR"),"\n",t.createElement(n.li,null,t.createElement(n.a,{href:"/linked-assets/925506244e705dff5ebbfeff5927a40e/react-19-native-metadata.md"},"React 19 Native Metadata")," — Metadata hoisting with streaming"),"\n",t.createElement(n.li,null,t.createElement(n.a,{href:"/linked-assets/37d1c89f8266fd0f03818d68f8999302/tutorial.md"},"React Server Components")," — RSC builds on streaming SSR for even more advanced rendering"),"\n"))}n.A=function(e){void 0===e&&(e={});const{wrapper:n}=Object.assign({},(0,s.R)(),e.components);return n?t.createElement(n,e,t.createElement(l,e)):l(e)}}}]);
2//# sourceMappingURL=ae81b3cc-4a04abaf4288922d8b72.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.