9 tips for speeding up your Imager transforms

The number one support request I get for my Craft plugin, Imager, goes something like this: "Hi, I installed your plugin and now my server died. Please fix!". I'm paraphrasing; most users are a lot nicer than that. Here's the long version of my reply.

First of all, this is not a problem with Imager per se. No matter what PHP library you're using, you'll run into the same problems. The (slightly) longer version of the support request above, is "Hi, I installed your plugin, I tried to do a gazillion image transforms, and now my server died". The sad truth is that neither PHP, nor the webserver hardware most of us are using, are optimised to transform loads of images quickly and efficiently. 

With the new responsive images standard, the number of image transforms we want to create has increased dramatically, but the tools we use haven't changed. The main problems that we run into are:

  1. Since PHP (or rather GD and Imagick) have to decompress the image to memory before doing anything with it, servers are prone to running out of memory when opening large images. 
  2. Opening and transforming images takes some time, and servers are prone to running into execution time limits when you try to transform a lot of images in one request. 

Our best strategy is to minimise the memory usage and the time used per request. Different projects may require different approaches, but hopefully the tips below will help you figure out how to improve the performance enough that you don't run into problems.

1. Prepare your server

If a request times out or runs out of memory, there isn't much we can do. The application will die, and there is no way for Imager or Craft to clean things up. This can lead to all sorts of nasty things, so it's a good idea to try to prevent this from happening.

There are two PHP settings at play here: max_execution_time and memory_limit. Usually it's a good idea to increase these as much as you feel comfortable. 512M is usually a good amount of memory. The max execution time is a bit more scary. Ideally you'd want to increase this to 60 or maybe even 120 seconds, but that also means that any scripts that don't behave well will keep hogging your servers' resources for longer. This can be especially critical in high-traffic websites.  

If you’re using nginx, there are two other settings that control execution timeouts: request_terminate_timeout and fastcgi_read_timeout. Make sure these are also increased. 

2. Increase cache durations

Most likely, image transforms don't need to change unless you explicitly want them to. So using a high cacheDuration and cacheDurationRemoteFiles is probably a good idea. One year, 31536000, is usually a good value.

3. Reduce the number of transforms per request

This may seem like a given, but it's important to consider how many transforms you actually need to make per image. Ideally, we'd want to have lots of images in our srcset to optimise the end user performance, but in reality we need to make some compromises.

On list pages, where we want to show a lot of different images, decreasing the number of transform by 1-2 per image can make a huge difference. Say you're listing 20 articles on a page, each of which has a thumbnail with 6 different sizes in the srcset. That's 120 images that need to be created if the cache is cold. 

In some projects it might also make sense to prioritise what parts of the page you create the most transforms for. For instance, on a homepage, a hero image on the top of the page would probably benefit more from having a higher number of transforms than the article listing further down on the page.

Be smart, make compromises.

4. Reduce the size of the source image

Due to the way GD and Imagick work, for each transform PHP opens the original file and decompresses it to memory. This is the single most expensive task in the transform process. Reducing the size of a file that’s opened from say 3000x3000px (9 Mpix), to 1500x1500 (2.2 Mpix), will cut the number of pixels by a factor of ~4. That means the file will take only 1/4 as long for PHP to decompress. Also, if you’re file is on AWS, the download time needs to be factored in. 

We use Image Resizer on all our projects to reduce the size of the uploaded images at the time of the upload to a sensible size (usually something like 2500x2500px). This ensures that you'll never be creating all those transforms from a source file that's 4000x4000px.

So, the bigger the source image you're transforming from is, the longer each transform will take. That means that if the source image is big, and the target transforms are small, it's often more efficient to first create a smaller version of the source image and transform that to the target sizes. 

{# Our base image is 2000px wide #}
{% set img = craft.assets({ id: 64 }).first %}

{# Option 1: Use the source image to create five transforms between 400 and 800px #}
{% set transforms = craft.imager.transformImage(img, 
    [{ width: 400 }, { width: 800 }], 
    { ratio: 16/9 }, 
    { fillTransforms: true, fillInterval: 100 }) 
%}

{# Option 2: Create a temporary image that's 1000px wide, and use that to create the other transforms #}
{% set tempImage = craft.imager.transformImage(img, { width: 1000 }, { ratio: 16/9 }, { jpegQuality: 100 }) %}
{% set transforms = craft.imager.transformImage(tempImage, 
    [{ width: 400 }, { width: 800 }], 
    { ratio: 16/9 }, 
    { fillTransforms: true, fillInterval: 100 }) 
%}

The second option in the above example is about 30-40% faster in my tests.

The gains by doing this will differ depending on: 

  1. The size of the source image: there is more to gain the bigger the source image is.
  2. The number of transforms you create: the fewer transforms the less likely you are to gain anything because of the overhead of creating the temporary image.

5. Use a faster resizeFilter

If you’re using Imagick as your image driver, using a faster resizeFilter can decrease the transform time. The resizeFilter is the algorithm used to apply antialiasing when downsizing an image, and there are various different ones that can be used.

The default one, lanczos, results in very good quality but is rather slow. Using something at the other end of the scale, like triangle, can reduce the transform time by 20-40%, depending on the size of the source and target, and the server it's running on. It comes at the cost of a loss of quality, but that's to be expected.

And related to this, don't enable smartResizeEnabled, which will save you some bytes but will reduce performance by 10-30%, depending on the resize filter that's used.

6. Experiment with instanceReuseEnabled

Remember when I said that before a transform can be done, GD/Imagick has to open and decompress the original file to memory, and that this is the single most expensive operation? I lied, there is a way in Imager to avoid this. 

In the config and documentation you'll find a mysteriously named config setting, instanceReuseEnabled. Enabling this will make Imager open the source image only once and reuse the underlying image instance for each transform. This increases performance massively, in most of my test cases by 50-60%. There are a couple of downsides though:

  1. The quality of the transforms after the first one will suffer, since each successive transform will be done on the previous one, and not the original.
  2. You should always order your transforms from the biggest to the smallest. If you do it the other way around, you'll actually be upscaling the transforms, or they won't be upscaled at all if you have allowUpscale set to false.
  3. Any effects will be carried over from the previous transform.
{# This will be a disaster, quality wise #}
{% set transforms = craft.imager.transformImage(img, 
    [{ width: 400 }, { width: 800 }, { width: 1200 }, { width: 1600 }], 
    { ratio: 16/9 }, 
    { instanceReuseEnabled: true }) 
%}

{# All transforms after the first one will be grayscale  #}
{% set transforms = craft.imager.transformImage(img, 
    [{ width: 1600, effects: { grayscale: true } }, { width: 1200 }, { width: 800 }, { width: 400 }], 
    { ratio: 16/9 }, 
    { instanceReuseEnabled: true }) 
%}

Again, for some projects the improved performance will outweigh the reduced quality, for some it won't. But remember that you can enable this behaviour on a per transform basis, as shown in the examples above. 

7. Use post optimisation tools with care

If you’re using post transform optimisation tools, ask yourself if it’s really necessary. Yes, you’ll shave off a few bytes and make GPSI happier, but it comes at a considerable cost. 

8. Use cloud services with care

If you’re uploading transforms to AWS or Google Cloud, that also has a cost. Every single transformed file has to be uploaded to AWS through their API, and the latency quickly adds up. Consider instead using a pull CDN (that grabs the transformed file from your server when it's requested), or leave the transforms on your server (might be totally fine if your users are mostly local).

9. Preparse! 

Assuming you have cacheDuration set to something high, Imager won't have to recreate the transforms very often (unless you clear the Imager cache, in which case you should be prepared for CPU overload). The biggest problem now is when a lot of new content is added and new transforms have to be created. To avoid having users get a high TTFB, the performance hit can be moved over to the admin user by adding a Preparse field to your entry types that creates the image transforms when content is saved. 

To make this maintainable, make sure you tap into Preparse's ability to use Twig includes. We usually create one field and twig file per section or entry type, and include it in the fields settings like this: {% include '_preparse/news-entry' %}

Inside this twig file we can put whatever logic is needed to create the necessary image transforms. Here's an example from one of our projects:

{% set image = entry.image.first ?? null %}
{% set listImage = entry.listImage.first ?? null %}

{% if image %}
	{% include '_includes/news/news-article-image' with { image: image } only %}
{% endif %}

{% if listImage %}
	{% include '_includes/news/news-item-image.twig' with { image: listImage } only %}
{% else %}
	{% include '_includes/news/news-item-image.twig' with { image: image } only %}
{% endif %}

{% for block in entry.newsBlocks.type('images') %}
	{% include ["_blocks/newsblocks/" ~ (block.type | lower), "_blocks/" ~ (block.type | lower)] ignore missing with { block: block } only %}
{% endfor %}

The includes in the example above are the same that are used in our normal templates, ensuring that any changes to the code at a later point will be carried over to the Preparse field. 

Final words

Hopefully these tips will make your server a little bit happier with Imager. Unfortunately, even if you implement all these tips, you can still experience issues with long-running requests and execution timeouts. The only scalable way of handling this would be to use third-party services for off-loading the job of transforming images to an infrastructure that specialises in doing exactly that. Who knows, maybe Imager can help with that in the future... 


Support open source. Support Imager. Buy beer.
Imager is licensed under a MIT license, which means that it's completely free open source software, and you can use it for whatever you wish. Unfortunately, Imager doesn't make itself (it ought to, though!). A lot of beer has been consumed since version 1.0.0, and beers are expensive in Norway. Craft 3 is right around the corner, which will result in many late, beer-filled nights to get Imager updated to work with it. 

If you're using Imager and want to support the development, you now have the chance! Head over to Beerpay, and donate a beer or two. 

Posted in ⟶

  • Craft