Optimize image thumbnails

The default thumbnailer for Romanesco is pThumb. This extra utilizes the phpThumbOf library, which is embedded in MODX. It is flexible and reliable and has been working well for many years, but now it's starting to show its age a little. Modern image formats such as WebP are not supported and the compression algorithm is also not as effective as modern counterparts.

The result: thumbnails are either too large or too ugly, depending on the quality setting.

Another problem is that with the introduction of responsive images, it became a lot harder to optimize and compress images upfront. You probably need to generate different versions of the image for different screen sizes and pixel densities after you upload the image file, so tools like ImageOptim or a plugin to resize the image on upload don't really fit this workflow anymore. Unless you're willing to generate all the responsive versions upfront too, but... Yikes!

Online platforms like Cloudinary, Cloudflare and Amazon are now offering these services through an API, but call me an oldfashioned file hoarder; I just want to have everything stored and running on my own machine.

The solution: keep using pThumb for creating all the thumbnails, but optimize them after they've been generated.

How does it work?

It starts with the imgOptimizeThumb snippet. That's a post hook for pThumb (more on that later), which runs after the thumbnail is generated.

It uses the Squoosh library from Google to create a WebP version of the image and optimize the original. You need to install the Squoosh CLI package on your server with NPM.

If the Scheduler extra is installed, the Squoosh command is added there as an individual task. This means it takes a little while for all the images to be generated. Without Scheduler they're created when the page is requested, but the initial request will take a lot longer (the thumbnails are also being generated at this point).

To serve the WebP images in the browser, use Nginx to intercept the image request and redirect it to the WebP version. It will do so by setting a different header with the correct mime type, but only if the WebP image is available (and if the browser supports it). So you don't need to change the image paths in MODX or provide any fallbacks in HTML.

How to activate it?

Server

Install the Squoosh CLI tool:

npm install -g @squoosh/cli 

Nginx

Add the following to your nginx.conf:

http {
  ...
  map $http_accept $webp_ext {
    default "";
    "~*webp" ".webp";
  }
  map $uri $file_ext {
    default "";
    "~(\.\w+)$" $1;
  }
}

And in your server config under sites-available:

server {
  ...
  # Load WebP image if available
  location ~* "^(?<path>.+)\.(png|jpeg|jpg|gif)$" {
    try_files $path$webp_ext $path$file_ext =404;
  }
}

If you also want to set a longer cache lifetime, then add the following before that location block:

server {
  ...
  # Load WebP image if available
  location ~* "^(?<path>\/assets\/cache.+)\.(png|jpeg|jpg|gif)$" {
    expires 1y;
    add_header "Cache-Control" "public";
    try_files $path$webp_ext $path$file_ext =404;
  }

  # Add cache header to static assets
  location ~* '\.(?:jpg|jpeg|gif|png|webp|ico|etc...)' {
    ...
  }
}

The regex is constrained to the assets/cache folder now, so it won't interfere with the other static assets.

Source of this perfect little trick.

MODX

Find (or create) the system setting named pthumb.post_hook and add imgOptimizeThumb as value.

IMPORTANT NOTE: the post hook is not a standard feature of pThumb yet, so you'll need to overwrite the core class yourself. See this PR for details.

After adding the post hook, MODX will run the imgOptimizeThumb snippet after the thumbnail has been generated. This will execute the relevant Squoosh command.

And although entirely optional, it pays off to install the Scheduler extra. This shortens initial page loads and keeps server load in check. The imgOptimizeThumb snippet will detect Scheduler if it's installed and automatically handle task creation and batch execution.

Changing image quality

Image quality is defined under Configuration > Performance. This setting (img_quality) is usually forwarded directly to pThumb for its quality parameter. But now that we're post-processing our image after thumbnailing, it's actually better to generate the thumb with a high quality setting (separate from the img_quality value). Image optimization generally works better with a good source file.

To work this out, there is a system setting named romanesco.img_quality, which by default contains a placeholder for the img_quality setting under Configuration. But if we change this to something like 90, it means that the source file will be compressed with that quality setting and the optimized thumbs will use the config setting.

The Squoosh library does an impressive job of compressing images without much loss in detail, even at very low quality settings. So don't be afraid to set it to Low (50) or Minimum (30). For most images the difference is hardly observable, but it shaves off a substantial amount of bytes from the file size (sometimes as much as 80%).

And don't worry about the high quality original thumbs taking up a lot of disk space either. Squoosh will also optimize this image in place. They serve as fallbacks if WebP is not supported, or the .webp file is not yet generated.

Clearing image cache

The entire process described here is triggered when the thumbnail is generated. This only occurs if there is no image available in the thumbnail cache. So always make sure you clear thumbnail and site cache, otherwise nothing will happen.

In Romanesco, images are cached under assets/cache/img.

Can I use it without Romanesco?

I didn't try that yet, but I don't see why not. Just copy the imgOptimizeThumb snippet, remove the part on top that fetches the Romanesco class and replace the imgQuality variable with something that works for you.