Gatsby Headaches: Working With Media (Part 1)
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 GatsbyI think that the process of optimizing images can fall into four different buckets:
- 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. - 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. - Lazy loading other images.
We can prevent the rest of the images from render-blocking other assets using theloading="lazy"
attribute on images. - 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 thesrcset
andsizes
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 eitherblurred
ordominantColor
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 tofixed
for, as you might imagine, images with a fixed size,fullWidth
for images that span the entire container, andconstrained
for images scaled down to fit their container.loading
: This determines when the image should start loading while also supporting theeager
andlazy
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 theStaticImage
component doesn’t support dynamically loading images based on URLs fetched from data sources or APIs. - Compile-Time Image Handling
TheStaticImage
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 versatileGatsbyImage
component, theStaticImage
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, theGatsbyImage
component is the go-to choice. It can fetch images and optimize their loading behavior. - Complex transformations
TheGatsbyImage
component is well-suited for advanced transformations, using GraphQL queries to apply them. - Responsive images
For responsive design, theGatsbyImage
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(graphqlquery {
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(graphqlquery {
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(graphqlquery {
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.
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 GatsbySpeaking 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.
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!
ConclusionEven 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
- “Demystifying The New Gatsby Framework”
- “Gatsby Headaches And How To Cure Them: i18n (Part 1)”
- “Gatsby Headaches And How To Cure Them: i18n (Part 2)”