Shipping Faster Gatsby Blogs with gatsby-plugin-image: What Actually Changes
gatsby-plugin-image moves image optimization to build time: resized WebP/AVIF variants, lazy loading, and placeholders. Here's a working blog setup, the build-time and Sharp trade-offs, and how to verify the LCP win.
12 Dec 2025, 21:10 UTC

If your Gatsby blog's hero image is a 2 MB JPEG straight out of your camera roll, your Largest Contentful Paint (LCP) is paying for it on every mobile visit. The fix most Gatsby sites should reach for first is gatsby-plugin-image: it generates resized, modern-format variants at build time and lazy-loads them, so the browser only downloads what the current viewport needs. This post walks through what the plugin actually does, a working setup for a blog post image, and the trade-offs you'll want to know about before committing.
What the plugin does at build time
Gatsby's image pipeline is build-time, not request-time. When you run gatsby build, the plugin (backed by the Sharp image processing library) reads your source images and emits multiple resized files — typically WebP and AVIF alongside a fallback format — into the public folder. At runtime, the <GatsbyImage> component renders a <picture> element with srcset and sizes attributes, so the browser picks the smallest adequate file. It also handles lazy loading and a placeholder (blurred or dominant-color) so the page doesn't jump around while the image loads.
The key mental model: you pay the processing cost once, during the build, and every visitor benefits. There's no image server to run.
Setting it up for a blog post image
Assumptions: Gatsby 5, Node 18+, and images stored locally in your project (e.g., in src/images or alongside Markdown posts). Remote image URLs need extra handling — gatsby-source-filesystem does not fetch them for you.
Install the three packages (run from your project root, no special permissions needed):
npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharpThen register them in gatsby-config.js and make sure your images are sourced:
module.exports = {
plugins: [
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
{
resolve: `gatsby-source-filesystem`,
options: { name: `images`, path: `${__dirname}/src/images` },
},
],
}Query the image in your page or template with a gatsbyImageData field. A typical fragment for a post's featured image:
query PostBySlug($id: String!) {
markdownRemark(id: { eq: $id }) {
frontmatter {
title
featuredImage {
childImageSharp {
gatsbyImageData(
width: 1200
placeholder: BLURRED
formats: [AUTO, WEBP, AVIF]
)
}
}
}
}
}And render it:
import { GatsbyImage, getImage } from "gatsby-plugin-image"
const Post = ({ data }) => {
const image = getImage(data.markdownRemark.frontmatter.featuredImage)
return (
<article>
<GatsbyImage image={image} alt={data.markdownRemark.frontmatter.title} />
<h1>{data.markdownRemark.frontmatter.title}</h1>
</article>
)
}getImage safely unwraps the nested node; formats: [AUTO, WEBP, AVIF] keeps the original format as a fallback for older browsers while serving AVIF or WebP to modern ones. The width: 1200 caps the largest generated variant — set it to the widest your layout will ever display the image.
Trade-offs worth knowing before you commit
The main cost is build time. Every image × every width × every format is a Sharp operation, so a blog with hundreds of large photos can see noticeably longer builds and a much bigger public (and .cache) folder. Mitigations: cap width sensibly, drop AVIF if build time matters more than the extra compression, and let CI cache the .cache directory between builds.
Second, Sharp ships native binaries matched to your OS and CPU architecture. This mostly works out of the box, but mismatches surface in CI (e.g., installing on macOS ARM and deploying to a Linux x64 container from a committed node_modules, or cross-architecture Docker builds). The fix is usually to install dependencies inside the build environment rather than copying node_modules across machines.
Third, the placeholder and lazy-loading behavior is great below the fold but can hurt LCP if applied to your hero image. For the image most likely to be your LCP element, pass loading="eager" and consider fetchpriority="high" via the component's passthrough props, so the browser prioritizes it instead of deferring it.
How to verify it's actually working
Don't trust the config — check the output:
- Run
gatsby buildlocally, then look inpublicfor generated files with width suffixes and.webp/.avifextensions. If only the original JPEG exists, the transformer isn't picking up your images. - Run
gatsby developor serve the build, open Chrome DevTools → Network, and throttle to Slow 3G. Confirm the browser downloads an appropriately sized variant (not the full-resolution original) and that below-fold images only request after you scroll toward them. - Run a Lighthouse audit in DevTools before and after the change and compare LCP. On a typical image-heavy blog page, moving from an unoptimized multi-megabyte hero to sized WebP/AVIF variants commonly shaves LCP from the 3-second range down toward 2 seconds on throttled mobile — but measure your own page, since layouts and image weights vary.
The bottom line
gatsby-plugin-image is one of the highest-leverage changes you can make to a Gatsby content site: a few lines of config and a GraphQL fragment get you responsive sizing, modern formats, and lazy loading. Budget for longer builds on image-heavy sites, keep Sharp's native binaries in mind for CI, and mark your LCP candidate as eager. Then verify with a throttled Network tab and a Lighthouse run — the improvement should be visible in the metrics, not just the markup.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.