Images are a spectacular way of improving conversions, enhancing the user experience, and engaging visitors. But images are also a real challenge nowadays, even in an advanced framework like React.
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.
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 React 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.
The good news is that with the release of the new media area, DatoCMS nows offers a drop-in solution to all of these issues so that developers can focus on the more exciting stuff.
There's a lot to cover, so let's dive in!
DatoCMS handles React images with elegance
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:
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 React images
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, which represents a value as close as possible as the original quality of the image, minus reductions of heavy metadata. It's not a percentage score, so going above 75 does not guarantee better quality.
query MyQuery { blogPost { coverImage { jpeg: url(imgixParams: {fm: jpg, q: 60}) webp: url(imgixParams: {fm: webp, q: 60}) } }}
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:
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: 60, fit: max, w: 800, h: 600}) mediumRes: url(imgixParams: {fm: jpg, q: 60, 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 as 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:
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?
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:
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 } } }}
React 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:
precomputing a very lightweight thumbnail of the original image beforehand (< 1KB),
embedding it directly inside the HTML of the page in base64 format, and
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:
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:
1query MyQuery {2 blogPost {3 coverImage {4 responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 600, h: 600 })5
6 srcSet # <- HTML5 src/srcset/sizes attributes7 webpSrcSet #8 sizes #9 src #10
11 width # <- size information12 height #13 aspectRatio #14
15 alt # <- SEO attributes16 title #17
18 bgColor # <- background color placeholder19
20 base64 # <- blur-up placeholder, base64-encoded JPEG21 }22 }23 }24}
The responsiveImage
query takes some Imgix transformation parameters as well, and returns everything you might ever need to show your images:
HTML5
src
,srcset
andsizes
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 regularsrcset
and save some extra kB;The SEO
alt
andtitle
attributes you specified within DatoCMS;Size information (
width
,height
andaspectRatio
) 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:
1query MyQuery {2 allBlogPosts {3 title4 slug5 excerpt6 content7 publishedAt8 author {9 name10 avatar {11 responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 50 h: 50 }) {12 ...responsiveImageFragment13 }14 }15 }16 coverImage {17 responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 600, h: 600 }) {18 ...responsiveImageFragment19 }20 }21 }22}23
24
25fragment responsiveImageFragment on ResponsiveImage {26 srcSet27 webpSrcSet28 sizes29 src30 width31 height32 aspectRatio33 alt34 title35 bgColor36 base6437}
React + responsiveImage
= ❤️❤️❤️
To make it even easier in a React environment, we’ve released react-datocms
, a small package that offers React components and utilities, such as React images component, especially designed to work seamlessly with DatoCMS’s GraphQL Content Delivery API and Next.js.
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:
1import React from "react"2import { Image } from "react-datocms"3
4const Page = ({ data }) => (5 <div>6 <h1>{data.blogPost.title}</h1>7 <Image data={data.blogPost.cover.responsiveImage} />8 </div>9)10
11const query = gql`12 query {13 blogPost {14 title15 cover {16 responsiveImage(imgixParams: { fit: crop, w: 300, h: 300 }) {17 ...responsiveImageFragment18 }19 }20 }21 }22`23
24export 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?
React images and DatoCMS - Conclusion
For this whole work I feel we have to thank Gatsby, Gatsby CMS React 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 React 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.
We’ll keep on improving image management and our GraphQL API as we gather more feedback from the real-world scenarios of our customers. As always, we would love to hear your thoughts on this! If you'd like to share feedback, ask questions, or just shout KUDOS, you can reach out to us over our Community, Twitter, Slack, and of course, Support.