close up of a high-end camera lens.

How to optimise remote images in Gatsby

In this tutorial, we're taking a look at how we can optimise external images to use in Gatsby websites to improve page load times, all whilst keeping those pixels looking super sharp.

GatsbyJS is awesome for creating fast, scalable, static websites that source content from anywhere. Whilst a lot of the major plugins for feeding data into your site support the Gatsby image optimisation plugin (gatsby-plugin-image) out-of-the-box, it’s not the case for all. Luckily for us, it’s simple to transform such images and make them utilise that optimising goodness! 🤤

Why bother?

In the relentless pursuit of owning a perfect Lighthouse score for your website, having properly optimised images is only going to help. The file sizes are smaller overall, so it’s quicker to load a webpage on devices with a dodgy internet connection. Also, using responsive images that are sized according to the device width, is going to ensure your images look great on all screen sizes. i.e. who wants to wait for an 8K image to load on their 4-inch iPhone SE? If you want to follow the best practices and recommendations for building a modern website, your images have got to be optimised. So let’s get it working for Gatsby!

Update project dependencies

In order to get things working, we need to first update our project dependencies. Run the following command in a Gatsby project directory to get all the necessary packages for optimising images:

yarn add gatsby-plugin-image gatsby-plugin-sharp gatsby-source-filesystem gatsby-transformer-sharp
yarn add gatsby-plugin-image gatsby-plugin-sharp gatsby-source-filesystem gatsby-transformer-sharp

And just in case you’re wondering, here’s what each plugin does:
gatsby-plugin-image creates responsive, optimised images for our Gatsby site.
gatsby-source-filesystem creates file nodes from our remote images.
gatsby-plugin-sharp handles the processing of our images.
gatsby-transformer-sharp creates the necessary nodes we can query with GraphQL for our site in conjunction with gatsby-source-filesystem.

Next, we should get our source plugin where we’ll pull content into our Gatsby site. For this tutorial we’ll use gatsby-source-ghost to pull content from a Ghost blog, but feel free to use any other source plugin.

yarn add gatsby-source-ghost
yarn add gatsby-source-ghost

Alternatively, check out this GitHub repo where you’ll find all the dependencies and the completed example project.

Now we just need to add our new plugins to the list of plugins in gatsby-config.js:

gatsby-config.js
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
  siteMetadata: {
    siteUrl: "https://www.example.tld",
    title: "gatsby-optimise-remote-images",
  },
  plugins: [
    "gatsby-plugin-image",
    "gatsby-plugin-sharp",
    "gatsby-transformer-sharp",
    {
      resolve: "gatsby-source-ghost",
      options: {
        apiUrl: process.env.GHOST_API_URL,
        contentApiKey: process.env.GHOST_API_KEY,
        version: "v3",
      },
    },
  ],
};
gatsby-config.js
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
  siteMetadata: {
    siteUrl: "https://www.example.tld",
    title: "gatsby-optimise-remote-images",
  },
  plugins: [
    "gatsby-plugin-image",
    "gatsby-plugin-sharp",
    "gatsby-transformer-sharp",
    {
      resolve: "gatsby-source-ghost",
      options: {
        apiUrl: process.env.GHOST_API_URL,
        contentApiKey: process.env.GHOST_API_KEY,
        version: "v3",
      },
    },
  ],
};

Create remote nodes for GraphQL queries

Now with our dependencies installed, we can move on to creating the necessary nodes for our optimised remote images. With this, we can include the nodes in our GraphQL queries and access the necessary data to feed into the GatsbyImage component, which will optimise our images.

Create the file gatsby-node.js if you don’t have it already and add in the following code. Peep the comments if you want to know what’s going on! 👀

gatsby-node.js
const { createRemoteFileNode } = require("gatsby-source-filesystem"); // We'll use this to create the file nodes from the remote images
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;
 
  // creates a relationship between GhostPost and the File node for the optimized image
  createTypes(`
    type GhostPost implements Node {
      remote_image: File @link
    }
  `); // change "GhostPost" to whatever type you're using from your source plugin
};
exports.onCreateNode = async ({
  actions: { createNode },
  getCache,
  createNodeId,
  node,
}) => {
  // we need to verify that we're using the correct node created by our source plugin so we check its type and if it has a value
  if (node.internal.type === `GhostPost` && node.feature_image !== null) {
    // create the file node
    const fileNode = await createRemoteFileNode({
      url: node.feature_image, // URL of the remote image
      getCache, // Gatsby cache
      createNode, // helper function to generate the node
      createNodeId, // helper function to generate the node ID
      parentNodeId: node.id, // id of the parent node of the file
      node,
    });
    // if the file node was created, attach the new node
    if (fileNode) {
      node.remote_image = fileNode.id;
    }
  }
};
gatsby-node.js
const { createRemoteFileNode } = require("gatsby-source-filesystem"); // We'll use this to create the file nodes from the remote images
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;
 
  // creates a relationship between GhostPost and the File node for the optimized image
  createTypes(`
    type GhostPost implements Node {
      remote_image: File @link
    }
  `); // change "GhostPost" to whatever type you're using from your source plugin
};
exports.onCreateNode = async ({
  actions: { createNode },
  getCache,
  createNodeId,
  node,
}) => {
  // we need to verify that we're using the correct node created by our source plugin so we check its type and if it has a value
  if (node.internal.type === `GhostPost` && node.feature_image !== null) {
    // create the file node
    const fileNode = await createRemoteFileNode({
      url: node.feature_image, // URL of the remote image
      getCache, // Gatsby cache
      createNode, // helper function to generate the node
      createNodeId, // helper function to generate the node ID
      parentNodeId: node.id, // id of the parent node of the file
      node,
    });
    // if the file node was created, attach the new node
    if (fileNode) {
      node.remote_image = fileNode.id;
    }
  }
};

Now we can access our new remote images for optimisation in our GraphQL queries! 🎉 Put simply, all that we’re doing here is downloading the remote images to the filesystem, so that Gatsby can use the created files and optimise our images for us.

Usage in pages

Adding the optimised images to our site is exactly the same procedure we would use for adding a locally sourced image. We just need to update the GraphQL query. Let’s test this out by adding a list of our blog posts to the home page, with their featured images displayed as well. This is where we’ll make use of the GatsbyImage component. Replace everything in src/pages/index.js with this:

src/pages/index.js
import * as React from "react";
import { Link, graphql } from "gatsby";
import { GatsbyImage } from "gatsby-plugin-image";
const IndexPage = ({ data }) => {
  const blogPosts = data.allGhostPost.edges;
  return (
    <>
      <h1>Blog Posts</h1>
      <div>
        <ul>
          {blogPosts.map((post, i) => (
            <li key={i}>
              <Link to={post.node.slug}>
                {/* GatsbyImage component to render our optimised image */}
                <GatsbyImage
                  image={post.node.remote_image.childImageSharp.gatsbyImageData}
                  alt={post.node.title}
                />
                <p>{post.node.title}</p>
              </Link>
            </li>
          ))}
        </ul>
      </div>
    </>
  );
};
export default IndexPage;
export const IndexQuery = graphql`
  query blogListQuery {
    allGhostPost(sort: { fields: [published_at], order: DESC }) {
      edges {
        node {
          slug
          title
          published_at(formatString: "DD MMMM YYYY")
          remote_image {
            childImageSharp {
              # this is the field which we'll pass into the GatsbyImage component
              # we add the DOMINANT_COLOR placeholder to make a nice effect when lazy loading
              gatsbyImageData(placeholder: DOMINANT_COLOR)
            }
          }
        }
      }
    }
  }
`;
src/pages/index.js
import * as React from "react";
import { Link, graphql } from "gatsby";
import { GatsbyImage } from "gatsby-plugin-image";
const IndexPage = ({ data }) => {
  const blogPosts = data.allGhostPost.edges;
  return (
    <>
      <h1>Blog Posts</h1>
      <div>
        <ul>
          {blogPosts.map((post, i) => (
            <li key={i}>
              <Link to={post.node.slug}>
                {/* GatsbyImage component to render our optimised image */}
                <GatsbyImage
                  image={post.node.remote_image.childImageSharp.gatsbyImageData}
                  alt={post.node.title}
                />
                <p>{post.node.title}</p>
              </Link>
            </li>
          ))}
        </ul>
      </div>
    </>
  );
};
export default IndexPage;
export const IndexQuery = graphql`
  query blogListQuery {
    allGhostPost(sort: { fields: [published_at], order: DESC }) {
      edges {
        node {
          slug
          title
          published_at(formatString: "DD MMMM YYYY")
          remote_image {
            childImageSharp {
              # this is the field which we'll pass into the GatsbyImage component
              # we add the DOMINANT_COLOR placeholder to make a nice effect when lazy loading
              gatsbyImageData(placeholder: DOMINANT_COLOR)
            }
          }
        }
      }
    }
  }
`;

And that’s it! Run yarn develop in your terminal and navigate to http://localhost:8000 to see it all in action. Don’t forget to add some styles to make it all look 🔥