Skip to content

Instantly share code, notes, and snippets.

@Schniz
Last active January 27, 2021 21:27
Show Gist options
  • Save Schniz/33179cc0f32a2d10f8bfe23cfd7785cb to your computer and use it in GitHub Desktop.
Save Schniz/33179cc0f32a2d10f8bfe23cfd7785cb to your computer and use it in GitHub Desktop.
πŸŽ’ Asset preloading bag in Rails

Usage

Add the following line to your ApplicationController:

around_action AssetPreloadingBag::Filter

And when you want to prerender something, you can simply call:

AssetPreloadingBag::Asset.as_fetch('/my-resource') # for instance

This works quite well with ViewComponent, imagine the following IconComponent which lazy loads SVG images as inline SVGs:

class IconComponent < ViewComponent::Base
  def initialize(name)
    @path = asset_pack_url("media/images/#{name}.svg")
  end

  def before_render
    AssetPreloadingBag::Asset.as_fetch(@path).register!
  end
end
<my-app-icon path="<%= @path %>"></my-app-icon>

Now instead of having a waterfall of requests, we tell the browser all your assets, or even better - inform our CDN to send these assets using HTTP/2 push.

module AssetPreloadingBag
class Asset < Struct.new(:path, :as, keyword_init: true)
def self.as_style(path)
new(path: path, as: :style)
end
def self.as_fetch(path)
new(path: path, as: :fetch)
end
def self.as_script(path)
new(path: path, as: :script)
end
def to_header_part
"<#{path}>; rel=preload; as=#{as}; crossorigin"
end
def register!
Container.push(self)
end
end
end
module AssetPreloadingBag
class Container
attr_reader :links
def initialize
@links = Set.new
end
def push(value)
links << value
self
end
def to_header
links.map(&:to_header_part).join(', ')
end
def apply_to_response(response)
response.headers['Link'] = to_header unless links.empty?
end
def self.get
Thread.current.fetch(:current_render_links)
end
def self.create
Thread.current[:current_render_links] = new
end
def self.clear
Thread.current[:current_render_links] = nil
end
def self.push(...)
get.push(...)
end
end
end
module AssetPreloadingBag
# Creates a new preloading bag container for this rendering phase.
# Then it collects, and sets the Link header
module Filter
def self.around(controller)
Container.create
yield
Container.get.apply_to_response(controller.response)
Container.clear
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment