Upgrade Guide: ActiveStorage in Rails 6 and ImageProcessing
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:
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
Clap to support the author, help others find it, and make your opinion count.