From a9012af6883284a35bc5e6166507d429fe10482d Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 28 Jul 2020 11:57:36 +0200 Subject: [PATCH] Automatically set Link header for each stylesheet and script elements[0] can be serialized in `Link` headers[1] to allow the browser to preload them before it parsed the HTML body. It is particularly useful for scripts included at the bottom of the document. [0] https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content [1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link --- .../action_view/helpers/asset_tag_helper.rb | 32 ++++++++++------ actionview/test/abstract_unit.rb | 2 +- .../test/template/asset_tag_helper_test.rb | 38 ++++++++++++++----- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 91794fe4e4..cbf64867aa 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -86,11 +86,11 @@ module ActionView def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys - early_hints_links = [] + preload_links = [] sources_tags = sources.uniq.map { |source| href = path_to_javascript(source, path_options) - early_hints_links << "<#{href}>; rel=preload; as=script" + preload_links << "<#{href}>; rel=preload; as=script" tag_options = { "src" => href }.merge!(options) @@ -100,7 +100,7 @@ module ActionView content_tag("script", "", tag_options) }.join("\n").html_safe - request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request + send_preload_links_header(preload_links) sources_tags end @@ -136,11 +136,11 @@ module ActionView def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys - early_hints_links = [] + preload_links = [] sources_tags = sources.uniq.map { |source| href = path_to_stylesheet(source, path_options) - early_hints_links << "<#{href}>; rel=preload; as=style" + preload_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", @@ -149,7 +149,7 @@ module ActionView tag(:link, tag_options) }.join("\n").html_safe - request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request + send_preload_links_header(preload_links) sources_tags end @@ -281,12 +281,12 @@ module ActionView crossorigin: crossorigin }.merge!(options.symbolize_keys)) - early_hints_link = "<#{href}>; rel=preload; as=#{as_type}" - early_hints_link += "; type=#{mime_type}" if mime_type - early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin - early_hints_link += "; nopush" if nopush + preload_link = "<#{href}>; rel=preload; as=#{as_type}" + preload_link += "; type=#{mime_type}" if mime_type + preload_link += "; crossorigin=#{crossorigin}" if crossorigin + preload_link += "; nopush" if nopush - request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request + send_preload_links_header([preload_link]) link_tag end @@ -482,6 +482,16 @@ module ActionView type end end + + def send_preload_links_header(preload_links) + if respond_to?(:request) && request + request.send_early_hints("Link" => preload_links.join("\n")) + end + + if respond_to?(:response) && response + response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",") + end + end end end end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index ddd34da7bd..c7e3f0bc02 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -84,7 +84,7 @@ class RoutedRackApp end class BasicController - attr_accessor :request + attr_accessor :request, :response def config @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index efef74ca00..a59929a2ea 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -9,23 +9,33 @@ ActionView::Template::Types.delegate_to Mime class AssetTagHelperTest < ActionView::TestCase tests ActionView::Helpers::AssetTagHelper - attr_reader :request + attr_reader :request, :response + + class FakeRequest + attr_accessor :script_name + def protocol() "http://" end + def ssl?() false end + def host_with_port() "localhost" end + def base_url() "http://www.example.com" end + def send_early_hints(links) end + end + + class FakeResponse + def headers + @headers ||= {} + end + end def setup super @controller = BasicController.new - @request = Class.new do - attr_accessor :script_name - def protocol() "http://" end - def ssl?() false end - def host_with_port() "localhost" end - def base_url() "http://www.example.com" end - def send_early_hints(links) end - end.new - + @request = FakeRequest.new @controller.request = @request + + @response = FakeResponse.new + @controller.response = @response end def url_for(*args) @@ -499,6 +509,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(), javascript_include_tag("foo.js") end + + def test_should_set_preload_links + stylesheet_link_tag("http://example.com/style.css") + javascript_include_tag("http://example.com/all.js") + expected = "; rel=preload; as=style,; rel=preload; as=script" + assert_equal expected, @response.headers["Link"] + end + def test_image_path ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end