How to offer responsive, progressive images in 2020, in one-line

stefanoverna

Stefano Verna

Posted on December 11, 2019

How to offer responsive, progressive images in 2020, in one-line

Disclaimer: I work at DatoCMS, and this was originally written for our blog.

Images are a spectacular way of improving conversions, enhancing the user experience, and engaging visitors. But images are also a real challenge nowadays.

As new devices boast retina displays and higher resolutions, designers are eager to leverage these resolutions to provide beautiful vibrant graphics. But, that comes at a cost: asset size. To get a sense of the growth, average page weight has increased by 34-55% in the last 3 years, of which 70% is due to heavier images.

Alt Text

If you’re on a fiber connection, downloading 1.7MB of data for a single web page (!!!) might not slow you down much, and it gives you a richer experience. But if you’re on a cellular connection, you would probably prefer a lower quality image to a painfully slow page.

So the question is: are we, as developers, able to optimize web pages for such a wide range of resolutions, pixel densities, and network connection speeds? Sure, we’ve got all the technology we need! Is it easy? Nope, more like a nightmare, and that’s precisely the reason why average page weight keeps on increasing: it’s too much work to handle it properly, especially if you don't have big budgets.

Here’s a quick recap of what it means to offer best-in-class support of images for a webpage nowadays (we’ll cover every bullet point in detail later):

  • you need to encode all of your images in JPEG and WebP, and offer one version or the other depending on whether the browser supports the latter;
  • you need to provide low-resolution versions of every image you offer — remember, both JPEG and WebP! — so that screens with low resolution or pixel density don't have to download a huge image for no reason;
  • you need to lazy load images, that is, defer the loading of images that aren’t immediately visible to when a visitor scrolls down to them, avoiding useless HTTP requests and downloading of data before time;
  • especially with lazy loading in place, your HTML needs to embed some kind of placeholder for the image, both to avoid terrible jerky scrolling/layout juddering, and to give visitors a visual hint that in a few milliseconds images will be arriving;
  • you need to handle SEO properly so that search engines will still be able to scrape your page even with all of this stuff in place.

Sure, in the last couple of years, we've seen an increased availability of tools that help alleviate the suffering of developers in this regard. However, the reality is that it's still pretty hard to nail the problem, as the perfect solution requires implementing a combination of all of the stuff mentioned above, which is quite daunting and labor-intensive.

Let's see how DatoCMS offers a drop-in solution to all of these issues so that developers can you can work on more exciting stuff.

Getting the image size

Ok, let's start with the basics. At a minimum, our GraphQL CDN-powered API can give you the URL of an image you uploaded, together with its width and height:

query MyQuery {
  blogPost {
    coverImage {
      url
      width
      height
    }
  }
}

This is what you are expected to get with most CMSs. Fine for the 2000s, not so much nowadays. Can we do better than that?

Re-encoding and downscaling

Now the first optimization you can get for free with DatoCMS is converting your image to JPEG or WebP with the fm argument. You can easily adjust the output quality if needed (the default is 75%):

query MyQuery {
  blogPost {
    coverImage {
      jpeg: url(imgixParams: {fm: jpg, q: 80})
      webp: url(imgixParams: {fm: webp, q: 80})
    }
  }
}

How does this work? Well, every image you upload to DatoCMS is stored on Imgix — the leading cloud image-processing company. They offer a CDN optimized for image delivery, which provides on-the-fly manipulations and caching. By simply adding some parameters to your image URL, you can enhance, resize and crop images, compress them and change the format for better performance, create complex compositions and extract useful metadata.

The beauty is that since it’s GraphQL, you can easily see the documentation of all the different transformations right from the query explorer, every argument is strongly typed, so you don’t have to worry about typos:

Alt Text

If you need to downscale your original image to take into account low-resolution devices, you just need to add a couple of parameters to the same query:

query MyQuery {
  blogPost {
    coverImage {
      lowRes: url(imgixParams: {fm: jpg, q: 80, fit: max, w: 800, h: 600})
      mediumRes: url(imgixParams: {fm: jpg, q: 80, fit: max, w: 1024, h: 768})
    }
  }
}

The fit: max argument will resize the image to fit within a specific dimension without cropping or distorting the image, but will not increase the size of the image if it is smaller than the output size. The resulting image will maintain the same aspect ratio of the input image.

There are plenty of resizing/cropping modes you can use, you just have to change the fit parameter, and we'll do everything for you.

What about progressive images?

“Progressive Images” is a hot topic these days. It's more of an umbrella term that covers a broad category of techniques rather than a single specific one.

In general, the underlying idea is that perceived speed is more important than actual speed, so you want to offer an extremely low quality version of an image soon as possible, and then progressively enhance it fetching additional data.

Lazy loading and content reflow

One of the most useful techniques you can implement to reduce initial load times and cut down traffic costs drastically is to implement lazy loading. With lazy loading, you use JavaScript to defer the loading of images only when a visitor scrolls down to them, avoiding useless HTTP requests and data download before time.

The problem with lazy loading is the infamous content reflow (also known as layout trashing). What is it? Reflow is the name of the web browser process for re-calculating the positions and geometries of elements in the document, for the purpose of re-rendering part or all of the document.

As soon an image is loaded, it causes content to be pushed down or jump to a different location, and can make the page feel like it is still loading and is bad for perceived performance. Content reflowing is especially bad for user experience when not initiated by user interaction.

Here’s a simple example demonstrating the effect:

Alt Text

While content reflow happens with every page load, lazy loading techniques definitely amplify the effect as they make content reflow happen when users are not expecting to see it. So even if PJPEGs are great to enhance your webpage progressively, they can’t do anything to prevent this issue from happening.

Color background placeholders

The way you can remove this issue is to know the aspect ratio of your images beforehand so that you can:

  • wrap the image in a <div> with the same aspect ratio as the image;
  • load the image in background and when it’s ready…;
  • …replace the <div> with the final image, maybe with some subtle transition effect.

Here's the result. Much better to the eyes, right?

Alt Text

If we knew the dominant color of the image beforehand, we could even fill the placeholder space in our HTML page with a background color representative of the image that is being loaded.

That’s exactly what Google Search does, and the result might approximately be this:

Alt Text

As you can see, background-color placeholders work great with images that have a single predominant color but might not be ideal for all scenarios.

With DatoCMS, you can easily get both the image size and dominant colors in one call:

query MyQuery {
  blogPost {
    coverImage {
      url(imgixParams: {fm: jpg})
      width
      height
      colors { hex }
    }
  }
}

Image placeholders (LQIP)

Medium and Facebook popularised a variant of this technique that has a much broader range of use than background-color placeholders, and is being referenced around the web with different names:

  • Embedded Image Preview (EIP)
  • Low Quality Image Placeholders (LQIP)
  • Blur-up Image Loading

The technique relies on:

  1. precomputing a very lightweight thumbnail of the original image beforehand (< 1KB),
  2. embedding it directly inside the HTML of the page in base64 format, and
  3. scaling it up to take up the size of the original image to load.

The result is a blurred-up preview of the original image immediately visible to the visitor, without the need to perform any additional HTTP request or fetch more data without causing content reflow:

Alt Text

With DatoCMS, you can get the base64 encoded placeholder of your original image with the blurUpThumb query. You can pass the exact same transformations you asked for your original image so that the placeholder will be properly aligned:

query MyQuery($params: ImgixParams) {
  blogPost {
    coverImage {
      url(imgixParams: $params)
      blurUpThumb(imgixParams: $params)
    }
  }
}

Putting it all together: introducing the responsiveImage query

Now that we’ve covered everything that’s involved in offering best-in-class images to your web pages, it’s time to show how you can solve everything with a single GraphQL query to our CDN:

query MyQuery {
  blogPost {
    coverImage {
      responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 600, h: 600 })

        srcSet             # <- HTML5 src/srcset/sizes attributes
        webpSrcSet         #
        sizes              #
        src                #

        width              # <- size information
        height             #
        aspectRatio        #

        alt                # <- SEO attributes
        title              #

        bgColor            # <- background color placeholder

        base64             # <- blur-up placeholder, base64-encoded JPEG
      }
    }
  }
}

The responsiveImage query takes some Imgix transformation parameters as well, and returns everything you might ever need to show your images:

  • HTML5 src, srcset and sizes attributes that you can directly feed to your <img> elements to get out-of-the-box responsive images;
  • The WebP version of the srcset (webpSrcSet) so that modern browsers can use that instead of the regular srcset and save some extra kB;
  • The SEO alt and title attributes you specified within DatoCMS;
  • Size information (width, height and aspectRatio) to properly handle content-reflow. These dimensions will be the ones you’ll get after the Imgix transformations you requested, so you can use them directly into your HTML;
  • Dominant background color (bgColor) if you want to go with the background color technique;
  • Blur-up base64 placeholder (base64) if you want to go with the LQIP technique;

If you just want lazy-loaded responsive images, your HTML could be something like this (not production-ready, just to get a general idea):

<picture>
  <source srcset="{{webpSrcSet}}" type="image/webp">
  <source srcset="{{srcSet}}">
  <img src="{{src}}" alt="{{alt}}" title="{{title}}" loading="lazy">
</picture>

Want to avoid content reflow?

<div style="position: relative;">
  <div style="padding-top: {{100.0 / aspectRatio}}%;">
  <picture style="position: absolute; left: 0; top: 0; width: 100%">
    <source srcset="{{webpSrcSet}}" type="image/webp">
    <source srcset="{{srcSet}}">
    <img src="{{src}}" alt="{{alt}}" title="{{title}}" loading="lazy">
  </picture>
</div>

Want to add background color placeholders? Just add bgColor to the outer <div>:

<div style="position: relative; background-color: {{bgColor}};">
  <div style="padding-top: {{100.0 / aspectRatio}}%;">
  <picture style="position: absolute; left: 0; top: 0; width: 100%">
    <source srcset="{{webpSrcSet}}" type="image/webp">
    <source srcset="{{srcSet}}">
    <img src="{{src}}" alt="{{alt}}" title="{{title}}" loading="lazy">
  </picture>
</div>

Prefer blur-up LQIP? Throw base64 in the mix:

<div style="position: relative; background-image: url({{base64}}); background-size: cover;">
  <div style="padding-top: {{100.0 / aspectRatio}}%;">
  <picture style="position: absolute; left: 0; top: 0; width: 100%">
    <source srcset="{{webpSrcSet}}" type="image/webp">
    <source srcset="{{srcSet}}">
    <img src="{{src}}" alt="{{alt}}" title="{{title}}" loading="lazy">
  </picture>
</div>

And bear in mind, thanks to the power of GraphQL, you can combine multiple queries in a single API call. You can basically fetch the whole content of a webpage, both text and images, with no extra latency in a single, CDN-powered HTTP request:

query MyQuery {
  allBlogPosts {
    title
    slug
    excerpt
    content
    publishedAt
    author {
      name
      avatar {
        responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 50 height: 50 }) {
          ...responsiveImageFragment
        }
      }
    }
    coverImage {
      responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 600, height: 600 }) {
        ...responsiveImageFragment
      }
    }
  }
}


fragment responsiveImageFragment on ResponsiveImage {
  srcSet
  webpSrcSet
  sizes
  src
  width
  height
  aspectRatio
  alt
  title
  bgColor
  base64
}

React + responsiveImage = ❤️❤️❤️

To make it even easier in a React environment, we’ve just released react-datocms, a small package that offers React components and utilities specially designed to work seamlessly with DatoCMS’s GraphQL Content Delivery API.

  • TypeScript ready;
  • CSS-in-JS ready;
  • compatible with any GraphQL library (Apollo, graphql-hooks, graphql-request, etc.);
  • usable both client and server side;
  • compatible with vanilla React, Next.js and pretty much any other solution.

The Github repo offers a set of examples, but here’s is the gist of it of it:

import React from "react"
import { Image } from "react-datocms"

const Page = ({ data }) => (
  <div>
    <h1>{data.blogPost.title}</h1>
    <Image data={data.blogPost.cover.responsiveImage} />
  </div>
)

const query = gql`
  query {
    blogPost {
      title
      cover {
        responsiveImage(imgixParams: { fit: crop, w: 300, h: 300 }) {
          ...responsiveImageFragment
        }
      }
    }
  }
`

export default withQuery(query)(Page);

Isn’t this wonderful? It seems like we’re back in the old days when offering an image was just a matter of adding an innocent <img> tag to the page, right?

Expect us to release similar integrations for Vue, Svelte, and all the other cool kids in town very soon.

Conclusion

For this whole work I feel we have to thank Gatsby and its gatsby-image component, which has been a great source of inspiration. Building on their idea, we are freeing it so that can be used used anywhere, by anyone.

DatoCMS is just an API, it’s agnostic by definition**. You’re not forced to use it within the context of a static website. Image transformations happen on the fly and get cached forever in CDN, instead of being generated at build-time. You can use responsiveImage from the browser, Node.JS, PHP, Ruby, a mobile app, whatever. The use cases are endless.

We believe this is an enormous leap forward for modern, progressive web development, and we are just at the beginning of this journey. Responsive/progressive images are one of the main building blocks for the modern web, and developers need higher-level primitives to handle them without getting crazy. And the right layer to provide such primitives is in the API, accessible in every possible context.

💖 💪 🙅 🚩
stefanoverna
Stefano Verna

Posted on December 11, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related