Optimizing Images - Part 3

Optimizing Images - Part 3

When we allow users to upload images, they usually upload files without any optimization for the web. It’s up to us to add some measure to prevent those images from slowing down our app. Luckily, the different gems commonly used to handle user uploads also give us solutions for this problem.

This is the third installment on a series of blog posts about optimizing images, check the previous ones here (raster images) and here (vector graphics).

ActiveStorage

A common mistake when displaying ActiveStorage image attachments is to display the attachment as uploaded by the user like this:

image_tag user.avatar

This will use the image exactly as uploaded by the user, even if the image is 3000x3000px and we display it in a 100x100px square on our page.

To show a more optimized image, we can use ActiveStorage’s variants to tell ActiveStorage to modify the image we want to display:

image_tag user.avatar.variant(resize_to_limit: [100, 100])

This will use a resized version of the original image, up to 100x100px.

For variants to work, we must have the image_processing gem in the Gemfile and we also need one of the valid image processing tools available on our system: Vips or MiniMagick (through the imagemagick and libvips -or vips- libraries). If this is not configured correctly, the code won’t fail with any exception, but it will display a broken image.

According to Rails’ documentation, Vips is recommended if available: it can be up to 10x faster and use 10% the memory compared to MiniMagic. (benchmarks)

A variant allows us to change even more properties to optimize the image even further, not just the dimensions. We can change the format, quality, type of compression, and any other option available in Vips or MiniMagick:

# with Vips
image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 })

You can read more about variants and available transformations here. You can also compare different quality settings and formats in the first installment of this blog posts series.

With ActionText

ActionText is Rails’ built-in solution to display a WYSIWYG (What You See Is What You Get) editor. When attaching files in the editor’s content, it uses ActiveStorage internally to handle them, so the same recommendations described before apply here, but since the image_tag is not on our views we have to override the default views that come with ActionText to use variants.

ActionText renders a _blob partial to display attachments. By default it resizes any image:

image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ])

We can see that ActionText already helps by resizing the file to some predefined dimensions, but we can override this partial to use other optimizations that fit better with our requirements. We could also override other partials for the complete content or even create our own rendered version of the rich content (that is outside of the scope of this article).

The representation method is a wrapper around the variant and preview methods. If the attachment is an image it’s equivalent to call .variant, if it’s a video or a pdf it uses .preview.

PaperClip

When using this gem, we don’t have the option to generate styles of the original image on the fly (PaperClip’s equivalent to ActiveStorage’s variants), but it still allows us to generate styles when the image is uploaded.

First we have to tell PaperClip which styles we’ll use when displaying the attachments:

class User < ApplicationRecord
  has_attached_file :avatar, styles: { medium: "300x300>", thumb: "100x100>" }
end

Then we can use a specific style instead of the original image with:

image_tag user.avatar.url(:thumb)

For styles to work, ImageMagick must be installed on the system.

Adding New Styles

If we are adding new styles to an application that already has uploaded files, we need to re-generate the styles for those images since PaperClip styles are not generated on the fly.

You can also try the attachment_on_the_fly gem that extends PaperClip’s functionality.

To do so, after configuring the new styles, we can run a simple script to go over all images and re-generates all styles (note that this is a CPU/IO-intensive and time-consuming task). We can either run the paperclip:refresh rake task or run something like:

users.find_each do |user|
  user.avatar.reprocess!
end

You may need to process records in batches if this simple approach is not possible.

Custom Processors

If the configuration options provided by the default processor are not enough for your needs, you can also create your custom processor and use any system tool to manipulate the image. An example of that is the paperclip-vips gem that provides image processing but using Vips instead of ImageMagick, with the speed and memory benefits mentioned in the ActiveStorage section.

To use a custom processor, we have to first define a processor extending PaperClip::Processor in lib/paperclip_processors and then we have to reference it in the attachment declaration:

class User < ApplicationRecord
  has_attached_file :avatar,
    styles: { medium: "300x300>", thumb: "100x100>" }
    processors: [:vips]
end

CarrierWave

CarrierWave is another popular gem to handle file uploads. The versions of the original image (equivalent to ActiveStorage’s variants) are created when the image is uploaded and not on the fly (similar to how PaperClip works).

The versions we want to use in our app are defined in a class that should extend CarrierWave::Uploader::Base:

class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  version :medium do
    process resize_to_fit: [300, 300]
  end

  version :thumb do
    process resize_to_fill: [100, 100]
  end
end

class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploader
end

Then, in our view, instead of using user.avatar_url (that displays the original image), we can pass the version we want as a parameter:

image_tag user.avatar_url(:thumb)

Adding New Versions

Since versions are created when a file is uploaded, if we change our Uploader class we have to re-process the images to re-generate the updated versions. The process is similar to the one used for PaperClip: we have to iterate over all the records that have an image and call a method to recreate the versions.

User.find_each do |user|
  user.avatar.recreate_versions! if user.avatar
end

# or

instance = MyUploader.new
instance.recreate_versions!(:thumb, :large)

This is a CPU/IO-intensive and time-consuming operation, so you may need to do this in batches. Also notice that you don’t have to re-generate all the versions, you can specify which ones you want to speed up this process. You can also consider using Vips instead of MiniMagick.

Other Gems

We are not covering all the options here, an application might be using other gems like Refile or Dragonfly for example, and those gems also provide methods to generate variants of the images uploaded by the user.

Conclusion

When we allow users to upload images to the site, it’s always a good idea to be prepared in case users upload non-optimized images. If we don’t, we can end up with a page showing a few images and requiring tens of megabytes when a simple resize can change that, improving the performance of our app.

Do you need help improving the performance of your application even more? Check our Tune Report service, we can help you!

Get the book