Responsive Image Optimization With Media in Drupal 9

Sean B
Sean B
Mar 6 · 6 min read

Responsive images have always been a pain to configure properly. In Drupal you can create your breakpoints in your theme or module and use the Responsive Image module to set up different responsive image styles, defining which image style to use for a specific breakpoint. This takes quite some work and planning to set everything up, and maintaining all the image styles if changes need to be made is always a pain. Most of the sites I have built lately also have a fluid design. Since the images are defined for fixed breakpoints, this leads to a lot of the images being loaded for the user are still too big.

After struggling with this, we thought about how we could find a way to improve this. In HTML5 we can define a “srcset” attribute, which loads the correct image based on the browsers viewport. The default “src” contains a really small version of the image by default for better performance. Also notice the HTML5 “loading” attribute to enable lazy loading of images for even more optimization.

<img src="image-50w.jpg" srcset="image-480w.jpg 480w, image-800w.jpg 800w" alt="My pretty image" loading="lazy" />

Media view modes per aspect ratio

Since we were using media to add images to content, we experimented with having media view modes defined by aspect ratio, combined with a bunch of different image styles for the images in that specific aspect ratio. The media template could provide all the image styles with different widths for that image style, and use the “srcset” attribute to let the browser pick the best image. So we now got image styles for a 4:3 ratio and 16:9 ratio like:

  • responsive_4_3_50w

For images maintaining their original ratio we just use the width, like 50w, 150w, etc. The media template for our “16_9" view mode (media — image — 16-9.html.twig) now looked like this, using the “image_style“ filter of the Twig Tweak module to load the actual image URLs for image styles from the file:

{#
/**
* @file
* Default theme implementation to display an image.
*/
#}
{% set file = media.field_media_image.entity %}
{% set src = file.uri.value|image_url('responsive_16_9_50w') %}
{% set srcset = [
file.uri.value|image_style('responsive_16_9_150w') ~ ' 150w',
file.uri.value|image_style('responsive_16_9_350w') ~ ' 350w',
file.uri.value|image_style('responsive_16_9_550w') ~ ' 550w',
file.uri.value|image_style('responsive_16_9_950w') ~ ' 950w',
file.uri.value|image_style('responsive_16_9_1250w') ~ ' 1250w',
file.uri.value|image_style('responsive_16_9_1450w') ~ ' 1450w',
] %}
<img src="{{ src }}" srcset="{{ srcset|join(',') }}" alt="{{ media.field_media_image.alt }}" loading="lazy" />

Image styles based on container width

The first problem we noticed, was the “srcset” attribute uses the viewport width, not the width of the image container. This means when the viewport is 1400px and the image is shown in a column with a width of 200px, the image style with a width of 1400px is chosen by the browser. This was not giving us the result we were looking for. The only way to figure out the width of the container is via JavaScript, so we wrote a little script to figure out the available width for each image and load the correct image style using ResizeObserver. The ResizeObserver does not work in IE11, but this was not a requirement for our project. Besides, Drupal will also drop IE11 support in Drupal 10! To prevent the browser from initially loading the large images from the “srcset” attribute, we changed the “srcset” attribute to “data-srcset” and let the JavaScript handle the rest.

// Fetch all images containing a "data-srcset" attribute.
const images = context.querySelectorAll('img[data-srcset]');

// Create a ResizeObserver to update the image "src" attribute when its
// parent container resizes.
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
const images = entry.target.querySelectorAll('img[data-srcset]');
images.forEach(image => {
const availableWidth = Math.floor(image.parentNode.clientWidth);
const attrWidth = image.getAttribute('width');
const sources = image.getAttribute('data-srcset').split(',');

// If the selected image is already bigger than the available width,
// we do not update the image.
if (attrWidth && attrWidth > availableWidth) {
return;
}

// Find the best matching source based on actual image space.
let source, responsiveImgPath, responsiveImgWidth;
for (source of sources) {
let array = source.split(' ');
responsiveImgPath = array[0];
responsiveImgWidth = array[1].slice(0, -1);
if (availableWidth < responsiveImgWidth) {
break;
}
}

// Update the "src" with the new image and also set the "width"
// attribute to easily check if we need a new image after resize.
image.setAttribute('src', responsiveImgPath);
image.setAttribute('width', responsiveImgWidth);
});
}
});

// Attach the ResizeObserver to the image containers.
images.forEach(image => {
observer.observe(image.parentNode);
});

Auto generate image styles

The second problem with this method was creating all the image styles we needed. This could be fixed with a form to automatically create all the image styles we needed for our aspect ratios. So we built the Easy Responsive Images module. The module needs a minimum and maximum width in combination with a preferred amount of pixels between each image style. An optional list of aspect ratio’s can also be defined. When the configuration is saved, the styles are automatically generated.

Image optimization and WebP support

Now that we have the best possible images loaded based on the container, we can take one more step to improve the performance of our images. Using the Image Optimize module, we can create optimization pipelines that can automatically apply to images displayed via image styles. We chose to use JpegOptim and PngQuant supported via the Image Optimize Binaries module (the PreviousNext blog contains some more data on the module and results). If you can not install those binaries on your server, there is also a ImageAPI Optimize GD module.

Then there is also the ImageAPI Optimize WebP module.

WebP is a modern image format that provides superior lossless and lossy compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster.

In our tests we found that for most images, WebP is about 30% — 50% smaller than jpg images. For png images it is even more. To easily load the WebP version of an image when a browser supports it, we created a “image_url” Twig filter in the Easy Responsive Images module, with added bonus support for external images via the Imagecache External module.

The final file for our media view mode using the JavaScript and the new Twig filter looks like this:

{#
/**
* @file
* Default theme implementation to display an image.
*/
#}
{{ attach_library('easy_responsive_images/resizer') }}
{% set file = media.field_media_image.entity %}
{% set src = file.uri.value|image_url('responsive_16_9_50w') %}
{% set srcset = [
file.uri.value|image_url('responsive_16_9_150w') ~ ' 150w',
file.uri.value|image_url('responsive_16_9_350w') ~ ' 350w',
file.uri.value|image_url('responsive_16_9_550w') ~ ' 550w',
file.uri.value|image_url('responsive_16_9_950w') ~ ' 950w',
file.uri.value|image_url('responsive_16_9_1250w') ~ ' 1250w',
file.uri.value|image_url('responsive_16_9_1450w') ~ ' 1450w',
] %}
<img src="{{ src }}" data-srcset="{{ srcset|join(',') }}" alt="{{ media.field_media_image.alt }}" loading="lazy" />

The example uses the same aspect ratio for all defined widths, but technically that is not a requirement. Using a different aspect ratio for smaller/larger screens can still be used based on the requirements, although that would make the setup a bit more complex and would require more view modes for your media.

That’s about it. Some next steps could be adding a formatters for the modules and figuring out support for retina images (even though these would increase the image sizes).

Hope this helps anyone looking to improve and optimise the way they implement responsive images in Drupal.

Geek Culture

Proud to geek out.

Sign up for Geek Culture Hits

By Geek Culture

Subscribe to receive top 10 most read stories of Geek Culture — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Geek Culture

A new tech publication by Start it up (https://medium.com/swlh).

Sean B

Written by

Sean B

Freelance Drupal Developer. Passionate about Open Source. Loves working on challenging projects with talented people. Maintainer of Media in Drupal 8 core.

Geek Culture

A new tech publication by Start it up (https://medium.com/swlh).

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store