Gatsby Headaches: Working With Media (Part 1)

Gatsby is an exceptionally flexible static site generator with a robust community and ecosystem to extend a Gatsby site with additional features. Did you know that there are more than a dozen image-related plugins alone? Working with media can become a real headache when you have to research plugins and choose the right one, particularly when we’re talking about lots of different image formats and different plugins for handling different formats. Discover how to incorporate a variety of media formats — images, videos, GIFs, SVGs, and so on — in a Gatsby website. Juan Rodriguez shares what he’s learned about optimizing files for improved performance after working with different plugins and techniques.

Working with media files in Gatsby might not be as straightforward as expected. I remember starting my first Gatsby project. After consulting Gatsby’s documentation, I discovered I needed to use the gatsby-source-filesystem plugin to make queries for local files. Easy enough!

That’s where things started getting complicated. Need to use images? Check the docs and install one — or more! — of the many, many plugins available for handling images. How about working with SVG files? There is another plugin for that. Video files? You get the idea.

It’s all great until any of those plugins or packages become outdated and go unmaintained. That’s where the headaches start.

If you are unfamiliar with Gatsby, it’s a React-based static site generator that uses GraphQL to pull structured data from various sources and uses webpack to bundle a project so it can then be deployed and served as static files. It’s essentially a static site generator with reactivity that can pull data from a vast array of sources.

Like many static site frameworks in the Jamstack, Gatsby has traditionally enjoyed a great reputation as a performant framework, although it has taken a hit in recent years. Based on what I’ve seen, however, it’s not so much that the framework is fast or slow but how the framework is configured to handle many of the sorts of things that impact performance, including media files.

So, let’s solve the headaches you might encounter when working with media files in a Gatsby project. This article is the first of a brief two-part series where we will look specifically at the media you are most likely to use: images, video, and audio. After that, the second part of this series will get into different types of files, including Markdown, PDFs, and even 3D models.

Solving Image Headaches In Gatsby

I think that the process of optimizing images can fall into four different buckets:

  1. Optimize image files.
    Minimizing an image’s file size without losing quality directly leads to shorter fetching times. This can be done manually or during a build process. It’s also possible to use a service, like Cloudinary, to handle the work on demand.
  2. Prioritize images that are part of the First Contentful Paint (FCP).
    FCP is a metric that measures the time between the point when a page starts loading to when the first bytes of content are rendered. The idea is that fetching assets that are part of that initial render earlier results in faster loading rather than waiting for other assets lower on the chain.
  3. Lazy loading other images.
    We can prevent the rest of the images from render-blocking other assets using the loading="lazy" attribute on images.
  4. Load the right image file for the right context.
    With responsive images, we can serve one version of an image file at one screen size and serve another image at a different screen size with the srcset and sizes attributes or with the <picture> element.

These are great principles for any website, not only those built with Gatsby. But how we build them into a Gatsby-powered site can be confusing, which is why I’m writing this article and perhaps why you’re reading it.

Lazy Loading Images In Gatsby

We can apply an image to a React component in a Gatsby site like this:

import * as React from "react"; import forest from "./assets/images/forest.jpg"; const ImageHTML = () => { return <img src={ forest } alt="Forest trail" />;
};

It’s important to import the image as a JavaScript module. This lets webpack know to bundle the image and generate a path to its location in the public folder.

This works fine, but when are we ever working with only one image? What if we want to make an image gallery that contains 100 images? If we try to load that many <img> tags at once, they will certainly slow things down and could affect the FCP. That’s where the third principle that uses the loading="lazy" attribute can come into play.

import * as React from "react"; import forest from "./assets/images/forest.jpg"; const LazyImageHTML = () => { return <img src={ forest } loading="lazy" alt="Forest trail" />;
};

We can do the opposite with loading="eager". It instructs the browser to load the image as soon as possible, regardless of whether it is onscreen or not.

import * as React from "react"; import forest from "./assets/images/forest.jpg"; const EagerImageHTML = () => { return <img src={ forest } loading="eager" alt="Forest trail" />;
};

Implementing Responsive Images In Gatsby

This is a basic example of the HTML for responsive images:

<img srcset="./assets/images/forest-400.jpg 400w, ./assets/images/forest-800.jpg 800w" sizes="(max-width: 500px) 400px, 800px" alt="Forest trail"
/>

In Gatsby, we must import the images first and pass them to the srcset attribute as template literals so webpack can bundle them:

import * as React from "react"; import forest800 from "./assets/images/forest-800.jpg"; import forest400 from "./assets/images/forest-400.jpg"; const ResponsiveImageHTML = () => { return ( <img srcSet={` ${ forest400 } 400w, ${ forest800 } 800w `} sizes="(max-width: 500px) 400px, 800px" alt="Forest trail" /> );
};

That should take care of any responsive image headaches in the future.

Loading Background Images In Gatsby

What about pulling in the URL for an image file to use on the CSS background-url property? That looks something like this:

import * as React from "react"; import "./style.css"; const ImageBackground = () => { return <div className="banner"></div>;
};
/* style.css */ .banner { aspect-ratio: 16/9; background-size: cover; background-image: url("./assets/images/forest-800.jpg"); /* etc. */
}

This is straightforward, but there is still room for optimization! For example, we can do the CSS version of responsive images, which loads the version we want at specific breakpoints.

/* style.css */ @media (max-width: 500px) { .banner { background-image: url("./assets/images/forest-400.jpg"); }
}

Using The gatsby-source-filesystem Plugin

Before going any further, I think it is worth installing the gatsby-source-filesystem plugin. It’s an essential part of any Gatsby project because it allows us to query data from various directories in the local filesystem, making it simpler to fetch assets, like a folder of optimized images.

npm i gatsby-source-filesystem

We can add it to our gatsby-config.js file and specify the directory from which we will query our media assets:

// gatsby-config.js module.exports = { plugins: [ { resolve: `gatsby-source-filesystem`, options: { name: `assets`, path: `${ __dirname }/src/assets`, }, }, ],
};

Remember to restart your development server to see changes from the gatsby-config.js file.

Now that we have gatsby-source-filesystem installed, we can continue solving a few other image-related headaches. For example, the next plugin we look at is capable of simplifying the cures we used for lazy loading and responsive images.

Using The gatsby-plugin-image Plugin

The gatsby-plugin-image plugin (not to be confused with the outdated gatsby-image plugin) uses techniques that automatically handle various aspects of image optimization, such as lazy loading, responsive sizing, and even generating optimized image formats for modern browsers.

Once installed, we can replace standard <img> tags with either the <GatsbyImage> or <StaticImage> components, depending on the use case. These components take advantage of the plugin’s features and use the <picture> HTML element to ensure the most appropriate image is served to each user based on their device and network conditions.

We can start by installing gatsby-plugin-image and the other plugins it depends on:

npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp

Let’s add them to the gatsby-config.js file:

// gatsby-config.js module.exports = {
plugins: [ // other plugins
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`], };

This provides us with some features we will put to use a bit later.

Using The StaticImage Component

The StaticImage component serves images that don’t require dynamic sourcing or complex transformations. It’s particularly useful for scenarios where you have a fixed image source that doesn’t change based on user interactions or content updates, like logos, icons, or other static images that remain consistent.

The main attributes we will take into consideration are:

  • src: This attribute is required and should be set to the path of the image you want to display.
  • alt: Provides alternative text for the image.
  • placeholder: This attribute can be set to either blurred or dominantColor to define the type of placeholder to display while the image is loading.
  • layout: This defines how the image should be displayed. It can be set to fixed for, as you might imagine, images with a fixed size, fullWidth for images that span the entire container, and constrained for images scaled down to fit their container.
  • loading: This determines when the image should start loading while also supporting the eager and lazy options.

Using StaticImage is similar to using a regular HTML <img> tag. However, StaticImage requires passing the string directly to the src attribute so it can be bundled by webpack.

import * as React from "react"; import { StaticImage } from "gatsby-plugin-image"; const ImageStaticGatsby = () => { return ( <StaticImage src="./assets/images/forest.jpg" placeholder="blurred" layout="constrained" alt="Forest trail" loading="lazy" /> ); };

The StaticImage component is great, but you have to take its constraints into account:

  • No Dynamically Loading URLs
    One of the most significant limitations is that the StaticImage component doesn’t support dynamically loading images based on URLs fetched from data sources or APIs.
  • Compile-Time Image Handling
    The StaticImage component’s image handling occurs at compile time. This means that the images you specify are processed and optimized when the Gatsby site is built. Consequently, if you have images that need to change frequently based on user interactions or updates, the static nature of this component might not fit your needs.
  • Limited Transformation Options
    Unlike the more versatile GatsbyImage component, the StaticImage component provides fewer transformation options, e.g., there is no way to apply complex transformations like cropping, resizing, or adjusting image quality directly within the component. You may want to consider alternative solutions if you require advanced transformations.

Using The GatsbyImage Component

The GatsbyImage component is a more versatile solution that addresses the limitations of the StaticImage component. It’s particularly useful for scenarios involving dynamic image loading, complex transformations, and advanced customization.

Some ideal use cases where GatsbyImage is particularly useful include:

  • Dynamic Image Loading
    If you need to load images dynamically based on data from APIs, content management systems, or other sources, the GatsbyImage component is the go-to choice. It can fetch images and optimize their loading behavior.
  • Complex transformations
    The GatsbyImage component is well-suited for advanced transformations, using GraphQL queries to apply them.
  • Responsive images
    For responsive design, the GatsbyImage component excels by automatically generating multiple sizes and formats of an image, ensuring that users receive an appropriate image based on their device and network conditions.

Unlike the StaticImage component, which uses a src attribute, GatsbyImage has an image attribute that takes a gatsbyImageData object. gatsbyImageData contains the image information and can be queried from GraphQL using the following query.

query { file(name: { eq: "forest" }) { childImageSharp { gatsbyImageData(width: 800, placeholder: BLURRED, layout: CONSTRAINED) } name }
}

If you’re following along, you can look around your Gatsby data layer at http://localhost:8000/___graphql.

From here, we can use the useStaticQuery hook and the graphql tag to fetch data from the data layer:

import * as React from "react"; import { useStaticQuery, graphql } from "gatsby"; import { GatsbyImage, getImage } from "gatsby-plugin-image"; const ImageGatsby = () => { // Query data here: const data = useStaticQue(graphql``); return <div></div>;
};

Next, we can write the GraphQL query inside of the graphql tag:

import * as React from "react"; import { useStaticQuery, graphql } from "gatsby"; const ImageGatsby = () => { const data = useStaticQuery(graphql` query { file(name: { eq: "forest" }) { childImageSharp { gatsbyImageData(width: 800, placeholder: BLURRED, layout: CONSTRAINED) } name } } `); return <div></div>;
};

Next, we import the GatsbyImage component from gatsby-plugin-image and assign the image’s gatsbyImageData property to the image attribute:

import * as React from "react"; import { useStaticQuery, graphql } from "gatsby"; import { GatsbyImage } from "gatsby-plugin-image"; const ImageGatsby = () => { const data = useStaticQuery(graphql` query { file(name: { eq: "forest" }) { childImageSharp { gatsbyImageData(width: 800, placeholder: BLURRED, layout: CONSTRAINED) } name } } `); return <GatsbyImage image={ data.file.childImageSharp.gatsbyImageData } alt={ data.file.name } />;
};

Now, we can use the getImage helper function to make the code easier to read. When given a File object, the function returns the file.childImageSharp.gatsbyImageData property, which can be passed directly to the GatsbyImage component.

import * as React from "react"; import { useStaticQuery, graphql } from "gatsby"; import { GatsbyImage, getImage } from "gatsby-plugin-image"; const ImageGatsby = () => { const data = useStaticQuery(graphql` query { file(name: { eq: "forest" }) { childImageSharp { gatsbyImageData(width: 800, placeholder: BLURRED, layout: CONSTRAINED) } name } } `); const image = getImage(data.file); return <GatsbyImage image={ image } alt={ data.file.name } />;
};

Using The gatsby-background-image Plugin

Another plugin we could use to take advantage of Gatsby’s image optimization capabilities is the gatsby-background-image plugin. However, I do not recommend using this plugin since it is outdated and prone to compatibility issues. Instead, Gatsby suggests using gatsby-plugin-image when working with the latest Gatsby version 3 and above.

If this compatibility doesn’t represent a significant problem for your project, you can refer to the plugin’s documentation for specific instructions and use it in place of the CSS background-url usage I described earlier.

Solving Video And Audio Headaches In Gatsby

Working with videos and audio can be a bit of a mess in Gatsby since it lacks plugins for sourcing and optimizing these types of files. In fact, Gatsby’s documentation doesn’t name or recommend any official plugins we can turn to.

That means we will have to use vanilla methods for videos and audio in Gatsby.

Using The HTML video Element

The HTML video element is capable of serving different versions of the same video using the <source> tag, much like the img element uses the srset attribute to do the same for responsive images.

That allows us to not only serve a more performant video format but also to provide a fallback video for older browsers that may not support the bleeding edge:

import * as React from "react"; import natureMP4 from "./assets/videos/nature.mp4"; import natureWEBM from "./assets/videos/nature.webm"; const VideoHTML = () => { return ( <video controls> <source src={ natureMP4 } type="video/mp4" /> <source src={ natureWEBM } type="video/webm" /> </video> );
}; P;

We can also apply lazy loading to videos like we do for images. While videos do not support the loading="lazy" attribute, there is a preload attribute that is similar in nature. When set to none, the attribute instructs the browser to load a video and its metadata only when the user interacts with it. In other words, it’s lazy-loaded until the user taps or clicks the video.

We can also set the attribute to metadata if we want the video’s details, such as its duration and file size, fetched right away.

<video controls preload="none"> <source src={ natureMP4 } type="video/mp4" /> <source src={ natureWEBM } type="video/webm" />
</video>

Note: I personally do not recommend using the autoplay attribute since it is disruptive and disregards the preload attribute, causing the video to load right away.

And, like images, display a placeholder image for a video while it is loading with the poster attribute pointing to an image file.

<video controls preload="none" poster={ forest }> <source src={ natureMP4 } type="video/mp4" /> <source src={ natureWEBM } type="video/webm" />
</video>

Using The HTML audio Element

The audio and video elements behave similarly, so adding an audio element in Gatsby looks nearly identical, aside from the element:

import * as React from "react"; import audioSampleMP3 from "./assets/audio/sample.mp3"; import audioSampleWAV from "./assets/audio/sample.wav"; const AudioHTML = () => { return ( <audio controls> <source src={ audioSampleMP3 } type="audio/mp3" /> <source src={ audioSampleWAV } type="audio/wav" /> </audio> );
};

As you might expect, the audio element also supports the preload attribute:

<audio controls preload="none"> <source src={ audioSampleMP3 } type="audio/mp3" /> <source src={a udioSampleWAV } type="audio/wav" />
</audio>

This is probably as good as we can do to use videos and images in Gatsby with performance in mind, aside from saving and compressing the files as best we can before serving them.

Solving iFrame Headaches In Gatsby

Speaking of video, what about ones embedded in an <iframe> like we might do with a video from YouTube, Vimeo, or some other third party? Those can certainly lead to performance headaches, but it’s not as we have direct control over the video file and where it is served.

Not all is lost because the HTML iframe element supports lazy loading the same way that images do.

import * as React from "react"; const VideoIframe = () => { return ( <iframe src="https://www.youtube.com/embed/jNQXAC9IVRw" title="Me at the Zoo" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen loading="lazy" /> );
};

Embedding a third-party video player via iframe can possibly be an easier path than using the HTML video element. iframe elements are cross-platform compatible and could reduce hosting demands if you are working with heavy video files on your own server.

That said, an iframe is essentially a sandbox serving a page from an outside source. They’re not weightless, and we have no control over the code they contain. There are also GDPR considerations when it comes to services (such as YouTube) due to cookies, data privacy, and third-party ads.

Solving SVG Headaches In Gatsby

SVGs contribute to improved page performance in several ways. Their vector nature results in a much smaller file size compared to raster images, and they can be scaled up without compromising quality. And SVGs can be compressed with GZIP, further reducing file sizes.

That said, there are several ways that we can use SVG files. Let’s tackle each one in the contact of Gatsby.

Using Inline SVG

SVGs are essentially lines of code that describe shapes and paths, making them lightweight and highly customizable. Due to their XML-based structure, SVG images can be directly embedded within the HTML <svg> tag.

import * as React from "react"; const SVGInline = () => { return ( <svg viewBox="0 0 24 24" fill="#000000"> <!-- etc. --> </svg> ); };

Just remember to change certain SVG attributes, such as xmlns:xlink or xlink:href, to JSX attribute spelling, like xmlnsXlink and xlinkHref, respectively.

Using SVG In img Elements

An SVG file can be passed into an img element’s src attribute like any other image file.

import * as React from "react"; import picture from "./assets/svg/picture.svg"; const SVGinImg = () => { return <img src={ picture } alt="Picture" />;
};

Loading SVGs inline or as HTML images are the de facto approaches, but there are React and Gatsby plugins capable of simplifying the process, so let’s look at those next.

Inlining SVG With The react-svg Plugin

react-svg provides an efficient way to render SVG images as React components by swapping a ReactSVG component in the DOM with an inline SVG.

Once installing the plugin, import the ReactSVG component and assign the SVG file to the component’s src attribute:

import * as React from "react"; import { ReactSVG } from "react-svg"; import camera from "./assets/svg/camera.svg"; const SVGReact = () => { return <ReactSVG src={ camera } />;
};

Using The gatsby-plugin-react-svg Plugin

The gatsby-plugin-react-svg plugin adds svg-react-loader to your Gatsby project’s webpack configuration. The plugin adds a loader to support using SVG files as React components while bundling them as inline SVG.

Once the plugin is installed, add it to the gatsby-config.js file. From there, add a webpack rule inside the plugin configuration to only load SVG files ending with a certain filename, making it easy to split inline SVGs from other assets:

// gatsby-config.js module.exports = { plugins: [ { resolve: "gatsby-plugin-react-svg", options: { rule: { include: /\.inline\.svg$/, }, }, }, ],
};

Now we can import SVG files like any other React component:

import * as React from "react"; import Book from "./assets/svg/book.inline.svg"; const GatsbyPluginReactSVG = () => { return <Book />;
};

And just like that, we can use SVGs in our Gatsby pages in several different ways!

Conclusion

Even though I personally love Gatsby, working with media files has given me more than a few headaches.

As a final tip, when needing common features such as images or querying from your local filesystem, go ahead and install the necessary plugins. But when you need a minor feature, try doing it yourself with the methods that are already available to you!

If you have experienced different headaches when working with media in Gatsby or have circumvented them with different approaches than what I’ve covered, please share them! This is a big space, and it’s always helpful to see how others approach similar challenges.

Again, this article is the first of a brief two-part series on curing headaches when working with media files in a Gatsby project. The following article will be about avoiding headaches when working with different media files, including Markdown, PDFs, and 3D models.

Further Reading

Smashing Editorial
(gg, yk)