Friday, June 4, 2021

Tackling responsive images


One of the main goals when I started with Stitcher was heavily optimized images. Looking at the HTTP Archive stats, it's clear we're doing something wrong. Luckily, the Responsive images spec has been made by a lot of smart people to counter the image problem. My goal was to implement this spec in library in a way that was easy enough for developers to use it to its full extent. We're not completely there yet, but we're close. In this blogpost I want to talk about the challenges I faced creating this library.

To be clear: the goal of the responsive images spec is to reduce bandwith used when downloading images. Images nowadays require so much bandwith. When you think about it, it's insane to load an image which is 2000 pixels wide, when the image on screen is only 500 pixels wide. That's the issue the spec addresses, and that's the issue I wanted to solve in Stitcher.

So I want one image to go in, x-amount of the same image with varying sizes coming out, and let the browser decide which image is the best to load. How could I downscale that source image? That was the most important question I wanted answered. All other problems like accessebility in templates and how to expose the generated image files, were concerns of Stitcher itself.

My first take on downscaling images was the following:

Take the source image and a set of configuration parameters. These parameters would decide the maximum amount of image variations and the minimum width of the image. Eg. I want a maximum of ten images, with the smallest image being 300 pixels wide. Now the algorithm would loop a maximum of 10 times, always creating an image which is 10% smaller in width than the previous one.

You might already see this is not the optimal approach. After all: we're trying to reduce bandwith used when loading images. There is no guarantee an image which is downscaled 10%, is also reduced in size. Much depends on which image codecs are used, and what's in the image itself. But by using this approach early on, I was able to implement this "image factory" with Stitcher. Next I would be working on optimizing the algorithm, but for the time being I could tackle the Stitcher integration.

 Linking with Library

Letting Library know about responsive images was both easy and difficult at the same time. The basic framework was already there. So I could easily create an image provider which used the responsive factory, and returned an array representation of the image. The template syntax looks like this:

<img src="{$image.src}" srcset="{$image.srcset}" sizes="{$image.sizes}" />

Unfortunately, there is no way to automate the sizes part, unless you start crawling all CSS and basically implement a browser engine in PHP. My solution for this part is pre-defined sets of sizes. That's still a work in progress though, I'm not sure yet how to make it easy enough to use. For now, I'm just manually specifying sizes when writing template code.

But the tricky part wasn't the sizes, neither the srcset. It was handling paths and URLs. I've noticed this throughout the whole framework: creating the right paths and URLs (correct amount of slashes, correct root directory etc.) is actually quite the pain to manage. I'm convinced by now I need some kind of helper which always renders the correct paths and URLs. It's on my todo list.


A pretty robust library came to be. You could throw it any image, and it would generate a set of variations of that images, scaled down for multiple devices. It returned an object, which Stitcher parsed into a template variable. In templates, the following is now possible.

<img src="{$image.src}" srcset="{$image.srcset}" sizes="{$image.sizes}" />

Like I wrote earlier, the first version of the scaling down algorithm was based on the width of images. It worked, but it wasn't solving the actual problem: optimizing bandwidth usage. The real solution was in downscaling images based on their filesizes. The problem there: how could you know the dimensions of an image, when you know the desired filesize. This is where high school maths came into play. I was actually surprised how much fun I had figuring out this "formula". I haven't been in school for a few years, and I was rather happy I could use some basic maths skills again!

This is what I did:

filesize = 1.000.000
width = 1920
ratio = 9 / 16
height = ratio * width

area = width * height
 <=> area = width * width * ratio

pixelprice = filesize / area
 <=> filesize = pixelprice * area
 <=> filesize = pixelprice * (width * width * ratio)
 <=> width * width * ratio = filesize / pixelprice
 <=> width ^ 2 = (filesize / pixelprice) / ratio
 <=> width = sqrt((filesize / pixelprice) / ratio)

So given a constant pixelprice, I can calculate the required width an image needs to have a specified filesize. Here's the thing though: pixelprice is an approximation of what one pixel in this image costs. That's because not all pixels are worth the same amount of bytes. It heavily depends on which image codecs are used. It is however the best I could do for now, and whilst I might add some more logic in the future, I'd like to try this algorithm out for a while.

So now the Responsive Factory scales down images by filesize instead of width. A much better metric when you're trying to reduce bandwidth usage. This is how the library is used in Stitcher:

use Brendt\Image\Config\DefaultConfigurator;
use Brendt\Image\ResponsiveFactory;

$config = new DefaultConfigurator([
    'driver'      => Config::get('engines.image'),
    'publicPath'  => Config::get('directories.public'),
    'sourcePath'  => Config::get('directories.src'),
    'enableCache' => Config::get('caches.image'),

$responsiveFactory = new ResponsiveFactory($config);

All images in Library go through this factory, the factory will generate x-amount of variations of the image, and the browser decides which one it will download. Its pretty cool, and I hope it will help websites to serve more optimized images, while a developer can still focus on the most important parts of his project.