Upgrade Guide: ActiveStorage in Rails 6 and ImageProcessing

kinopyo avatar

kinopyo

Upgrading from Rails 5.2 to 6.0.0.rc1 was smooth. Most of the improvements are transparent and happening behind the scenes. However, if you're using Active Storage, there are some work for you to get the enhancements or fix some tricky stuffs.

ImageProcessing as the Variant Processor

This is a big deal, a great improvement.

In Rails 5, Active Storage has been using mini_magick gem to handle image resizing (variants). In Rails 6, a new gem called ImageProcessing is the new default to handle image variants (resizing and processing).

ImageProcessing's advantages:

  • The new methods #resize_to_fit, #resize_to_fill, etc also sharpen the thumbnail after resizing.
  • Fixes the orientation automatically, no more mistakenly rotated images!
  • Provides another backend libvips that has significantly better performance than ImageMagick.
  • Has a clear goal and scope and is well maintained. (It was originally written to be used with Shrine.)

Will explain in details but first let's get your app ready for Rails 6 Active Storage.

Get Started with ImageProcessing

First, upgrade to Rails 6. As the time of writing, RC1 is the latest version.

(If you follow the Upgrade Guide and run rake rails:update, you may get an migration file that adds foreign keys to Active Storage tables.)

Then, replace mini_magick gem with image_processing.

- gem "mini_magick"
+ gem "image_processing", "~> 1.0"

Lastly, update your variants usage. Note that the new #resize_to_fit asks for an array for width and height.

- <%= image_tag user.avatar.variant(resize: "300x200") %>
+ <%= image_tag user.avatar.variant(resize_to_fit: [300, 200]) %>

That's it. Let's talk about the advantages of ImageProcessing a bit more.

The Resize Methods

The 3 #reize_to_xxx methods listed below all retain the original aspect ratio while give your more resizing options.

  • #resize_to_fit: Will downsize the image if it's larger than the specified dimensions or upsize if it's smaller.
  • #resize_to_limit: Will only resize the image if it's larger than the specified dimensions
  • #resize_to_fill: Will crop the image in the larger dimension if it's larger than the specified dimensions

You can also pass more options to the variant with this syntax:

user.avatar.variant(resize_to_fit: [64, 64], monochrome: true, quality: 80)

Sharpening Effect and Performance

Comare to the original #resize method, #resize_to_fit also sharpens the image for free!

You may ask, how different is it visually?

Borrowing the image from the author's original blog:

Sharpening effect
Sharpening effect

It may take you 5-10 seconds if you really want to find the subtle differences, but in a glance, I think the effect is quite noticeable.

You may also ask, how much overhead the sharpening adds?

The detailed benchmark is listed here. A quick summary:

"It depends on the thumbnail size – the smaller the thumbnail is, the less time it takes to sharpen it."

With ImageMagick, resizing a 1600x900 to 800x800 and sharpen it is 1.87x slower, and to 300x300 is 1.18x slower. On libvips it doesn't go above 1.20x slower, on average it's only about 1.10x slower.

It seems like nothing is free 😁 I think if you're able to use libvips, then the tradeoff is acceptable.

Using libvips as the backend

Quoting from the ImageProcessing readme:

Libvips is a newer library that can process images very rapidly (often multiple times faster than ImageMagick).

You can tell Active Storage to use libvips as its variant processor:

# Use Vips for processing variants.
config.active_storage.variant_processor = :vips

But note that libvips is not officially supported by Heroku yet (not included in the Build Pack). So if your app is hosted on Heroku, you may want to explore some of the forked build packs.

Learn more: https://github.com/janko/image_processing/issues/32


Other Notable or Possible Breaking Changes

You can find the full list of notable changes in 6.0 ActiveStorage release notes.

One that is particularly handy for my use case is this one: Persist uploaded files assigned to a record to storage when the record is saved instead of immediately (Pull Request).

It makes attaching an image to a new record much easier.

post = Post.new

post.attach(params[:image])
# Now, it won't store the record in `active_storage_blobs` table immediately

post.attributes = post_attributes
post.save 
# When post is saved successfully, it stores Active Storage records in to blobs and attachments tables

#attach(file) return value change (undocumented)

Previosly, #attach(uploaded_file) method would return the uploaded file (Attachment object), but the behavior is changed in Rails 6 (since this commit):

The workaround is to retrieve the uploaded file(s) via post.images.last.

# Rails 5.2
images = post.images.attach(params[:file])
# => #<ActiveStorage::Attachment:0x... id: 123, name: "images", record_type: "Post"...>

# Rails 6
post.images.attach(params[:file])
# => true

post.images.last
# => #<ActiveStorage::Attachment:0x... id: 123, name: "images", record_type: "Post"...>

This hit me as a surprise. I thought either we (Rails) missed the documentation and/or the behavior change was not intended. Then I found an issue reported at the rails repo, but it was closed by:

#attach has never been documented as returning anything, which means you ought not rely on its return value.
https://github.com/rails/rails/issues/36238#issuecomment-491472747

😥 It's such a core method for Active Storage that is so accessible and commonly used. It's like the ActiveRecord#save. I think it ought to be documented, or is it just me? 🤷‍♂️

On Monkey Patches

In Bloggie, we're using this hack (Caching variants with ActiveStorage) to allow Active Storage variants to be cached by CDN. As any other monkey matches, it makes you sweat when you bump the major version of the original code.

Luckily, It still works in Rails 6.

It's sad that there are still no viable options in Rails 6 to make Active Storage to work with CDN. You may want to follow this long-standing issue where people commenting the needs and sharing various monkey patches...

/rant

This overall keeps lowering my confidence towards Active Storage. It's a new Rails component with a shining debut and great first impression, but after using it for a while in a real production-level code base, it feels like a lad to me - too young for the real world. It targets a relatively limited use case, which adds more workloads to the developers who want to use it to build a public website with solid performance and CDN cache.

We engineers pick frameworks and tools carefully. We not only buy it with its current state, we also buy in its long-term vision and philosophy. After adding Active Storage to my codebase and also watching the issues on rails repo over a year, I can't say I would recommend it to you. 🤐

Just my 2 cents. Close rant. 🙇

References

kinopyo avatar
Written By

kinopyo

Indoor enthusiast, web developer, and former hardcore RTS gamer. #parenting
Published in Rails
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.