Skip to content

Commit bc90695

Browse files
AbanoubGhadbanalexeyr-ci2Judahmeek
authored
Add configurable loading strategy for generated component packs (#1712)
* Add configurable loading strategy for generated component packs - Replace `defer_generated_component_packs` with more flexible `generated_component_packs_loading_strategy` - Add support for `:async`, `:defer`, and `:sync` loading strategies - Validate loading strategy based on Shakapacker version - Update helper to support new loading strategy configuration - Add comprehensive specs for the new configuration option * Update CHANGELOG and release notes of v15.0.0 * Update generated component pack loading strategy test for CI compatibility * Add force_load option to redux_store method at ReactOnRails::Controller * Add packer version environment variable to CI workflows * Refactor CI packer version environment variable handling in workflows --------- Co-authored-by: Alexey Romanov <[email protected]> Co-authored-by: Judah Meek <[email protected]>
1 parent 99a2d71 commit bc90695

File tree

16 files changed

+245
-28
lines changed

16 files changed

+245
-28
lines changed

.github/workflows/examples.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ jobs:
9595
fi
9696
- name: Increase the amount of inotify watchers
9797
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
98+
- name: Set packer version environment variable
99+
run: |
100+
echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV
98101
- name: Main CI
99102
if: steps.changed-files.outputs.any_changed == 'true'
100103
run: bundle exec rake run_rspec:${{ matrix.versions == 'oldest' && 'web' || 'shaka' }}packer_examples

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ jobs:
190190
git config user.name "Your Name"
191191
git commit -am "stop generators from complaining about uncommitted code"
192192
- run: cd spec/dummy && bundle info shakapacker
193+
- name: Set packer version environment variable
194+
run: |
195+
echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV
193196
- name: Main CI
194197
run: bundle exec rake run_rspec:all_dummy
195198
- name: Store test results

.github/workflows/rspec-package-specs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
git commit -am "stop generators from complaining about uncommitted code"
5050
- name: Set packer version environment variable
5151
run: |
52-
echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV
52+
echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV
5353
- name: Run rspec tests
5454
run: bundle exec rspec spec/react_on_rails
5555
- name: Store test results

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
2323

2424
Changes since the last non-beta release.
2525

26+
#### Added
27+
28+
- Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
29+
30+
### Removed (Breaking Changes)
31+
32+
- Deprecated `defer_generated_component_packs` configuration option. You should use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
33+
2634
### [15.0.0-alpha.2] - 2025-03-07
2735

2836
See [Release Notes](docs/release-notes/15.0.0.md) for full details.

Gemfile.development_dependencies

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
gem "shakapacker", "8.0.0"
3+
gem "shakapacker", "8.2.0"
44
gem "bootsnap", require: false
55
gem "rails", "~> 7.1"
66

Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ GEM
336336
rexml (~> 3.2, >= 3.2.5)
337337
rubyzip (>= 1.2.2, < 3.0)
338338
websocket (~> 1.0)
339-
semantic_range (3.0.0)
340-
shakapacker (8.0.0)
339+
semantic_range (3.1.0)
340+
shakapacker (8.2.0)
341341
activesupport (>= 5.2)
342342
package_json
343343
rack-proxy (>= 0.6.1)
@@ -431,7 +431,7 @@ DEPENDENCIES
431431
scss_lint
432432
sdoc
433433
selenium-webdriver (= 4.9.0)
434-
shakapacker (= 8.0.0)
434+
shakapacker (= 8.2.0)
435435
spring (~> 4.0)
436436
sprockets (~> 4.0)
437437
sqlite3 (~> 1.6)

docs/release-notes/15.0.0.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,27 @@ Major improvements to component and store hydration:
2121
- Can use `async` scripts in the page with no fear of race condition
2222
- No need to use `defer` anymore
2323

24+
### Enhanced Script Loading Strategies
25+
26+
- New configuration option `generated_component_packs_loading_strategy` replaces `defer_generated_component_packs`
27+
- Supports three loading strategies:
28+
- `:async` - Loads scripts asynchronously (default for Shakapacker ≥ 8.2.0)
29+
- `:defer` - Defers script execution until after page load (doesn't work well with Streamed HTML as it will wait for the full page load before hydrating the components)
30+
- `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (better to upgrade to Shakapacker 8.2.0 and use `:async` strategy)
31+
- Improves page performance by optimizing how component packs are loaded
32+
2433
## Breaking Changes
2534

2635
### Component Hydration Changes
2736

28-
- The `defer_generated_component_packs` and `force_load` configurations now default to `false` and `true` respectively. This means components will hydrate early without waiting for the full page load. This improves performance by eliminating unnecessary delays in hydration.
37+
- The `defer_generated_component_packs` configuration has been deprecated. Use `generated_component_packs_loading_strategy` instead.
38+
- The `generated_component_packs_loading_strategy` defaults to `:async` for Shakapacker ≥ 8.2.0 and `:sync` for Shakapacker < 8.2.0.
39+
- The `force_load` configuration now defaults to `true`.
40+
- The new default values of `generated_component_packs_loading_strategy: :async` and `force_load: true` work together to optimize component hydration. Components now hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML).
2941

3042
- The previous need for deferring scripts to prevent race conditions has been eliminated due to improved hydration handling. Making scripts not defer is critical to execute the hydration scripts early before the page is fully loaded.
3143
- The `force_load` configuration makes `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load.
32-
- If you want to keep the previous behavior, you can set `defer_generated_component_packs: true` or `force_load: false` in your `config/initializers/react_on_rails.rb` file.
44+
- If you want to keep the previous behavior, you can set `generated_component_packs_loading_strategy: :defer` or `force_load: false` in your `config/initializers/react_on_rails.rb` file.
3345
- You can also keep it for individual components by passing `force_load: false` to `react_component` or `stream_react_component`.
3446
- Redux store now supports `force_load` option, which defaults to `config.force_load` (and so to `true` if that isn't set). If `true`, the Redux store will hydrate immediately as soon as its server-side data reaches the client.
3547
- You can override this behavior for individual Redux stores by calling the `redux_store` helper with `force_load: false`, same as `react_component`.
@@ -50,6 +62,12 @@ Major improvements to component and store hydration:
5062

5163
- If you call it in a `turbolinks:load` listener to work around the issue documented in [Turbolinks](https://www.shakacode.com/react-on-rails/docs/rails/turbolinks/#async-script-loading), the listener can be safely removed.
5264

65+
### Script Loading Strategy Migration
66+
67+
- If you were previously using `defer_generated_component_packs: true`, use `generated_component_packs_loading_strategy: :defer` instead
68+
- If you were previously using `defer_generated_component_packs: false`, use `generated_component_packs_loading_strategy: :sync` instead
69+
- For optimal performance with Shakapacker ≥ 8.2.0, consider using `generated_component_packs_loading_strategy: :async`
70+
5371
## Store Dependencies for Components
5472

5573
When using Redux stores with multiple components, you need to explicitly declare store dependencies to optimize hydration. Here's how:

lib/react_on_rails/configuration.rb

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def self.configuration
4949
# Maximum time in milliseconds to wait for client-side component registration after page load.
5050
# If exceeded, an error will be thrown for server-side rendered components not registered on the client.
5151
# Set to 0 to disable the timeout and wait indefinitely for component registration.
52-
component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT
52+
component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT,
53+
generated_component_packs_loading_strategy: nil
5354
)
5455
end
5556

@@ -60,23 +61,23 @@ class Configuration
6061
:generated_assets_dirs, :generated_assets_dir, :components_subdirectory,
6162
:webpack_generated_files, :rendering_extension, :build_test_command,
6263
:build_production_command, :i18n_dir, :i18n_yml_dir, :i18n_output_format,
63-
:i18n_yml_safe_load_options,
64+
:i18n_yml_safe_load_options, :defer_generated_component_packs,
6465
:server_render_method, :random_dom_id, :auto_load_bundle,
6566
:same_bundle_for_client_and_server, :rendering_props_extension,
6667
:make_generated_server_bundle_the_entrypoint,
67-
:defer_generated_component_packs, :force_load, :rsc_bundle_js_file,
68+
:generated_component_packs_loading_strategy, :force_load, :rsc_bundle_js_file,
6869
:react_client_manifest_file, :component_registry_timeout
6970

7071
# rubocop:disable Metrics/AbcSize
7172
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
7273
replay_console: nil, make_generated_server_bundle_the_entrypoint: nil,
73-
trace: nil, development_mode: nil,
74+
trace: nil, development_mode: nil, defer_generated_component_packs: nil,
7475
logging_on_server: nil, server_renderer_pool_size: nil,
7576
server_renderer_timeout: nil, raise_on_prerender_error: true,
7677
skip_display_none: nil, generated_assets_dirs: nil,
7778
generated_assets_dir: nil, webpack_generated_files: nil,
7879
rendering_extension: nil, build_test_command: nil,
79-
build_production_command: nil, defer_generated_component_packs: nil,
80+
build_production_command: nil, generated_component_packs_loading_strategy: nil,
8081
same_bundle_for_client_and_server: nil,
8182
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil,
8283
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
@@ -124,6 +125,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
124125
self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint
125126
self.defer_generated_component_packs = defer_generated_component_packs
126127
self.force_load = force_load
128+
self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy
127129
end
128130
# rubocop:enable Metrics/AbcSize
129131

@@ -139,6 +141,7 @@ def setup_config_values
139141
# check_deprecated_settings
140142
adjust_precompile_task
141143
check_component_registry_timeout
144+
validate_generated_component_packs_loading_strategy
142145
end
143146

144147
private
@@ -151,6 +154,42 @@ def check_component_registry_timeout
151154
raise ReactOnRails::Error, "component_registry_timeout must be a positive integer"
152155
end
153156

157+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
158+
def validate_generated_component_packs_loading_strategy
159+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
160+
161+
if defer_generated_component_packs
162+
if %i[async sync].include?(generated_component_packs_loading_strategy)
163+
Rails.logger.warn "**WARNING** ReactOnRails: config.defer_generated_component_packs is " \
164+
"superseded by config.generated_component_packs_loading_strategy"
165+
else
166+
Rails.logger.warn "[DEPRECATION] ReactOnRails: Use config." \
167+
"generated_component_packs_loading_strategy = :defer rather than " \
168+
"defer_generated_component_packs"
169+
self.generated_component_packs_loading_strategy ||= :defer
170+
end
171+
end
172+
173+
msg = <<~MSG
174+
ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \
175+
does not support async script loading, which may cause performance issues. Please either:
176+
1. Use :sync or :defer loading strategy instead of :async
177+
2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading
178+
MSG
179+
if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0])
180+
self.generated_component_packs_loading_strategy ||= :async
181+
elsif generated_component_packs_loading_strategy.nil?
182+
Rails.logger.warn("**WARNING** #{msg}")
183+
self.generated_component_packs_loading_strategy = :sync
184+
elsif generated_component_packs_loading_strategy == :async
185+
raise ReactOnRails::Error, "**ERROR** #{msg}"
186+
end
187+
188+
return if %i[async defer sync].include?(generated_component_packs_loading_strategy)
189+
190+
raise ReactOnRails::Error, "generated_component_packs_loading_strategy must be either :async, :defer, or :sync"
191+
end
192+
154193
def check_autobundling_requirements
155194
raise_missing_components_subdirectory if auto_load_bundle && !components_subdirectory.present?
156195
return unless components_subdirectory.present?

lib/react_on_rails/controller.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ module Controller
1212
#
1313
# Be sure to include view helper `redux_store_hydration_data` at the end of your layout or view
1414
# or else there will be no client side hydration of your stores.
15-
def redux_store(store_name, props: {})
15+
def redux_store(store_name, props: {}, force_load: nil)
16+
force_load = ReactOnRails.configuration.force_load if force_load.nil?
1617
redux_store_data = { store_name: store_name,
17-
props: props }
18+
props: props,
19+
force_load: force_load }
1820
@registered_stores_defer_render ||= []
1921
@registered_stores_defer_render << redux_store_data
2022
end

lib/react_on_rails/helper.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,13 @@ def load_pack_for_generated_component(react_component_name, render_options)
422422
is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name))
423423
raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present
424424
end
425-
append_javascript_pack_tag("generated/#{react_component_name}",
426-
defer: ReactOnRails.configuration.defer_generated_component_packs)
425+
426+
options = { defer: ReactOnRails.configuration.generated_component_packs_loading_strategy == :defer }
427+
# Old versions of Shakapacker don't support async script tags.
428+
# ReactOnRails.configure already validates if async loading is supported by the installed Shakapacker version.
429+
# Therefore, we only need to pass the async option if the loading strategy is explicitly set to :async
430+
options[:async] = true if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async
431+
append_javascript_pack_tag("generated/#{react_component_name}", **options)
427432
append_stylesheet_pack_tag("generated/#{react_component_name}")
428433
end
429434

spec/dummy/Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,8 @@ GEM
331331
rexml (~> 3.2, >= 3.2.5)
332332
rubyzip (>= 1.2.2, < 3.0)
333333
websocket (~> 1.0)
334-
semantic_range (3.0.0)
335-
shakapacker (8.0.0)
334+
semantic_range (3.1.0)
335+
shakapacker (8.2.0)
336336
activesupport (>= 5.2)
337337
package_json
338338
rack-proxy (>= 0.6.1)
@@ -423,7 +423,7 @@ DEPENDENCIES
423423
scss_lint
424424
sdoc
425425
selenium-webdriver (= 4.9.0)
426-
shakapacker (= 8.0.0)
426+
shakapacker (= 8.2.0)
427427
spring (~> 4.0)
428428
sprockets (~> 4.0)
429429
sqlite3 (~> 1.6)

spec/dummy/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"sass": "^1.43.4",
5151
"sass-loader": "^12.3.0",
5252
"sass-resources-loader": "^2.1.0",
53-
"shakapacker": "8.0.0",
53+
"shakapacker": "8.2.0",
5454
"style-loader": "^3.3.1",
5555
"terser-webpack-plugin": "5.3.1",
5656
"url-loader": "^4.0.0",

spec/dummy/spec/helpers/react_on_rails_helper_spec.rb

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,60 @@ class PlainReactOnRailsHelper
5050
allow(helper).to receive(:append_javascript_pack_tag)
5151
allow(helper).to receive(:append_stylesheet_pack_tag)
5252
expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error
53-
expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false })
53+
54+
if ENV["CI_PACKER_VERSION"] == "oldest"
55+
expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false })
56+
else
57+
expect(helper).to have_received(:append_javascript_pack_tag)
58+
.with("generated/component_name", { defer: false, async: true })
59+
end
5460
expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name")
5561
end
5662

63+
context "when async loading is enabled" do
64+
before do
65+
allow(ReactOnRails.configuration).to receive(:generated_component_packs_loading_strategy).and_return(:async)
66+
end
67+
68+
it "appends the async attribute to the script tag" do
69+
original_append_javascript_pack_tag = helper.method(:append_javascript_pack_tag)
70+
begin
71+
# Temporarily redefine append_javascript_pack_tag to handle the async keyword argument.
72+
# This is needed because older versions of Shakapacker (which may be used during testing)
73+
# don't support async loading, but we still want to test that the async option is passed
74+
# correctly when enabled.
75+
def helper.append_javascript_pack_tag(name, **options)
76+
original_append_javascript_pack_tag.call(name, **options)
77+
end
78+
79+
allow(helper).to receive(:append_javascript_pack_tag)
80+
allow(helper).to receive(:append_stylesheet_pack_tag)
81+
expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error
82+
expect(helper).to have_received(:append_javascript_pack_tag).with(
83+
"generated/component_name",
84+
{ defer: false, async: true }
85+
)
86+
expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name")
87+
ensure
88+
helper.define_singleton_method(:append_javascript_pack_tag, original_append_javascript_pack_tag)
89+
end
90+
end
91+
end
92+
93+
context "when defer loading is enabled" do
94+
before do
95+
allow(ReactOnRails.configuration).to receive(:generated_component_packs_loading_strategy).and_return(:defer)
96+
end
97+
98+
it "appends the defer attribute to the script tag" do
99+
allow(helper).to receive(:append_javascript_pack_tag)
100+
allow(helper).to receive(:append_stylesheet_pack_tag)
101+
expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error
102+
expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: true })
103+
expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name")
104+
end
105+
end
106+
57107
it "throws an error in development if generated component isn't found" do
58108
allow(Rails.env).to receive(:development?).and_return(true)
59109
expect { helper.load_pack_for_generated_component("nonexisting_component", render_options) }

spec/dummy/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6068,10 +6068,10 @@ sha.js@^2.4.0, sha.js@^2.4.8:
60686068
inherits "^2.0.1"
60696069
safe-buffer "^5.0.1"
60706070

6071-
shakapacker@8.0.0:
6072-
version "8.0.0"
6073-
resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.0.0.tgz#f29537c19078af7318758c92e7a1bca4cee96bdd"
6074-
integrity sha512-HCdpITzIKXzGEyUWQhKzPbpwwOsgTamaPH+0kXdhM59VQxZ3NWnT5cL3DlJdAT3sGsWCJskEl3eMkQlnh9DjhA==
6071+
shakapacker@8.2.0:
6072+
version "8.2.0"
6073+
resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.2.0.tgz#c7bed87b8be2ae565cfe616f68552be545c77e14"
6074+
integrity sha512-Ct7BFqJVnKbxdqCzG+ja7Q6LPt/PlB7sSVBfG5jsAvmVCADM05cuoNwEgYNjFGKbDzHAxUqy5XgoI9Y030+JKQ==
60756075
dependencies:
60766076
js-yaml "^4.1.0"
60776077
path-complete-extname "^1.0.0"

0 commit comments

Comments
 (0)