Media Files in Gatsby: Where the Confusion Starts

Gatsby bills itself as a flexible static site generator, and that flexibility extends to media handling — perhaps a little too far. A first encounter with Gatsby's image ecosystem often goes like this: you consult the docs, install gatsby-source-filesystem to query local files, and then realize that images require one plugin, SVGs require another, and videos require yet another. The number of available image-related plugins alone exceeds a dozen.

The real pain begins when those plugins fall out of maintenance. Outdated packages become a source of friction, and what should be a straightforward task — placing an image on a page — turns into a research project.

Gatsby itself is a React-based static site generator that relies on GraphQL to pull structured data from multiple sources and webpack to bundle the output into deployable static files. That architecture has traditionally earned it a strong reputation for performance in the Jamstack world, although more recent developer surveys show that reputation has slipped. In practice, Gatsby's speed is less about the framework itself and more about how it is configured — particularly when it comes to media files, which are often the heaviest assets on a page.

This is the first of a two-part look at handling media in Gatsby, focused on the formats you are most likely to encounter: images, video, and audio. The second part covers other file types, including Markdown, PDFs, and 3D models.

When Images Start to Weigh the Site Down

Image optimization for a Gatsby project falls into four practical buckets: shrinking file sizes, prioritizing images above the fold, lazy loading the rest, and serving the correct file for the viewport via srcset, sizes, or the <picture> element. These rules apply to any site, but Gatsby's build and data layer shape how you implement them.

Direct Imports and Native Lazy Loading

The simplest way to get an image into a Gatsby component is to import it as a module so webpack bundles it and resolves the path:

import * as React from "react";

import forest from "./assets/images/forest.jpg";

const ImageHTML = () => {
  return <img src={ forest } alt="Forest trail" />;
};

A single image is easy. A gallery with 100 is not. Uncontrolled, all those <img> requests compete with initial render. The browser-native fix is the loading attribute:

import * as React from "react";

import forest from "./assets/images/forest.jpg";

const LazyImageHTML = () => {
  return <img src={ forest } alt="Forest trail" />;
};

Adding loading="lazy" defers offscreen requests. Flip it to loading="eager" to force immediate loading for above-the-fold images:

import * as React from "react";

import forest from "./assets/images/forest.jpg";

const EagerImageHTML = () => {
  return <img src={ forest } alt="Forest trail" />;
};

Responsive Sources, Manually

For responsive images, the standard HTML pattern relies on srcset and sizes:

<img
 
 
  alt="Forest trail"
/>

In Gatsby, imported images go into a template literal so webpack processes each candidate file:

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

      `}
     
      alt="Forest trail"
    />
  );
};

You can take the same approach for CSS background images by importing the URL and assigning it to background:

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. */
}

To serve different background crops at different breakpoints, use a media query for each variant:

/* style.css */

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

Sourcing Assets from the Filesystem

Before moving to plugin-based magic, add gatsby-source-filesystem to unlock queries against local directories. Install it, then register it in gatsby-config.js with the path to your media folder:

npm i gatsby-source-filesystem
// gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,

      options: {
        name: `assets`,

        path: `${ __dirname }/src/assets`,
      },
    },
  ],
};

Restart the development server after editing gatsby-config.js; otherwise the new source won't appear in the data layer.

The gatsby-plugin-image Upgrade Path

The modern gatsby-plugin-image plugin (not the older gatsby-image) automates lazy loading, responsive sizing, and format negotiation. It exposes two components: StaticImage for fixed, compile-time images, and GatsbyImage for anything dynamic.

Install the plugin and its peer dependencies:

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

Then declare all of them in gatsby-config.js:

// gatsby-config.js

module.exports = {
plugins: [

// other plugins
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`],

};

StaticImage: Fast but Rigid

StaticImage works like an <img> tag with Gatsby's pipeline behind it. Key props include a required src, alt, a placeholder of blurred or dominantColor, a layout of fixed, fullWidth, or constrained, and loading with eager or lazy. The src must be a literal string for 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"
     
    />
  );
  };

The tradeoffs are immediate. No dynamic URLs — the image is resolved at build time. Transformations are limited; you cannot crop, resize, or adjust quality from inside the component. If an image changes with user interaction or CMS content, StaticImage won't fit.

GatsbyImage: Dynamic and Queryable

GatsbyImage removes those constraints. It consumes an image prop holding a gatsbyImageData object, which you pull from GraphQL. Ideal for API-driven content, rich transformations, and automatic responsive variants. A query for an image file looks like this:

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

    name
  }
}

Poke around the GraphQL playground at http://localhost:8000/___graphql to inspect your data layer. Fetch the data in a component with useStaticQuery and the graphql tag:

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>;
};

Write the query to select gatsbyImageData:

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>;
};

Then pass the result to the component:

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 } />;
};

For cleaner code, the getImage helper extracts file.childImageSharp.gatsbyImageData from a File object and hands it straight to GatsbyImage:

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 } />;
};

A Note on gatsby-background-image

The gatsby-background-image plugin exists, but it is outdated and skittish with current Gatsby versions. The project's own guidance is to use gatsby-plugin-image for Gatsby 3 and up. If you must use it, the plugin documentation covers setup — but treat it as a legacy option rather than a default.

Handling Video and Audio in Gatsby Without Plugins

Gatsby’s documentation offers little guidance for working with video and audio files — there are no official plugins for sourcing, optimizing, or transforming these media types. That leaves the HTML video and audio elements as the most practical route.

The video Element

The HTML video element supports multiple sources via nested <source> tags, similar to how srcset works for responsive images. This lets you serve a modern format like WebM while providing a fallback for older browsers:

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;

Lazy loading works differently for videos than for images. The loading="lazy" attribute is not supported on video, but the preload attribute achieves a similar effect. Setting preload="none" tells the browser to fetch the video and its metadata only after the user interacts with it. If you need duration and file size available immediately, use preload="metadata" instead.

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

  <source src={ natureWEBM } type="video/webm" />
</video>

Note: Avoid the autoplay attribute. It overrides preload and forces the video to load immediately, which is disruptive and defeats lazy-loading efforts.

The poster attribute can display a placeholder image while the video loads:

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

  <source src={ natureWEBM } type="video/webm" />
</video>

The audio Element

The audio element works almost identically to video, differing only in the element name and supported attributes:

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>
  );
};

It also supports the preload attribute with the same behavior:

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

  <source src={a udioSampleWAV } type="audio/wav" />
</audio>

Without dedicated Gatsby plugins, the best performance strategy for video and audio is to compress source files before adding them to your project, then use preload and poster to control what gets fetched and when.

Lazy-Loading Embedded iFrames

Third-party embeds from YouTube, Vimeo, or other services come with their own constraints — you don’t control the video file or its hosting. But the iframe element supports native lazy loading:

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
     
    />
  );
};

An embedded iframe can be simpler than hosting video files yourself, and it’s cross-platform compatible. That said, iframes are sandboxes running external code; they carry their own overhead and you have no control over their payload. GDPR concerns also apply — services like YouTube may set cookies or serve third-party ads, which has privacy implications.

SVG Options in Gatsby

SVGs offer real performance advantages: vector files are typically much smaller than raster images, scale without quality loss, and compress well with GZIP. There are several ways to bring SVGs into a Gatsby project, each with trade-offs.

Inline SVG in JSX

Because SVG is XML-based, you can drop the markup directly into a component with an <svg> element:

import * as React from "react";

const SVGInline = () => {

  return (

    <svg viewBox="0 0 24 24" fill="#000000">

      <!-- etc. -->

    </svg>

  );

};

When embedding inline, remember that certain SVG attributes need JSX naming conventions — for example, xmlns:xlink becomes xmlnsXlink, and xlink:href becomes xlinkHref.

SVG in an img Element

An SVG file works fine as the src of a standard img element, just like any other image format:

import * as React from "react";

import picture from "./assets/svg/picture.svg";

const SVGinImg = () => {
  return <img src={ picture } alt="Picture" />;
};

Inline SVG and img are the baseline approaches. Two plugins can simplify the process if you prefer component-based usage.

The react-svg Plugin

react-svg turns SVG files into React components by replacing a ReactSVG component in the DOM with the actual inline SVG markup. After installing it, import ReactSVG and pass the SVG file to its src prop:

import * as React from "react";

import { ReactSVG } from "react-svg";

import camera from "./assets/svg/camera.svg";

const SVGReact = () => {
  return <ReactSVG src={ camera } />;
};

The gatsby-plugin-react-svg Plugin

This plugin integrates svg-react-loader into Gatsby’s webpack configuration. With it, you can import SVG files directly as React components and have them bundled inline. After installation, add the plugin to gatsby-config.js and define a webpack rule to separate inline SVGs from other image assets:

// gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: "gatsby-plugin-react-svg",

      options: {
        rule: {
          include: /\.inline\.svg$/,
        },
      },
    },
  ],
};

Once configured, SVG imports behave like any other component:

import * as React from "react";

import Book from "./assets/svg/book.inline.svg";

const GatsbyPluginReactSVG = () => {
  return <Book />;
};

These approaches give you flexibility in how SVGs are handled — inline markup, standard image references, or component-based imports — depending on the needs of each page.