Skip to content

[Pro] Add buffered RSC rendering helpers for static pages - #4268

Merged
justin808 merged 17 commits into
mainfrom
codex/issue-4263-buffered-rsc-rendering
Jul 1, 2026
Merged

[Pro] Add buffered RSC rendering helpers for static pages#4268
justin808 merged 17 commits into
mainfrom
codex/issue-4263-buffered-rsc-rendering

Conversation

@justin808

@justin808 justin808 commented Jun 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds buffered_stream_react_component for static/cacheable pages that need the Pro streaming/RSC renderer without committing the Rails response through ActionController::Live.
  • Adds cached_buffered_stream_react_component, including cache-key invalidation on the server bundle digest and, when RSC support is enabled, the RSC bundle digest.
  • Centralizes RSC bundle digest handling in ReactOnRailsPro::Cache.base_cache_key for prerendered Pro cache keys, so cached_stream_react_component, cached_react_component, cached_react_component_hash, cached_buffered_stream_react_component, and the internal Pro prerender cache share deploy invalidation behavior.
  • Documents usage, CSP/cache-order caveats, API reference entries, fragment-cache behavior, and cache tag coverage.

Codex Decision Log

  • Non-blocking: The RSC bundle digest/sentinel is in shared base_cache_key, not only the new buffered helper.
    • Decision: Keep the shared-key behavior for all prerendered Pro cache keys, including the internal Pro prerender cache.
    • Why: In an RSC-enabled app, server-rendered output can begin depending on the RSC bundle without a Ruby helper callsite changing, so the shared Pro prerender key should move when the RSC bundle moves.
    • Review later: None; this is the maintainer-approved widened scope.

Validation

  • SECURITY_PREFLIGHT_OK for Support cacheable buffered rendering for static RSC pages #4263 with warning limited to issue body by justin808.
  • pnpm install --frozen-lockfile.
  • node script/generate-llms-full.mjs and node script/generate-llms-full.mjs --check.
  • pnpm start format.listDifferent.
  • script/check-docs-sidebar.
  • git diff --check.
  • script/ci-changes-detector origin/main: routes to Pro Ruby core, Pro dummy app, and uncategorized/full-suite coverage.
  • LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 RUBYOPT="-EUTF-8" bundle exec rspec spec/react_on_rails_pro/cache_spec.rb from react_on_rails_pro: 30 examples, 0 failures.
  • LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 RUBYOPT="-EUTF-8" bundle exec rspec spec/helpers/react_on_rails_pro_helper_spec.rb -e '#cached_buffered_stream_react_component' from react_on_rails_pro/spec/dummy: 7 examples, 0 failures.
  • BUNDLE_GEMFILE=../Gemfile bundle exec rubocop app/helpers/react_on_rails_pro_helper.rb lib/react_on_rails_pro/cache.rb spec/react_on_rails_pro/cache_spec.rb spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb --ignore-parent-exclusion from react_on_rails_pro: no offenses.
  • Pre-push hook passed on current head: branch RuboCop for 4 Pro Ruby files and Lychee link check for branch Markdown files, 0 errors.

Local broad-gate note

  • bin/ci-local --changed currently reaches a repo-wide RuboCop pass and fails on pre-existing offenses under react_on_rails/spike/3313_prism_gemfile_rewriter/*. This PR does not touch those files; focused changed-file lint/spec/docs checks above passed.

Review status

  • Accepted the maintainer-approved scope decision from the unresolved RSC cache-key review threads: widen this PR to include the RSC bundle digest in all prerendered Pro cache keys when RSC support is enabled.
  • Fixed current-head review findings in d84e7ed: missing-RSC-bundle sentinel, cache-hit auto_load_bundle normalization, and nil/false on_complete handling.
  • Replied to and resolved all current-head review threads.

Labels: ready-for-hosted-ci, full-ci, benchmark - this touches Pro helper/runtime caching behavior and server rendering paths.

Fixes #4263

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds RSC-aware cache key composition, a buffered streaming helper that returns complete HTML, a cached buffered variant, matching specs, and documentation updates describing the new behavior and related cache caveats.

Changes

Buffered RSC Rendering

Layer / File(s) Summary
RSC bundle digest in cache key
react_on_rails_pro/lib/react_on_rails_pro/cache.rb, react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb, react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb, react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
Prerender cache keys now include the RSC bundle digest when RSC support is enabled, or a missing-bundle sentinel when the digest cannot be read; specs cover enabled and disabled RSC cases.
buffered_stream_react_component helper
react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb, react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
Adds a helper that forces prerender: true, buffers streamed chunks into complete HTML, supports optional on_complete, and preserves caller options.
cached_buffered_stream_react_component helper
react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb, react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
Adds a fragment-cached buffered helper that rejects on_complete, uses buffered rendering on cache misses, instruments ScoutApm, and is covered by cache-hit and collision tests.
Documentation updates
CHANGELOG.md, docs/oss/api-reference/ruby-api-pro.md, docs/oss/building-features/caching.md, docs/pro/fragment-caching.md, docs/pro/streaming-ssr.md, docs/pro/strict-csp.md, llms-full-pro.txt, llms-full.txt
Documents the new helpers, RSC cache-key behavior, Rails context/render-order caveats, CSP nonce caveats, and cache-warming guidance.

Estimated code review effort: 3 (Moderate) | ~30 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: buffered RSC rendering helpers for static pages.
Linked Issues check ✅ Passed The PR adds buffered and cached buffered RSC helpers for static/cacheable pages, matching the requested supported path for #4263.
Out of Scope Changes check ✅ Passed The docs, cache-key, and test changes all support the buffered static RSC rendering work; no unrelated scope is evident.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-4263-buffered-rsc-rendering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@justin808
justin808 force-pushed the codex/issue-4263-buffered-rsc-rendering branch from df2363c to 6faadf1 Compare June 28, 2026 09:58
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Code Review

This PR adds buffered_stream_react_component and cached_buffered_stream_react_component to bridge the gap between the Pro streaming/RSC renderer and static/cacheable pages that don't want ActionController::Live response commits. The cache-key construction correctly chains through fetch_react_componentreact_component_cache_keybase_cache_key, so both the SSR bundle hash (bundle_hash) and RSC bundle hash (rsc_bundle_hash) end up in the cache key when prerender: true. Documentation is thorough.

Two correctness bugs found, both in cached_buffered_stream_react_component:


Bug 1 — Cache hits serve double-escaped HTML (high severity)

buffered_stream_react_component returns html.html_safe — an ActiveSupport::SafeBuffer. fetch_react_component stores this in Rails.cache via Rails.cache.fetch. Rails cache serializes via Marshal; ActiveSupport::SafeBuffer is a String subclass, and after Marshal.dump/Marshal.load the html_safe? flag is not preserved — the value comes back as a plain String. cached_buffered_stream_react_component returns that plain String directly without calling .html_safe. When the view renders <%= cached_buffered_stream_react_component(...) %>, ERB sees an unsafe string and HTML-escapes it, turning <div> into &lt;div&gt;.

First request (cache miss) renders correctly; every subsequent request (cache hit) shows escaped HTML source code.

Fix:

fetch_react_component(component_name, cache_options) do
  ...
  buffered_stream_react_component(component_name, options)
end.html_safe

See inline comment for the exact line.


Bug 2 — rsc_bundle_hash raises Errno::ENOENT when RSC is disabled (medium severity)

The cache-key lambda unconditionally calls ReactOnRailsPro::Utils.rsc_bundle_hash. That method calls Digest::MD5.file(rsc_bundle_js_file_path) — if the RSC bundle file doesn't exist (RSC support disabled, no RSC bundle built), it raises Errno::ENOENT. The lambda is evaluated on every cache lookup, so an RSC-disabled app using cached_buffered_stream_react_component gets an exception on every request, not just on first use.

By contrast, stream_react_component_with_async_props and rsc_payload_react_component_with_async_props both guard with unless ReactOnRailsPro.configuration.enable_rsc_support at method entry.

buffered_stream_react_component itself also has no such guard (though it would fail later at the renderer level rather than at cache-key construction time).

See inline comment for the suggested fix.

Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Code Review

This PR adds buffered_stream_react_component and cached_buffered_stream_react_component to bridge the gap between the Pro streaming/RSC renderer and static/cacheable Rails pages that can't use ActionController::Live. The architecture is sound: buffering chunks into an HTML string, re-marking it html_safe, and wiring into the existing fetch_react_component cache machinery with an explicit rsc_bundle_hash appended to the user's cache key.

Verified correct:

  • The cache key for cached_buffered_stream_react_component correctly includes both the SSR server bundle hash (via base_cache_key triggered by prerender: true) and the RSC bundle hash (via the lambda). Both bundles are covered for cache invalidation.
  • each_chunk yields html_safe strings (not raw objects), so the on_complete callback and the html.html_safe call are safe.
  • The absent enable_rsc_support guard is intentional and consistent with stream_react_component — HTML streaming degrades gracefully when RSC support is disabled.

One cleanup finding (inline): prerender: true in the cache-miss options block passed to buffered_stream_react_component is redundant — the method unconditionally forces it on entry — and may mislead readers into thinking the caller controls it.

Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Outdated
@justin808

Copy link
Copy Markdown
Member Author

Address-review summary

Scan scope: full current unresolved review threads after the first review round.

Mattered

  • Addressed cached-hit HTML safety: cached buffered results are normalized with html_safe, and the spec now forces a plain String cache hit before asserting safety. Commit: d0f7fcb.
  • Addressed non-RSC cache-key regression: cached buffered rendering only includes the RSC bundle hash when RSC support is enabled; the regression spec disables RSC and makes rsc_bundle_hash raise. Commit: d0f7fcb.
  • Addressed redundant miss-render option: removed the duplicate inner prerender true while preserving prerender in cache_options and buffered_stream_react_component. Commit: d0f7fcb.

Skipped

  • None.

Next default scan starts after this comment. Say check all reviews to rescan the full PR.

Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Outdated
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review: [Pro] Add buffered RSC rendering helpers for static pages

This PR adds buffered_stream_react_component and cached_buffered_stream_react_component — two new Pro helpers that run components through the streaming/RSC renderer but buffer all chunks before returning a single HTML string to Rails, avoiding the ActionController::Live response-commit constraint. The cache-key construction correctly includes both server bundle digest (via prerender: true / base_cache_key) and RSC bundle digest (via the explicit lambda), and the documentation additions (CSP/nonce caveat, Rails-context ordering, caching/streaming reference pages) are thorough. Error propagation through each_chunk, on_complete skip-on-failure, and raw_options immutability all look sound.

Three issues found, ranked by severity:


1. Test at line 1100 makes the cache-population assertion a tautology (test quality)

The manual Rails.cache.write(expected_cache_key, String.new(first_result), expires_in: 60) on line 1100 runs before the expect(Rails.cache.read(expected_cache_key)).to eq(first_result) assertion on line 1109 — so line 1109 only proves the manual write worked, not that fetch_react_component auto-populated the cache on the miss path. A key-computation bug (e.g., miss-path key ≠ hit-path key) would be invisible here.

The second test (lines 1114–1149) already covers auto-caching correctly (no manual write, direct read assertion). The fix for the first test is to move the Rails.cache.read assertion before the manual write, or drop the manual write entirely. See inline comment.


2. stub_pro_bundle_hashes injected into shared mock_request_and_response (scope creep)

Placing stub_pro_bundle_hashes inside mock_request_and_response (line 417) means it now runs for every existing stream_react_component test, not just the new buffered ones. The URL mock uses a regex so request matching still works, but the stub silently changes the value of ReactOnRailsPro::Utils.bundle_hash for any assertion that reads it after setup. It would be cleaner to call stub_pro_bundle_hashes only from the new buffered-test before blocks (or define a separate mock_request_and_response_with_bundle_stubs variant) so the scope change is explicit. See inline comment.


3. Cache-key asymmetry between cached_buffered_stream_react_component and cached_stream_react_component (pre-existing gap)

cached_buffered_stream_react_component correctly includes rsc_bundle_hash in its cache key when RSC is enabled; cached_stream_react_component does not (it only gets bundle_hash via base_cache_key(prerender: true)). This means an RSC bundle update invalidates buffered entries but leaves streaming entries stale. This is a pre-existing gap in fetch_stream_react_component, not introduced here, but it's now visible as an inconsistency between the two sibling helpers. Worth a follow-up issue. See inline comment.

@justin808

Copy link
Copy Markdown
Member Author

Address-review summary

Scan scope: review feedback after the previous summary on the current #4268 head.

Mattered

  • Addressed cache-population proof thread: 3ea95eb captures the cache entry immediately after the MISS render and before the manual plain-String write.
  • Addressed shared bundle-hash stub clarity: 3ea95eb documents why streaming helper specs use deterministic test hashes when generated SSR/RSC bundle files are absent locally.
  • Deferred cached_stream_react_component RSC hash asymmetry: pre-existing broader runtime behavior, not changed in this buffered-helper PR; no follow-up issue created without maintainer direction.

Skipped

  • None.

Deferred-work tracking: PR summary comment only for the pre-existing cached_stream_react_component cache-key asymmetry; no new issue created.

Next default scan starts after this comment. Say check all reviews to rescan the full PR.

…ered-rsc-rendering

* origin/main:
  Docs: record RSC Rspack client refs investigation (#4269)
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review: [Pro] Add buffered RSC rendering helpers for static pages

This PR adds two well-motivated helpers — buffered_stream_react_component and cached_buffered_stream_react_component — that let RSC/streaming rendering work without ActionController::Live. The implementation is structurally sound, the test coverage is solid, and the CSP/cache-tag documentation additions are accurate. Three issues below warrant attention before merge.


1. Cache key collision when RSC is disabled (bug — can cause NoMethodError in production)

When ReactOnRailsPro.configuration.enable_rsc_support is false, the cache key produced by cached_buffered_stream_react_component is identical to the one produced by cached_stream_react_component for the same component and user-supplied cache_key.

Both helpers ultimately call ReactOnRailsPro::Cache.react_component_cache_key(component_name, opts) with prerender: true and the raw user key as cache_key_value. When RSC is disabled the lambda in the buffered helper just returns the user key unchanged, so the two keys are:

["ror_component", ROR_VERSION, PRO_VERSION, bundle_hash, dep_key, "MyComp", "my-key"]   # both helpers

The problem: cached_stream_react_component stores an Array of chunks in the cache (via the on_complete write-through in handle_stream_cache_miss). cached_buffered_stream_react_component reads whatever is there and calls cached_result.html_safe (line 332). Array has no html_safe method → NoMethodError.

This can happen during a normal migration from streaming to buffered rendering when the developer keeps the same cache_key but switches helpers without clearing the cache.

Suggested fix: Differentiate the base key type for the buffered helper. The simplest path is to route cached_buffered_stream_react_component through a private method that passes "ror_buffered_component" as the type to base_cache_key, rather than reusing fetch_react_component which uses "ror_component".


2. on_complete: false raises NoMethodError (low-severity edge case)

Ruby's safe-navigation operator &. only short-circuits for nil, not false. If a caller passes on_complete: false (e.g. from a conditional expression), chunks = [] if on_complete leaves chunks as nil (assignment not taken), but then on_complete&.call(chunks)false.call(nil)NoMethodError: undefined method 'call' for false.

The same pattern exists in consumer_stream_async (pre-existing), but the buffered variant has no rescue block to absorb it. A guard like on_complete = nil unless on_complete.respond_to?(:call) near the top of the method would prevent this.


3. API reference: "Same caching options as cached_react_component" is inaccurate for cached_buffered_stream_react_component

docs/oss/api-reference/ruby-api-pro.md line 104 says:

Same caching options as cached_react_component.

cached_react_component does not force prerender: true (so the server bundle_hash is included only when the caller explicitly passes it), and it never appends rsc_bundle_hash. By contrast, cached_buffered_stream_react_component always forces prerender: true (adding server bundle_hash to every key) and, when enable_rsc_support is true, additionally wraps the user key in [user_key, rsc_bundle_hash]. The effective cache keys are materially different and the equivalence claim will mislead users who try to reason about key construction or cache sharing across helpers.

Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Outdated
Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Outdated
Comment thread docs/oss/api-reference/ruby-api-pro.md Outdated
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review: [Pro] Add buffered RSC rendering helpers for static pages

This PR introduces buffered_stream_react_component and cached_buffered_stream_react_component to serve RSC-rendered pages without committing the Rails response via ActionController::Live. The implementation cleanly mirrors the existing stream_react_component pattern, documentation is thorough, and the test coverage for the new cache helpers is solid. Two issues are worth addressing before merge.


Finding 1 — Cache type collision: NoMethodError when RSC support is disabled

File: react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb, line 332

When enable_rsc_support is false, the cache_key lambda in cached_buffered_stream_react_component returns the raw user-supplied key unchanged:

# lambda returns cache_key_value (e.g. "my-key") — same as what cached_stream_react_component writes
cache_key_value

ReactOnRailsPro::Cache.react_component_cache_key then produces the same cache key as fetch_stream_react_component would for the same component + user key. cached_stream_react_component writes an Array of chunk objects to that key; cached_buffered_stream_react_component reads that entry, gets an Array, and then calls cached_result.html_safe on it — Array has no html_safe method in Rails → NoMethodError in production on the first cache hit.

The fix is to use a distinct prefix for the buffered cache namespace. The simplest approach is passing a dedicated type string in base_cache_key. Alternatively, adding a type-check guard — cached_result.is_a?(String) ? cached_result.html_safe : cached_result — avoids the crash but masks the collision. The right fix is a distinct key prefix so the two cache stores never share entries.


Finding 2 — on_complete: fires on cache miss only, silently skipped on hit

File: react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb, lines 325–330

The cache-miss block does raw_options.merge(props: yield, ...) which forwards any on_complete: key from raw_options into buffered_stream_react_component, which then calls it after consuming all chunks. On a cache hit, fetch_react_component returns the cached string without yielding the block at all, so on_complete is never invoked.

This is low practical severity since on_complete is not documented as a supported option for cached_buffered_stream_react_component, but the asymmetry is invisible to a caller who infers the API from buffered_stream_react_component. A one-line fix strips the key before forwarding:

options = raw_options.except(:on_complete).merge(
  props: yield,
  skip_prerender_cache: true,
  auto_load_bundle: ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
)

Note — cached_stream_react_component is missing rsc_bundle_hash (pre-existing)

cached_buffered_stream_react_component correctly adds rsc_bundle_hash to its cache key when RSC is enabled (since the SSR bundle calls out to the RSC bundle via generateRSCPayload). cached_stream_react_component, by contrast, only includes the SSR bundle hash. An RSC-only bundle deploy will correctly bust buffered caches but leave streaming caches stale. This is a pre-existing gap not introduced by this PR, but it's worth tracking.

Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Outdated
@justin808

Copy link
Copy Markdown
Member Author

Address-review follow-up for the 2026-06-28 Claude threads on PR #4268.

Fixed in 48af3c234:

  • Namespaced cached_buffered_stream_react_component cache entries so buffered HTML cannot collide with regular streamed chunk-array entries when RSC support is disabled.
  • Treat buffered_stream_react_component(on_complete:) as callable only when it responds to call, covering on_complete: false.
  • Strip on_complete from cached buffered miss render options so cache hits and misses have consistent callback behavior.
  • Updated the Pro Ruby API docs to describe the actual cached buffered helper behavior: same key/tag/expiry options, forced prerender: true, buffered namespace, and optional RSC bundle digest when RSC support is enabled.

Resolved threads:

Local validation on 48af3c234:

  • bundle exec rspec spec/helpers/react_on_rails_pro_helper_spec.rb -e "buffers all streamed chunks into one normal Rails helper result" -e "ignores false on_complete values" -e "caches the fully buffered result without requiring the streaming view context" -e "does not require an RSC bundle hash when RSC support is disabled" -e "does not collide with cached stream entries when RSC support is disabled" -e "ignores on_complete on cache miss so hits and misses have the same callback behavior" from react_on_rails_pro/spec/dummy: 6 examples, 0 failures.
  • BUNDLE_GEMFILE="$(git rev-parse --show-toplevel)/Gemfile" bundle exec rubocop --ignore-parent-exclusion . from react_on_rails_pro: 240 files inspected, no offenses detected.
  • script/check-pro-license-headers: passed.
  • pnpm start format.listDifferent: passed.
  • script/check-docs-sidebar: passed.
  • git diff --check: passed.
  • script/ci-changes-detector origin/main: recommends Pro lint, Pro unit specs, Pro dummy integration, and Pro benchmarks; PR retains full-ci and benchmark labels.

No skipped review items from this pass.

…ered-rsc-rendering

* origin/main:
  [Pro] Test rejected RSC replacement retry notifications (#4250)
Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Outdated
@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review

Reviewed the buffered RSC rendering helpers (buffered_stream_react_component, cached_buffered_stream_react_component) and the centralized RSC-bundle-digest cache-key handling in ReactOnRailsPro::Cache.base_cache_key.

Summary: The design is sound. buffered_stream_react_component correctly avoids needing stream_view_containing_react_components/@async_barrier because StreamRequest#each_chunk already wraps itself in its own Sync { ... } block, so it's self-contained. Error handling is simpler (and correct) than the live-streaming path since nothing is written to the client until the full buffer is built — no partial-commit edge cases to worry about. The cache-key lambda in cached_buffered_stream_react_component resolves the user's cache_key proc exactly once (traced through fetch_react_componentReactOnRailsPro::Cache.react_component_cache_key), so no double-evaluation risk. The widened RSC-bundle-digest inclusion in base_cache_key is a deliberate, documented tradeoff (cold cache on every RSC-bundle deploy) and is consistently applied across cached_react_component, cached_react_component_hash, cached_stream_react_component, the new buffered helper, and the lower-level pro_rendering.rb fragment cache. Tests cover the new cache-key shape, the RSC-disabled path, and cache-collision avoidance between the buffered and streaming cache namespaces.

One minor finding (inline comment posted): cached_buffered_stream_react_component rejects the :on_complete option based on key presence rather than truthiness, unlike buffered_stream_react_component which tolerates falsy on_complete values (nil/false) as a no-op. This is a small inconsistency that could surprise callers sharing an options hash across both helpers — see inline comment for the specific line.

No correctness, security, or performance issues found beyond that nit. Docs/CHANGELOG/llms-full mirrors are consistent, and Pro license headers are untouched.

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache.rb Outdated
@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Summary

This PR adds buffered_stream_react_component/cached_buffered_stream_react_component (buffers the Pro streaming/RSC renderer's chunks into a single HTML string instead of committing through ActionController::Live), and widens ReactOnRailsPro::Cache.base_cache_key to include the RSC bundle digest in all prerendered Pro cache keys when RSC support is enabled. Docs and tests are updated to match.

I ran 8 finder passes (line-by-line, removed-behavior, cross-file, reuse/simplification/efficiency, altitude, conventions) followed by targeted verification agents on the two highest-signal candidates. Most candidates (buffer accumulation style, cache-key lambda wrapping, on_complete validation placement) turned out to be reasonable design choices rather than defects, and one — a suspected dropped response.stream.closed? disconnect check in buffered_stream_react_component — was refuted: that check only matters for ActionController::Live streaming, and this helper deliberately never touches the Live buffer (it returns complete HTML before Rails commits the response), so the comparison doesn't apply. All renderer-connection failure paths in StreamRequest/StreamDecorator raise rather than silently truncating, so cached output can't end up partial from that angle either.

One finding survived verification (posted as an inline comment on react_on_rails_pro/lib/react_on_rails_pro/cache.rb:192):

base_cache_key now calls ReactOnRailsPro::Utils.rsc_bundle_hash — which does an unrescued Digest::MD5.file(...) — for every prerender: true cache key whenever enable_rsc_support is globally true, regardless of whether the specific component actually renders through RSC. If the RSC bundle file is ever missing while the flag is on (build race, webpack misconfig limited to the RSC bundle), this raises for all cached rendering app-wide, not just RSC pages. I didn't find a startup check that guarantees the RSC bundle exists whenever enable_rsc_support is true. The PR description notes the cache-key widening was an accepted maintainer decision — this comment is specifically about the unrescued crash path, which seems like a separate robustness question worth confirming was intentional.

Everything else (helper mutation-safety via options.dup, cache-namespace collision avoidance between cached_stream_react_component and cached_buffered_stream_react_component, lazy props-block evaluation on cache hits, on_complete rejection for the cached buffered variant) checked out correctly against the tests and surrounding code.

@justin808

Copy link
Copy Markdown
Member Author

Address-review summary

Scan scope: review activity after 2026-07-01T07:45:08Z on PR #4268.

Mattered

  • Fixed missing-RSC-bundle cache-key crash risk: d84e7ed keeps the real RSC bundle digest in prerendered Pro cache keys when available and uses a stable rsc-bundle-missing sentinel only while the RSC bundle file is absent.
  • Fixed cached-hit auto_load_bundle normalization: d84e7ed uses the same normalized value for cache hits and cache misses, with a regression spec.
  • Fixed no-op on_complete handling: d84e7ed rejects callable callbacks for cached buffered rendering but allows nil and false, with regression coverage.
  • Replied to and resolved all four new current-head review threads.

Skipped

  • None.

Deferred-work tracking: none; all new current-head review findings were fixed in this PR.

Next default scan starts after this comment. Say check all reviews to rescan the full PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
react_on_rails_pro/lib/react_on_rails_pro/cache.rb (1)

222-229: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Missing-bundle fallback isn't memoized, so exceptions can repeat on every render.

ReactOnRailsPro::Utils.rsc_bundle_hash only caches @rsc_bundle_hash on success; when the RSC bundle file is absent, every call to rsc_bundle_cache_key re-triggers Digest::MD5.file and re-raises/rescues Errno::ENOENT. If enable_rsc_support is on but the bundle is persistently missing (e.g. misconfigured deploy), every prerendered cache-key computation pays this exception overhead on the request hot path.

Consider memoizing the sentinel result locally so the file-system probe/exception only happens once per process.

♻️ Proposed fix to memoize the missing-bundle fallback
       def rsc_bundle_cache_key
-        ReactOnRailsPro::Utils.rsc_bundle_hash
-      rescue Errno::ENOENT
-        RSC_BUNDLE_MISSING_CACHE_KEY
+        return `@rsc_bundle_cache_key` if defined?(`@rsc_bundle_cache_key`)
+
+        `@rsc_bundle_cache_key` = begin
+          ReactOnRailsPro::Utils.rsc_bundle_hash
+        rescue Errno::ENOENT
+          RSC_BUNDLE_MISSING_CACHE_KEY
+        end
       end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/lib/react_on_rails_pro/cache.rb` around lines 222 - 229,
The missing-bundle fallback in rsc_bundle_cache_key is not cached, so every call
can re-run ReactOnRailsPro::Utils.rsc_bundle_hash and repeat the Errno::ENOENT
rescue path. Memoize the sentinel result inside rsc_bundle_cache_key (or in
ReactOnRailsPro::Utils.rsc_bundle_hash) so the missing-bundle case is computed
once and reused, keeping the hot path from repeatedly hitting the
filesystem/exception flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/cache.rb`:
- Around line 222-229: The missing-bundle fallback in rsc_bundle_cache_key is
not cached, so every call can re-run ReactOnRailsPro::Utils.rsc_bundle_hash and
repeat the Errno::ENOENT rescue path. Memoize the sentinel result inside
rsc_bundle_cache_key (or in ReactOnRailsPro::Utils.rsc_bundle_hash) so the
missing-bundle case is computed once and reused, keeping the hot path from
repeatedly hitting the filesystem/exception flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d98a9dde-670c-402e-b41c-a5d86175602b

📥 Commits

Reviewing files that changed from the base of the PR and between ae9a810 and d84e7ed.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • docs/oss/building-features/caching.md
  • docs/pro/fragment-caching.md
  • llms-full-pro.txt
  • llms-full.txt
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache.rb
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
  • react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb
✅ Files skipped from review due to trivial changes (4)
  • CHANGELOG.md
  • docs/oss/building-features/caching.md
  • llms-full.txt
  • llms-full-pro.txt
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/pro/fragment-caching.md
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache.rb
@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Summary

Reviewed this PR at medium effort (line-by-line diff scan, removed-behavior audit, cross-file caller tracing, plus reuse/simplification/efficiency/altitude/conventions passes), including two targeted sub-agent investigations into the async/streaming machinery (each_chunk, Sync context requirements, exception handling) and the bundle_hash/rsc_bundle_hash memoization behavior.

What the PR does: Adds buffered_stream_react_component / cached_buffered_stream_react_component, which reuse the existing Pro streaming/RSC renderer but buffer all chunks into a single HTML string so static/cacheable pages can avoid ActionController::Live response commits. It also centralizes RSC bundle digest handling into ReactOnRailsPro::Cache.base_cache_key so prerendered Pro cache keys are invalidated correctly when the RSC bundle changes (with a rsc-bundle-missing sentinel for the pre-deploy window).

Verified as correct / not bugs (worth noting since they looked suspicious at first):

  • buffered_stream_react_component does not need to run inside stream_view_containing_react_components/Sync — both StreamRequest#each_chunk and the HTTP client self-wrap in Sync, so calling it from a plain controller/view works.
  • Chunks consumed via each_chunk are already fully composed HTML (console-replay script + Rails context handling included) — the buffered helper doesn't bypass that logic.
  • on_complete mutation-safety (html = buffer.join.html_safe; on_complete.call(buffer)) is correct and matches its test coverage.
  • bundle_hash/rsc_bundle_hash are both memoized per-process in production, so the new rsc_bundle_cache_key call in base_cache_key doesn't add meaningful per-request cost.
  • No hash-mutation bugs in cached_buffered_stream_react_component's option merging (all .merge calls, no accidental sharing between the cache-key closure and the render options).
  • +"" frozen-string workaround for the string-buffer path is correct given # frozen_string_literal: true.

Finding posted inline: base_cache_key's RSC-digest push (react_on_rails_pro/lib/react_on_rails_pro/cache.rb:182-192) is shared by more than the four helpers this PR's description calls out — it also feeds ProRendering.cache_key, the internal prerender_caching result cache, so every RSC-bundle deploy will cold-start that cache for all prerendered components (not just RSC ones) once enable_rsc_support is on. Likely intentional (mirrors the pre-existing bundle_hash behavior) but untested for that call site — flagging for confirmation.

No correctness, security, or data-integrity bugs found. Test coverage for the new helpers and cache-key behavior is thorough.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Pro Node Renderer Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
Pro Node Renderer: simple_eval (non-RSC) 2093.29 0.0% (2094.03) 4.32 ▼0.8% (4.36) 5.37 ▼4.5% (5.62) 200=62806
Pro Node Renderer: react_ssr (non-RSC) 1818.07 ▼1.5% (1845.45) 4.95 ▼0.4% (4.97) 6.18 ▲0.1% (6.17) 200=54544

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@justin808

Copy link
Copy Markdown
Member Author

Address-review summary

Scan scope: review activity after 2026-07-01T08:07:06Z on PR #4268.

Mattered

  • Resolved the final current-head base_cache_key blast-radius thread as intentional: the shared RSC digest/sentinel applies to all prerendered Pro cache keys, including the internal Pro prerender cache, so RSC bundle deploys invalidate the shared server-rendered output cache surface.
  • Recorded the decision in the PR body Codex Decision Log; no code change was required for this discussion item.

Skipped

  • None.

Deferred-work tracking: none.

Next default scan starts after this comment. Say check all reviews to rescan the full PR.

@justin808
justin808 added this pull request to the merge queue Jul 1, 2026
Merged via the queue into main with commit edb02c7 Jul 1, 2026
43 checks passed
@justin808
justin808 deleted the codex/issue-4263-buffered-rsc-rendering branch July 1, 2026 08:22
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Pro (shard 2/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/empty: Pro 1053.97 ▼12.4% (1202.75) 4.64 ▼20.7% (5.85) 6.36 ▼33.2% (9.52) 200=32047
/ssr_shell_error: Pro 166.32 ▲11.2% (149.62) 46.34 ▼3.0% (47.76) 70.22 ▼9.0% (77.2) 200=5029
/ssr_sync_error: Pro 169.14 ▲10.3% (153.4) 43.27 ▼6.3% (46.19) 65.65 ▼14.4% (76.72) 200=5116
/rsc_component_error: Pro 119.7 ▼12.7% (137.17) 37.58 ▼16.0% (44.71) 103.21 ▲31.8% (78.29) 200=4410
/non_existing_stream_react_component: Pro 191.16 ▲12.0% (170.7) 45.62 ▲13.7% (40.13) 62.37 ▼3.4% (64.58) 200=5776
/server_side_redux_app_cached: Pro 384.61 ▲8.2% (355.43) 20.95 ▲0.9% (20.77) 32.03 ▼8.0% (34.83) 200=11623
/loadable: Pro 164.17 ▲12.1% (146.44) 47.45 ▲2.8% (46.16) 70.53 ▼10.1% (78.48) 200=4963
/apollo_graphql: Pro 143.45 ▲9.9% (130.53) 42.03 ▼14.5% (49.16) 70.09 ▼13.9% (81.44) 200=4324,3xx=14
/console_logs_in_async_server: Pro 2.58 ▼21.0% (3.27) 2121.28 ▼0.1% (2123.53) 2146.92 ▼0.8% (2164.21) 200=94
/stream_error_demo: Pro 2.9 ▼11.7% (3.28) 2010.52 ▼0.1% (2011.56) 2045.15 ▼9.2% (2251.46) 200=94
/stream_async_components: Pro 157.21 ▲18.2% (132.97) 48.46 ▲0.8% (48.1) 78.84 ▼0.2% (79.03) 200=4738,3xx=16
/rsc_posts_page_over_http: Pro 150.74 ▲12.0% (134.62) 51.44 ▼2.0% (52.48) 82.88 ▼6.5% (88.62) 200=4556
/rsc_echo_props: Pro 66.64 ▲11.3% (59.89) 117.12 ▲0.7% (116.34) 161.35 ▼11.9% (183.07) 200=2019
/client_side_fouc_probe: Pro 297.67 ▼11.7% (337.25) 19.07 ▼7.2% (20.54) 43.05 ▲22.1% (35.26) 200=8997
/async_on_server_sync_on_client_client_render: Pro 350.45 ▲8.0% (324.53) 21.29 ▲1.4% (21.0) 34.07 ▼7.7% (36.93) 200=10592
/server_router_client_render: Pro 356.33 ▲6.5% (334.56) 17.74 ▼18.2% (21.7) 29.14 ▼14.6% (34.13) 200=10769
/unwrapped_rsc_route_stream_render: Pro 187.53 ▲11.5% (168.2) 41.03 ▼2.5% (42.09) 61.93 ▼10.4% (69.12) 200=5666
/async_render_function_returns_component: Pro 202.88 ▲11.2% (182.42) 26.99 ▼31.9% (39.64) 50.6 ▼19.9% (63.2) 200=6174
/native_metadata: Pro 187.83 ▲14.8% (163.66) 40.15 ▲0.8% (39.84) 59.63 ▼11.6% (67.49) 200=5667,3xx=13
/hybrid_metadata_streaming: Pro 185.46 ▲11.3% (166.63) 39.89 ▼5.7% (42.28) 60.59 ▼14.7% (71.02) 200=5608
/cache_demo: Pro 136.76 ▲8.5% (126.06) 54.8 ▲4.6% (52.39) 81.73 ▼6.6% (87.54) 200=4136
/client_side_hello_world: Pro 310.04 ▼8.3% (338.2) 18.35 ▼10.8% (20.57) 21.67 ▼37.5% (34.66) 200=9430
/client_side_hello_world_shared_store_controller: Pro 291.22 ▼8.4% (317.87) 19.31 ▼13.9% (22.43) 23.53 ▼35.9% (36.73) 200=8858
/server_side_hello_world_shared_store: Pro 129.66 ▲10.9% (116.95) 61.31 ▲0.4% (61.09) 91.75 ▼4.1% (95.7) 200=3920
/server_side_hello_world_shared_store_defer: Pro 131.78 ▲9.2% (120.72) 59.04 0.0% (59.02) 87.41 ▼6.6% (93.57) 200=3986
/server_side_hello_world_hooks: Pro 126.3 ▼30.2% (181.07) 26.43 ▼31.1% (38.36) 49.28 ▼20.0% (61.58) 200=6277
/server_side_log_throw: Pro 186.82 ▲12.5% (166.1) 40.44 ▼0.9% (40.79) 60.78 ▼7.2% (65.46) 200=5649
/source_mapped_prerender_error_probe: Pro 292.02 ▲13.9% (256.28) 26.81 ▼3.6% (27.82) 39.91 ▼8.6% (43.67) 3xx=8823
/server_side_log_throw_raise_invoker: Pro 425.05 ▲6.8% (398.16) 18.11 ▲1.5% (17.84) 26.88 ▼7.5% (29.06) 200=12844
/server_side_redux_app: Pro 181.07 ▲11.7% (162.17) 33.18 ▼22.1% (42.62) 63.85 ▼19.8% (79.58) 200=5478,3xx=2
/server_side_redux_app_cached: Pro 380.75 ▲7.1% (355.43) 21.03 ▲1.2% (20.77) 32.45 ▼6.8% (34.83) 200=11504
/render_js: Pro 400.09 ▲8.2% (369.71) 14.16 ▼28.2% (19.71) 26.38 ▼18.3% (32.29) 200=12170
/pure_component: Pro 198.12 ▲10.4% (179.45) 38.65 ▼2.2% (39.54) 61.19 ▼7.6% (66.22) 200=5989
/turbolinks_cache_disabled: Pro 365.54 ▲4.7% (349.14) 23.54 ▲20.6% (19.53) 32.45 ▼0.9% (32.75) 200=11046
/xhr_refresh: Pro 118.37 ▼4.0% (123.36) 66.02 ▲13.2% (58.32) 94.89 ▲5.2% (90.22) 200=3579
/broken_app: Pro 174.89 ▼2.3% (179.04) 36.2 ▼9.0% (39.78) 60.57 ▼7.3% (65.37) 200=5295
/server_render_with_timeout: Pro 66.45 ▲7.8% (61.63) 116.45 ▲2.7% (113.34) 129.89 ▲1.4% (128.04) 200=2021

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Core Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Core 5.25 🟢 52.3% (3.45) 1538.76 ▼29.9% (2194.81) 1918.22 ▼37.9% (3087.87) 200=165
/client_side_hello_world: Core 1147.57 🟢 74.0% (659.55) 6.09 ▼36.3% (9.56) 10.98 ▼42.3% (19.04) 200=34668
/client_side_rescript_hello_world: Core 1152.94 🟢 73.6% (663.98) 6.05 ▼37.1% (9.62) 12.59 ▼33.3% (18.88) 200=34824
/client_side_hello_world_shared_store: Core 1054.22 ▲66.4% (633.54) 6.14 ▼39.5% (10.15) 13.61 ▼34.4% (20.74) 200=31849
/client_side_hello_world_shared_store_controller: Core 1065.79 🟢 69.6% (628.24) 6.1 ▼37.7% (9.8) 13.53 ▼34.2% (20.56) 200=32195
/client_side_hello_world_shared_store_defer: Core 1038.96 🟢 65.3% (628.39) 6.22 ▼37.5% (9.96) 13.89 ▼31.7% (20.32) 200=31384
/server_side_hello_world_shared_store: Core 23.4 🟢 55.7% (15.03) 245.63 ▼52.3% (515.06) 439.6 ▼36.8% (695.92) 200=714
/server_side_hello_world_shared_store_controller: Core 23.54 🟢 59.5% (14.76) 266.9 ▼46.5% (499.09) 412.21 ▼44.8% (746.56) 200=720
/server_side_hello_world_shared_store_defer: Core 23.25 🟢 55.4% (14.96) 239.45 ▼52.4% (502.9) 440.67 ▼36.5% (694.01) 200=711
/server_side_hello_world: Core 37.1 ▲22.5% (30.28) 159.97 ▼36.8% (253.31) 203.64 ▼35.3% (314.52) 200=1128
/server_side_hello_world_hooks: Core 45.4 🟢 49.6% (30.36) 130.11 🟢 49.5% (257.85) 202.97 ▼36.5% (319.66) 200=1385
/server_side_hello_world_props: Core 46.64 🟢 58.2% (29.48) 179.08 ▼31.1% (260.08) 217.29 ▼31.7% (318.15) 200=1412
/client_side_log_throw: Core 842.0 ▲30.9% (643.44) 5.42 ▼44.5% (9.77) 8.05 ▼56.8% (18.64) 200=25606
/server_side_log_throw: Core 44.65 🟢 52.5% (29.27) 132.17 ▼48.7% (257.77) 208.17 ▼35.3% (321.64) 200=1363
/server_side_log_throw_plain_js: Core 45.17 🟢 50.7% (29.97) 138.96 ▼45.4% (254.33) 208.71 ▼34.9% (320.47) 200=1372
/server_side_log_throw_raise: Core 38.25 ▲28.2% (29.84) 123.96 ▼51.7% (256.79) 167.94 ▼47.7% (321.18) 3xx=1169
/server_side_log_throw_raise_invoker: Core 973.25 ▲26.0% (772.58) 5.87 ▼29.2% (8.29) 11.41 ▼27.6% (15.75) 200=29400
/server_side_hello_world_es5: Core 44.74 🟢 51.1% (29.61) 187.32 ▼26.2% (253.71) 222.54 ▼30.5% (320.27) 200=1358
/server_side_redux_app: Core 37.35 ▲31.8% (28.35) 156.42 ▼38.0% (252.45) 186.21 ▼41.9% (320.47) 200=1137
/server_side_hello_world_with_options: Core 36.82 ▲26.6% (29.09) 124.59 ▼51.0% (254.17) 184.29 ▼42.3% (319.57) 200=1127
/server_side_redux_app_cached: Core 1194.48 🟢 78.2% (670.33) 5.97 ▼41.6% (10.22) 10.75 ▼40.7% (18.13) 200=36080
/client_side_manual_render: Core 1130.26 🟢 68.4% (671.24) 5.81 ▼38.4% (9.43) 12.57 ▼32.7% (18.67) 200=34140
/render_js: Core 50.46 🟢 58.6% (31.81) 169.08 ▼27.6% (233.63) 204.02 ▼31.9% (299.71) 200=1529
/react_router: Core 44.23 🟢 59.0% (27.82) 133.26 ▼50.5% (269.18) 208.14 ▼38.7% (339.59) 200=1351
/pure_component: Core 48.05 🟢 58.0% (30.41) 174.66 ▼29.5% (247.65) 200.96 ▼35.7% (312.77) 200=1460
/react_compiler_example: Core 46.48 🟢 55.6% (29.86) 126.68 ▼49.7% (251.74) 200.13 ▼36.8% (316.56) 200=1416
/css_modules_images_fonts_example: Core 37.17 ▲25.1% (29.71) 122.73 ▼52.0% (255.92) 184.82 ▼41.3% (315.07) 200=1136
/turbolinks_cache_disabled: Core 1120.52 🟢 64.6% (680.6) 5.78 ▼37.0% (9.18) 12.76 ▼30.9% (18.48) 200=33850
/rendered_html: Core 38.69 ▲30.5% (29.64) 137.83 ▼45.8% (254.41) 165.63 ▼48.2% (319.59) 200=1181
/xhr_refresh: Core 24.2 🟢 56.6% (15.45) 351.66 ▼28.7% (493.33) 451.14 ▼32.0% (662.97) 200=738
/react_helmet: Core 45.03 🟢 51.4% (29.74) 186.57 ▼26.9% (255.2) 213.34 ▼33.5% (320.57) 200=1368
/broken_app: Core 44.96 🟢 55.1% (28.99) 189.66 ▼27.4% (261.33) 220.27 ▼32.0% (324.13) 200=1359
/image_example: Core 35.45 ▲21.2% (29.25) 166.97 ▼36.2% (261.84) 202.73 ▼37.6% (324.91) 200=1083
/font_optimization_example: Core 933.6 ▲21.5% (768.54) 6.09 ▼28.1% (8.47) 18.46 ▲12.9% (16.34) 200=28202
/client_side_activity: Core 1153.32 🟢 76.7% (652.87) 5.97 ▼38.4% (9.7) 10.96 ▼43.4% (19.37) 200=34839
/server_side_activity: Core 37.18 ▲23.5% (30.11) 146.48 ▼41.4% (249.95) 164.82 ▼48.5% (320.28) 200=1140
/turbo_frame_tag_hello_world: Core 1306.79 🟢 73.1% (754.73) 4.4 ▼48.5% (8.54) 8.25 ▼51.0% (16.83) 200=39478
/manual_render_test: Core 1048.56 ▲55.8% (672.89) 2.61 ▼72.5% (9.49) 11.21 ▼37.0% (17.79) 200=31889
/root_error_callbacks: Core 37.6 ▲24.2% (30.26) 156.53 ▼37.9% (252.21) 245.82 ▼21.4% (312.91) 200=1141
/hydration_scheduling: Core 13.06 ▲25.5% (10.4) 451.86 ▼37.7% (725.86) 725.31 ▼31.0% (1051.82) 200=402
/rails_form: Core 1131.42 ▲65.1% (685.11) 6.08 ▼34.6% (9.3) 11.27 ▼37.4% (18.0) 200=34178
/typed_rails_action: Core 1049.22 ▲56.0% (672.62) 6.08 ▼36.8% (9.62) 13.87 ▼33.0% (20.7) 200=31697

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Pro (shard 1/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Pro 41.71 ▼11.4% (47.05) 179.22 ▲13.6% (157.74) 245.67 ▲15.8% (212.11) 200=1264
/error_scenarios_hub: Pro 315.39 ▼10.3% (351.65) 24.84 ▲20.4% (20.63) 38.05 ▲15.8% (32.85) 200=9529
/ssr_async_error: Pro 2.71 ▼17.7% (3.29) 2011.39 0.0% (2011.45) 2038.52 ▼3.9% (2120.61) 200=94
/ssr_async_prop_error: Pro 1.26 ▲4.8% (1.2) 5017.24 ▼0.1% (5020.86) 5391.19 ▼50.5% (10881.7) 200=45
/non_existing_react_component: Pro 160.65 ▼9.2% (176.85) 48.78 ▲20.0% (40.66) 70.72 ▼4.1% (73.71) 200=4856
/non_existing_rsc_payload: Pro 137.14 ▼23.7% (179.67) 42.83 ▲11.8% (38.3) 86.67 ▲21.6% (71.28) 200=4148
/cached_react_helmet: Pro 314.93 ▼12.0% (357.73) 24.85 ▲21.7% (20.42) 37.25 ▲11.9% (33.28) 200=9515
/cached_redux_component: Pro 262.78 ▼27.9% (364.56) 21.79 ▲12.3% (19.4) 26.16 ▼17.6% (31.76) 200=7993
/lazy_apollo_graphql: Pro 128.49 ▼10.1% (143.0) 67.49 ▲38.3% (48.81) 95.48 ▲23.5% (77.28) 200=3887
/redis_receiver: Pro 87.69 ▼9.4% (96.78) 84.61 ▲22.4% (69.14) 152.36 ▲5.7% (144.18) 200=2651
/stream_shell_error_demo: Pro 122.06 ▼15.0% (143.55) 55.12 ▲26.8% (43.47) 81.13 ▲6.2% (76.41) 200=3966,3xx=20
/test_incremental_rendering: Pro 3.76 ▲17.2% (3.21) 2009.23 0.0% (2009.57) 2024.02 ▼9.6% (2238.14) 200=122
/rsc_posts_page_over_redis: Pro 77.68 ▼19.9% (97.0) 76.5 ▲3.8% (73.73) 168.94 ▲44.9% (116.61) 200=2353
/rsc_fouc_probe: Pro 134.84 ▼13.8% (156.37) 63.7 ▲45.6% (43.74) 89.61 ▲25.0% (71.68) 200=4127
/async_on_server_sync_on_client: Pro 2.2 ▲4.6% (2.1) 3012.06 ▲0.1% (3010.45) 3035.16 ▼33.9% (4589.67) 200=73
/server_router: Pro 116.38 ▼26.5% (158.26) 40.1 ▼9.8% (44.46) 62.75 ▼16.1% (74.83) 200=3544
/unwrapped_rsc_route_client_render: Pro 310.68 ▼12.9% (356.58) 20.36 ▲5.6% (19.28) 33.52 ▲4.4% (32.11) 200=9391
/async_render_function_returns_string: Pro 150.91 ▼9.5% (166.68) 50.6 ▲27.8% (39.59) 74.18 ▲16.2% (63.86) 200=4545,3xx=27
/async_components_demo: Pro 6.63 ▼0.5% (6.66) 1025.33 0.0% (1025.84) 1071.5 ▲0.7% (1063.67) 200=211
/stream_native_metadata: Pro 128.01 ▼23.8% (168.0) 45.94 ▲9.4% (41.98) 100.0 ▲43.8% (69.53) 200=3871
/rsc_native_metadata: Pro 8.62 ▲24.3% (6.93) 1010.94 0.0% (1010.61) 1026.12 ▼1.0% (1036.46) 200=271
/react_intl_rsc_demo: Pro 77.42 ▼10.8% (86.82) 73.15 ▼4.7% (76.77) 155.18 ▲24.5% (124.68) 200=2344
/client_side_hello_world_shared_store: Pro 296.2 ▼9.6% (327.52) 25.79 ▲20.8% (21.35) 40.92 ▲18.5% (34.53) 200=8949
/client_side_hello_world_shared_store_defer: Pro 302.42 ▼7.6% (327.27) 25.01 ▲13.8% (21.99) 37.46 ▲7.2% (34.93) 200=9142
/server_side_hello_world_shared_store_controller: Pro 68.62 ▼35.6% (106.49) 67.18 ▲10.3% (60.9) 95.87 ▲0.7% (95.22) 200=3325,3xx=27
/server_side_hello_world: Pro 137.98 ▼20.7% (174.0) 41.81 ▲2.7% (40.69) 55.44 ▼12.9% (63.68) 200=4199
/client_side_log_throw: Pro 271.65 ▼24.6% (360.14) 21.28 ▲5.6% (20.15) 32.8 ▲1.3% (32.39) 200=8208
/server_side_log_throw_plain_js: Pro 319.92 ▼12.3% (364.66) 24.61 ▲27.3% (19.33) 35.33 ▲11.4% (31.73) 200=9670
/server_side_log_throw_raise: Pro 224.6 ▼3.2% (231.99) 33.66 ▲16.3% (28.94) 54.06 ▲10.0% (49.14) 3xx=6746
/server_side_hello_world_es5: Pro 164.96 ▼7.4% (178.23) 47.34 ▲22.2% (38.73) 68.29 ▲4.1% (65.62) 200=4986
/server_side_hello_world_with_options: Pro 176.39 ▼2.6% (181.07) 44.28 ▲11.5% (39.71) 64.22 ▲0.9% (63.63) 200=5330
/client_side_manual_render: Pro 339.66 ▼3.9% (353.57) 22.74 ▲16.9% (19.46) 35.81 ▲11.5% (32.11) 200=10266
/react_router: Pro 174.52 ▼0.2% (174.88) 35.32 ▼7.4% (38.15) 64.32 ▲3.4% (62.21) 200=5253,3xx=24
/css_modules_images_fonts_example: Pro 190.31 ▲7.4% (177.26) 39.93 ▲2.4% (39.01) 58.63 ▼10.3% (65.34) 200=5757
/rendered_html: Pro 193.47 ▲3.9% (186.23) 40.31 ▲6.1% (37.99) 59.19 ▼3.7% (61.5) 200=5814
/react_helmet: Pro 98.59 ▼15.4% (116.57) 75.93 ▲23.4% (61.51) 94.2 ▼6.5% (100.72) 200=2988
/image_example: Pro 134.14 ▼21.3% (170.43) 42.91 ▲13.5% (37.8) 74.75 ▲18.5% (63.05) 200=4057
/posts_page: Pro 127.3 ▼4.2% (132.94) 58.87 ▲7.5% (54.76) 84.49 ▼1.5% (85.74) 200=3852

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmark full-ci ready-for-hosted-ci Run optimized hosted GitHub CI for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support cacheable buffered rendering for static RSC pages

1 participant