Responsive images

I've seen many solutions to the problems of responsive images. Most of them fall down in some way, many of them leave me with blurry images on a UHD 4K display. If you haven't heard of the srcset attribute then now is the time to use it.

Srcset

The solution I present here doesn't cover background images or situations that require art direction. If you need a different crop for your images as the page resizes you should look towards the picture element, but you may be able to take parts of this article to help you.

Srcset is an attribute of the img element supported by all major current browsers since 2015. Internet Explorer (all versions) doesn't support this attribute but it ignores it – the image contained in the standard src attribute will be rendered instead.

Srcset is the best possible way to render inline images for several reasons. The browser will only download one image – the most suitable for the situation, which avoids delays and double loading caused by JavaScript solutions. It's native so it's fast, and responds properly to zooming pages in and out (on desktop). You can try this on my about page – zoom into the page and you'll see the image never gets blurry, even at full screen 4K. Even though the CSS pixel ratio is 2 on my desktop (the reason why most websites serve me blurry images), srcset understands this and serves images accordingly. Images are loaded in as required, but smaller images won't be loaded if you're zooming out because there's no need.

The sizes attribute is also required to tell your img element what size the image is rendering at, at any given breakpoint. This should generally be a copy of the breakpoints on your site which convey size changes for the image.

As you can imagine, this can get quite bloaty which is why it's best to generate the srcset and sizes attributes with server side code. It's important to note that I say server side code – a src attribute is required on valid HTML, and adding the srcset and sizes with JavaScript will result in the bad double loading. Internet Explorer must load whatever's in the src attribute so using a tiny unrelated image to minimise the double loading impact simply wouldn't work for all users.

Example

Here's an example of an image with a srcset and sizes attributes:

<img src="image-600.jpg" srcset="image-600.jpg 600w, image-800.jpg 800w, image-1080.jpg 1080w, image-1200.jpg 1200w, image-1600.jpg 1600w, image-1920.jpg 1920w, image-2560.jpg 2560w, image-3840.jpg 3840w" sizes="(max-width: 800px) 100vw, (max-width: 1200px) 46vw, calc(600px - 4vw)" alt="Alternate text " class="left-50" />

It looks pretty big, so we'll dissect it.

src="image-600.jpg"

This part should look most familiar – the standard src attribute should contain the image that you wish browsers that don't support srcset to load, you can check support on caniuse. At the time of writing only Internet Explorer (all versions) and Opera Mini don't support srcset.

srcset="image-600.jpg 600w, image-800.jpg 800w, image-1080.jpg 1080w, image-1200.jpg 1200w, image-1600.jpg 1600w, image-1920.jpg 1920w, image-2560.jpg 2560w, image-3840.jpg 3840w"

There's quite a few images in there. Paths and filenames have been adjusted for brevity. There's a few ways to do this, but this is my favourite – specifying an image and then stating how wide it is, e.g.: image-600.jpg is 600px wide. You don't necessarily need to specify this number of images, but when the element is dynamically generated it makes sense because you're catering for a large range of browser sizes and zoom levels.

Now, the browser is clever but not that clever – it knows how wide the images it can download are, but it assumes that every image is being rendered at 100% of the screen width. This is fine if you're rendering images full screen but chances are you're not – this is where sizes comes in.

sizes="(max-width: 800px) 100vw, (max-width: 1200px) 46vw, calc(600px - 4vw)"

On the example above we've used a class which is defined as showing an image at half the container width unless the device is 800px or less, and then we make it full width. The sizes attribute replicates this, and includes allowances for margins.

Filesizes

You may be wondering what happens to full size 4K images – 3840 x 2160 must be quite heavy on the kilobytes, and you'd be right. But we're in luck because as screen resolution increases density does too. What that means is that we can compress the images more – where you may use 75% compression at a standard resolution you'll find you can get away with 30-50% at a 4K resolution.

When I first heard of this I thought it would be hideous, but remarkably it works perfectly well. A 1920 x 1080 jpg at 61% quality looks terrible stretched to 3840 x 2160, where the native 3840 x 2160 jpg at 41% quality looks just fine. The filesizes of 272KB at HD vs 636KB at UHD demonstrate that an image at 4x the resolution can be only 2.3x the filesize whilst vastly improving display quality.

Code

Here is an example of some code that resizes an original image into multiple responsive sizes and outputs the required img element code:

function insertPhoto($filename, $alt, $class, $size) {
	// usage: jpeg name (no .jpg), alt, class names, defined BP size
	$srcset = [];
	$widths = [600, 800, 1080, 1200, 1600, 1920, 2560, 3840]; // common screen widths
	// this sizes variable is specifically for 50% width desktop / 100% width mobile with a max container of 1200px, and is unique to this site.
	switch($size) {
		case 50: // 50%
			$sizes = '(max-width: 800px) 100vw, (max-width: 1200px) 46vw, calc(600px - 4vw)';
			break;

		default: // 100%
			$sizes = '(max-width: 800px) 100vw, (max-width: 1200px) 96vw, calc(1200px - 4vw)';
	}
	foreach ($widths as $width) {
		$srcset[] = PHOTOS.$filename.'-'.$width.'.jpg '.$width.'w';
	}
	$srcset = implode(', ', $srcset);
	$filename = $filename.'.jpg';
	if(!file_exists(ROOT.PHOTOS.preg_replace('/(\.jpg)$/', '-'.$width."$1", $filename))) {
		// requested file unavailable
		if(file_exists(ROOT.PHOTOS.'_original/'.$filename)) {
			// original exists, generate new responsive image set
			if(strpos($filename, '/')) {
				// file is in a folder, let's make the generated structure match
				$folders = explode('/', $filename);
				array_pop($folders);
				$currentFolder = ROOT.PHOTOS;
				foreach($folders as $folder) {
					$newFolder = $currentFolder.'/'.$folder;
					if(!is_dir($newFolder)) {
						mkdir($newFolder);
					}
					$currentFolder = $newFolder;
				}
			}
			list($originalWidth, $originalHeight) = getimagesize(ROOT.PHOTOS.'_original/'.$filename);
			$image = imagecreatefromjpeg(ROOT.PHOTOS.'_original/'.$filename);
			foreach($widths as $width) {
				$ratio = $originalWidth / $width;
				$height = intval($originalHeight / $ratio);
				$newImage = imagecreatetruecolor($width, $height);
				imagecopyresampled($newImage, $image, 0, 0, 0, 0, $width, $height, $originalWidth, $originalHeight);
				imagejpeg($newImage, ROOT.PHOTOS.preg_replace('/(\.jpg)$/', '-'.$width."$1", $filename), 80 - ceil($width / 100)); // quality calculated as 80 - width / 100
				unset($newImage); // stops memory leak
			}
		} else {
			// no original, return missing image image
			return '<img src="/assets/img/no.svg" alt="" class="'.$class.'"/>';
		}
	}
	// send img element with full responsive srcset
	$img = '<img src="'.PHOTOS.preg_replace('/(\.jpg)$/', '-'.$widths[0]."$1", $filename).'" srcset="'.$srcset.'" sizes="'.$sizes.'" alt="'.$alt.'" class="'.$class.'" />';
	return $img;
}

This code chunk can be used as is, but you should probably tweak it so that it alters a templated piece of HTML rather than doing it inline, and has the sizes information passed dynamically – but I'm not going to do everything for you 😉