Responsive images (img srcset) with ActiveStorage

Here is what I ended up with to get responsive images with Active Storage. Anyone would have suggestions to clean this up or make it more generic? Would it be worth upstreaming this kind of helper in Active Storage?

# app/models/foo.rb
class Foo < ApplicationRecord
  has_one_attached :picture
end

# app/controllers/foos_controller.rb
class FoosController < ApplicationController
  def index
    @foo = Foo.find(...)
  end
end

# app/helpers/foo_helper.rb
class FooHelper
  def blob_thumb(blob, limit = [150, 150])
    return unless (blob_width, blob_height = blob_size(blob))
    width, height = limit
    if width < blob_width || height < blob_height
      blob.representation(resize_to_limit: limit)
    else
      blob
    end
  end

  def responsive_image_tag(blob, widths: [640, 1024, 1440, 1920], sizes: "100vw", **options)
    return unless analyze_blob(blob)
    image_tag(blob, srcset: srcset_for(blob, widths:), sizes:, **options)
  end

  def srcset_for(blob, widths:)
    return unless (blob_width, blob_height = blob_size(blob))
    widths
      .filter { |width| width <= blob_width }
      .tap { |w| w.append(blob_width) if blob_width < widths.max }
      .map do |width|
        height = (1.0 * width / blob_width * blob_height).to_i
        [rails_storage_proxy_url(blob_thumb(blob, [width, height])), "#{width}w"]
      end
  end

  private

  def analyze_blob(blob)
    if !blob.is_a?(ActiveStorage::Attached) || blob.attached?
      blob.tap { |blob| blob.analyze unless blob.analyzed? }
    end
  end

  def blob_size(blob)
    analyze_blob(blob)&.metadata&.values_at(:width, :height)
  end
end

# app/views/foos/index.html.erb
<%= responsive_image_tag(@foo.picture, alt: @foo.title, class: '...') %>

The helper looks reasonable to me. I’d probably keep the actual list of widths and sizes outside Active Storage, though, since those are usually determined by the application layout. If the number of responsive sizes starts growing, another option is to keep only the original in Active Storage and move the resizing to a separate image processor. The srcset helper would then generate a URL for each width, while a CDN or reverse proxy could cache the processed results.

That avoids managing an Active Storage variant for every width, but it does mean running another service and configuring the cache correctly. imgproxy is one self-hosted option for that setup. Each srcset width can map to a different processing URL, with caching handled separately in front of it: External cache | imgproxy documentation