Skip to content

[Core] generated_stylesheet_hrefs_json runs for OSS apps on every auto-loaded component render, but only the Pro client reads data-generated-stylesheet-hrefs #4341

Description

@justin808

Summary

generated_stylesheet_hrefs_json (added in #4047) runs on every react_component render that uses auto_load_bundle, for OSS and Pro alike — but the attribute it emits, data-generated-stylesheet-hrefs, is consumed only by the Pro client package (packages/react-on-rails-pro/src/ClientSideRenderer.ts, FOUC reveal gating). For OSS users this is pure hot-path waste: in development (Shakapacker cache_manifest: false) each call re-reads and JSON-parses manifest.json per component per request; in production it computes hrefs and emits a JSON attribute that nothing ever reads.

Affected code

Introduced in

PR #4047 ("Fix Pro FOUC reveal gating"), merged 2026-06-15 (commit af6525d8b). Confirmed via git log --oneline -S "generated_stylesheet_hrefs_json" 43ed0e138..origin/main — single hit.

Mechanism

  1. Every react_component / react_component_hash call goes through internal_react_componentgenerate_component_script(render_options) (ProHelper is unconditionally included into ReactOnRails::Helper, so OSS apps run it too).
  2. generate_component_script computes the attribute for every auto-loaded component:
"data-generated-stylesheet-hrefs" =>
  generated_stylesheet_hrefs_json(render_options))
def generated_stylesheet_hrefs_json(render_options)
  return unless render_options.auto_load_bundle

  pack_name = "generated/#{render_options.react_component_name}"
  sources = preload_sources_for_stylesheet_pack(pack_name)
  hrefs = unique_preload_sources_by_href(sources).map { |source| source.fetch(:href) }
  hrefs.to_json if hrefs.present?
end
  1. preload_sources_for_stylesheet_packpreload_sources_for_packcurrent_shakapacker_instance.manifest.lookup_pack_with_chunks(pack_name, type: :stylesheet). With Shakapacker's cache_manifest: false (the development default), each lookup_pack_with_chunks call refreshes the manifest — a File.read + JSON.parse of manifest.json — so this happens once per auto-loaded component per request, on top of the lookups the framework already does for pack tags.
  2. The only reader of the resulting data-generated-stylesheet-hrefs attribute is ClientSideRenderer.ts in packages/react-on-rails-pro (GENERATED_STYLESHEET_HREFS_ATTRIBUTE, used by generatedStylesheetMatchesComponent for FOUC reveal gating). The OSS client package never reads it (verified by grep across packages/).

Failure scenario

Not a correctness failure — a per-render performance/waste regression:

  • OSS app in development with several auto-loaded components per page: each page render performs N extra full manifest.json read+parse cycles (manifest files in real apps are commonly hundreds of KB), inflating the dev feedback loop.
  • OSS app in production: extra manifest hash lookups, href de-duplication, to_json, and dead HTML bytes (a JSON array of stylesheet URLs) on every component spec tag — payload that no client code consumes.

Impact

  • P2 performance/cleanliness: measurable dev-mode overhead for OSS apps with auto_load_bundle, plus dead-weight HTML in production responses. No behavior break.
  • Pro apps pay the same dev-mode re-read cost per component even though a single per-request (or per pack name) memoization would suffice.

Suggested fix

Two independent, compatible changes:

  1. Early-return for OSS, since only the Pro client consumes the attribute:
def generated_stylesheet_hrefs_json(render_options)
  return unless ReactOnRails::Utils.react_on_rails_pro?
  return unless render_options.auto_load_bundle
  ...
end
  1. Memoize the href computation per pack name per request (mirroring other per-request caches in the helper):
def generated_stylesheet_hrefs_cache
  @generated_stylesheet_hrefs_cache ||= {}
end

def generated_stylesheet_hrefs_json(render_options)
  return unless ReactOnRails::Utils.react_on_rails_pro?
  return unless render_options.auto_load_bundle

  pack_name = "generated/#{render_options.react_component_name}"
  generated_stylesheet_hrefs_cache.fetch(pack_name) do
    sources = preload_sources_for_stylesheet_pack(pack_name)
    hrefs = unique_preload_sources_by_href(sources).map { |source| source.fetch(:href) }
    generated_stylesheet_hrefs_cache[pack_name] = hrefs.present? ? hrefs.to_json : nil
  end
end

(Helper instances are per-request in Rails, so an ivar cache is request-scoped.)

Test plan

  • Spec (OSS mode, react_on_rails_pro? stubbed false): render an auto-loaded component; assert the spec tag has no data-generated-stylesheet-hrefs attribute and preload_sources_for_stylesheet_pack is not called.
  • Spec (Pro mode): attribute still emitted with correct hrefs — existing Fix Pro FOUC reveal gating #4047 coverage in react_on_rails/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb should keep passing.
  • Spec: two components sharing a generated pack name in one render only trigger one manifest lookup (memoization).
  • Pro client test packages/react-on-rails-pro/tests/ClientSideRenderer.test.ts unaffected.
  • Run: cd react_on_rails && bundle exec rspec spec/dummy/spec/helpers/react_on_rails_helper_spec.rb.

Notes

  • Verified against origin/main 5264192cc by reading pro_helper.rb, helper.rb:526-565, and grepping all of packages/ for the attribute — consumers are only the Pro ClientSideRenderer.ts and its test. Confidence: high. The dev-mode re-read claim rests on Shakapacker's documented cache_manifest: false behavior (manifest refreshed on each lookup), not re-measured here.
  • A local fix session for this may already be queued (a task chip exists in the reporting workspace); this issue is the tracking artifact either way.
  • Related finding (same review): "[Core] add_rsc_stream_observability_to_render_options writes through to the options hash held by RenderOptions" (same helper hot path).

Found in a post-2026-06-12 code review of merged changes (review base 43ed0e1). Filed by Claude Code on behalf of Justin Gordon.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions