The power of Gatsby.js

As a software developer, we are used to thinking in abstractions. We filter details that are not relevant in a particular context and retain the essential information that is relevant in that context. The trick is to choose the right level of abstraction and thus reduce the complexity of a certain task at hand. In this blog our ‘task’ is adding responsive and progressive images loading functionality to websites. I was positively surprised by one of the great abstractions available in Gatsby.js, which makes it extremely easy to add responsive and progressive image loading functionality to websites (including my own) through the gatsby image component.

In this blog post I'll demonstrate how to use responsive and progressive image loading in Gatsby and verify gatsby-image’s slogan: “Speedy, optimized images without the work.”.


What is Gatsby

Gatsby is an open source framework based on React and powered by GraphQL for creating websites and apps. It's a Static Site Generator (SSG) that can be used as the presentation layer for a headless Content Management System (CMS) or standalone (without a CMS). Blogs, e-commerce, lead generation, portfolio and corporate websites are all suitable use cases for Gatsby.

Gatsby architecture

Websites created with Gatsby are super fast, extremely secure and based on a popular technology stack. Some example websites built with Gatsby are: reactjs.org, airbnb.io, airbnb.design/cereal, justdoit.nike.com, ca.braun.com/en-ca, bitcoin.com and spotify.design. These websites all leverage the ease of React components and the dynamics of GraphQL to generate highly performant websites. I've built my own website (christiaanrudolfs.com) with Gatsby where I use GraphQL to pull my blog content (in Markdown format) into React components to dynamically generate the blog section and transform it into HTML. As we’ll see next, GraphQL also plays an important role in image optimization within Gatsby.


Image optimization

Page and image loading times can make or break a website’s user experience. There are several practical case studies mentioned by WPO stats that demonstrate the impact of web performance optimization (WPO) on user experience.

Gatsby has built-in support for responsive and progressive image technologies. Responsive images work well on devices with widely differing screen sizes and resolutions. It uses resolution switching to display identical images with different sizes or resolutions and art direction to display different images to suit different display sizes. Progressive images are lazy loaded with a low resolution using a blur-up technique or traced placeholder SVG to speed up initial page load and save bandwidth.

Let’s find out how these image technologies can be implemented in Gatsby by leveraging gatsby-image.


Meet gatsby-image

Prerequisites

All code used in this blog can be found in my GitHub repository.

I assume that you’ve already installed a (recent) version of Node. If not, make sure you do. I’m using Node Version Manager (NVM) and currently Node 15.5.1 is selected. Next we need to install Gatsby:

npm install -g gatsby-cli

Verify the installation:

$ gatsby -v
Gatsby CLI version: 2.17.0

Create a new Gatsby project:

$ gatsby new gatsby-image-optimization

Start the new project:

$ gatsby develop

Open http://localhost:8000 to see the Gatsby default starter website. Now we’re ready to see gatsby-image in action.

Gatsby-image

The gatsby-image plugin utilizes two other components: gatsby-plugin-sharp and gatsby-transformer-sharp. The former uses the Sharp image processing library and is responsible for performing the actual image magic such as reducing image size with minimal loss of image quality. The latter is responsible for performing image resizing and cropping functions to create responsive images.

Note: try to delete the .cache folder within your project when changes to images are not correctly reflected within your application.

Our project is already initialized with the necessary plugins as seen in gatsby-config.js:

plugins: [
    "gatsby-plugin-sharp",
    "gatsby-transformer-sharp",
    {
      resolve: "gatsby-source-filesystem",
      options: {
        name: "images",
        path: "./src/images/",
      },
      __key: "images",
    },
  ]

The gatsby-source-filesystem plugin is required to source the images from the local filesystem into our application.

Now let’s find a nice photo from Pexels that we can use to demonstrate image optimization in Gatsby. I choose the large version of this image from Oliver Sjöström and copy it to our src/images folder. Next we’ll create the following sample page (in src/pages/image-optimization.js) that will act as a parent container for the images to be displayed:

import React from "react"

import ImageOptimization from "../components/image-optimization"

const ImageOptimizationPage = () => (
    <div
        style={{
            margin: `0 auto`,
            maxWidth: 960,
            padding: `0 1.0875rem 1.45rem`,
        }}>
        <div style={{maxWidth: `800px`, marginBottom: `1.45rem`}}>
            <ImageOptimization/>
        </div>
    </div>
)

export default ImageOptimizationPage

Then we’ll create a sample ImageOptimization component (in src/components/image-optimization.js) as follows:

import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"

const ImageOptimization = () => {
  const data = useStaticQuery(graphql`
    query {
      imageFixed: file(relativePath: { eq: "pexels-oliver-sjostrom.jpg" }) {
        childImageSharp {
          fixed(width: 1000) {
            ...GatsbyImageSharpFixed
          }
        }
      }
      imageFluid: file(relativePath: { eq: "pexels-oliver-sjostrom.jpg" }) {
        childImageSharp {
          fluid(maxWidth: 1000) {
            ...GatsbyImageSharpFluid
            ...GatsbyImageSharpFluidLimitPresentationSize
          }
        }
      }
      imageFluidTracedSVG: file(relativePath: { eq: "pexels-oliver-sjostrom.jpg" }) {
        childImageSharp {
          fluid(maxWidth: 1000) {
            ...GatsbyImageSharpFluid_tracedSVG
            ...GatsbyImageSharpFluidLimitPresentationSize
          }
        }
      }
      imageFluidNoBlur: file(relativePath: { eq: "pexels-oliver-sjostrom.jpg" }) {
        childImageSharp {
          fluid(maxWidth: 1000) {
            ...GatsbyImageSharpFluid_noBase64
          }
        }
      }
      imageFluidGrayscaleRotate: file(relativePath: { eq: "pexels-oliver-sjostrom.jpg" }) {
        childImageSharp {
          fluid(maxWidth: 1000, grayscale: true, rotate: 90) {
            ...GatsbyImageSharpFluid
          }
        }
      }
      imageFluidDuoTone: file(relativePath: { eq: "pexels-oliver-sjostrom.jpg" }) {
        childImageSharp {
          fluid(maxWidth: 1000, duotone: { highlight: "#44ffff", shadow: "#192550"}) {
            ...GatsbyImageSharpFluid
          }
        }
      }
      imageFluidMobile: file(relativePath: { eq: "pexels-oliver-sjostrom-mobile.jpg" }) {
        childImageSharp {
          fluid(maxWidth: 1000) {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  `)

  return <div>
    <Img fixed={data.imageFixed.childImageSharp.fixed} />
    <h4>Fixed image (blur-up effect).</h4>

    <Img fluid={data.imageFluid.childImageSharp.fluid} />
    <h4>Fluid image (blur-up effect).</h4>

    <Img fluid={data.imageFluidTracedSVG.childImageSharp.fluid} />
    <h4>Fluid image (traced placeholder SVG).</h4>

    <Img fluid={data.imageFluidNoBlur.childImageSharp.fluid} />
    <h4>Fluid image (without blur-up effect).</h4>

    <Img fluid={data.imageFluidGrayscaleRotate.childImageSharp.fluid} />
    <h4>Fluid image (grayscale and rotated).</h4>

    <Img fluid={data.imageFluidDuoTone.childImageSharp.fluid} />
    <h4>Fluid image (duotone).</h4>

    <Img fluid={[data.imageFluidMobile.childImageSharp.fluid,
      {
        ...data.imageFluid.childImageSharp.fluid,
        media: `(min-width: 768px)`,
      },
    ]} />
    <h4>Image art direction.</h4>
  </div>
}

export default ImageOptimization

This component contains the React code that renders the images based on the result of a GraphQL query. The result should look as follows:

All image optimization examples

Resolution switching

Our source image pexels-oliver-sjostrom.jpg has a resolution of 1920 x 1280 pixels. As we can see, all images in our web page are converted to smaller images with a width of 1000 pixels. The fixed image is always displayed with an absolute width of 1000 pixels, whereas the fluid image takes the width of its parent (div) which is 800 pixels in this case.

Fixed and fluid parent container difference

The imageFixed graphql query generates images with the specified width of 1000 pixels and additionally it tries to generate higher resolution images with widths of 1500 pixels and 2000 pixels (respectively 150% and 200% of the specified width). In our case the 2000 pixels width image will not be generated because the source image only has 1920 pixels available.

The imageFluid graphql query generates images of 25%, 50%, 100%, 150% and 200% of the specified maxWidth (which is 1000 pixels in our code). Also in this case the 2000 pixels width will not be generated.

The responsive HTML for each image is contained within a gatsby-image-wrapper container (div). For the fluid image the responsive HTML (explained in resolution switching) looks as follows:

<picture>
	<source srcset="/static/c587a3fcbf083a85b064d98cc12e814b/8f7df/pexels-oliver-sjostrom.jpg 250w,
	/static/c587a3fcbf083a85b064d98cc12e814b/0f3a1/pexels-oliver-sjostrom.jpg 500w,
	/static/c587a3fcbf083a85b064d98cc12e814b/a7715/pexels-oliver-sjostrom.jpg 1000w,
	/static/c587a3fcbf083a85b064d98cc12e814b/37d86/pexels-oliver-sjostrom.jpg 1500w,
	/static/c587a3fcbf083a85b064d98cc12e814b/d8255/pexels-oliver-sjostrom.jpg 1920w" sizes="(max-width: 1000px) 100vw, 1000px" />
	
	<img loading="lazy" sizes="(max-width: 1000px) 100vw, 1000px" 
         srcset="/static/c587a3fcbf083a85b064d98cc12e814b/8f7df/pexels-oliver-sjostrom.jpg 250w,
	/static/c587a3fcbf083a85b064d98cc12e814b/0f3a1/pexels-oliver-sjostrom.jpg 500w,
	/static/c587a3fcbf083a85b064d98cc12e814b/a7715/pexels-oliver-sjostrom.jpg 1000w,
	/static/c587a3fcbf083a85b064d98cc12e814b/37d86/pexels-oliver-sjostrom.jpg 1500w,
	/static/c587a3fcbf083a85b064d98cc12e814b/d8255/pexels-oliver-sjostrom.jpg 1920w" 
         src="/static/c587a3fcbf083a85b064d98cc12e814b/a7715/pexels-oliver-sjostrom.jpg" alt="" 
         style="position:absolute;top:0;left:0;opacity:1;width:100%;height:100%;object-fit:cover;object-position:center"/>
</picture>

When resizing the browser window, we can see that the fixed image remains the same width whereas the fluid image resizes to a smaller image (therefore being responsive).

Fixed and fluid resize difference

Progressive loading

Gatsby has built-in support for progressive image loading and uses a blur-up effect (default) or traced placeholder SVG to show a preview of the image while it loads. This way users will see the whole image partially loaded instead of seeing half of an image fully loaded. The screenshot below shows how it looks like when the images are partially loaded (but displayed as a whole). Note that the third image uses no effect. You can see this effect when refreshing the image-optimization page.

Progressive effects

Art direction

While resolution switching loads different sizes of the same image, art direction loads completely different images depending on the display size. Art direction can be applied by providing an array of fixed or fluid images along with a media key per image to gatsby-image’s fixed or fluid props, e.g. as follows:

<Img fluid={[data.imageFluidMobile.childImageSharp.fluid,
 {
   ...data.imageFluid.childImageSharp.fluid,
   media: `(min-width: 768px)`,
 },
]} />

In this example we’ve specified that the image pexels-oliver-sjostrom.jpg (retrieved by the imageFluid query) should be displayed when the viewport’s width is 768 pixels or more, otherwise the image pexels-oliver-sjostrom-mobile.jpg (retrieved by the imageFluidMobile query) should be displayed. In this way, a smaller image is shown on mobile devices that zooms in on the padel racket:

Art direction

Other cool features

The gatsby-plugin-sharp plugin supports many additional options for transforming images. Three of these options (that can be found in our code example) are grayscale, rotate and duotone:

Grayscale and duotone


Conclusion

The power of Gatsby has been demonstrated by showcasing its impressive image optimization capabilities. Gatsby offers clearly defined React components leveraging GraphQL and Sharp under the hood, making adding responsive and progressive images to a website a breeze. Gatsby-image’s slogan validated! All code in this post can be found in my GitHub repository.