LCP Gatsby 5+

Fix LCP in Gatsby: Optimize Largest Contentful Paint in 2026

Gatsby ships with a powerful image processing pipeline, yet Largest Contentful Paint scores above 4 seconds are common on freshly scaffolded Gatsby sites. The root cause is almost always the same: the deprecated gatsby-image package still in use, a hero image served as a plain img tag, or the modern gatsby-plugin-image component left in its lazy-loading default for an above-the-fold element. Gatsby's static site generation already eliminates server-side rendering delay from TTFB, which gives it a structural advantage over SSR frameworks. Squandering that advantage on an unoptimized hero image is the single most common performance regression on Gatsby projects. This guide walks through five concrete, measurable fixes — from migrating to gatsby-plugin-image and configuring AVIF output, to injecting preload links in the document head and delegating image processing to a custom CDN for large sites with thousands of pages. Following all five steps consistently brings LCP from the poor range into the good range in a single build.

Expected results

The metrics below reflect a Gatsby 5.13 marketing site with a 1,200 x 630 hero image, measured with Lighthouse 12 on a simulated 4G connection (10 Mbps down, 40ms RTT) from a Netlify CDN edge node. Your numbers will vary based on image size, hosting, and page weight, but the relative gains are representative.

Before

4.2s

LCP score (Poor) — plain img tag, JPEG only, no preload, lazy loading default

After

1.4s

LCP score (Good) — GatsbyImage with AVIF, eager loading, preload link injected at build

TL;DR — quick wins in order of impact:
  1. Replace gatsby-image with gatsby-plugin-image and enable AVIF output in gatsby-plugin-sharp.
  2. Add loading="eager" and fetchpriority="high" to the hero GatsbyImage or StaticImage.
  3. Install gatsby-plugin-preload-link-crossorigin to inject a rel=preload tag at build time.
  4. Use withArtDirection to serve appropriately cropped images at each breakpoint.
  5. Configure a custom image CDN to avoid rebuilding images on every deploy for large sites.

Common causes of poor LCP in Gatsby

Before reaching for fixes, it is worth understanding why Gatsby sites underperform on LCP despite generating static HTML at build time. The static HTML delivery advantage is real — TTFB on a CDN-hosted Gatsby site is typically under 100ms — but that advantage disappears if the LCP element is not in the initial HTML payload or is deprioritized by the browser.

  • Using the deprecated gatsby-image package. The original gatsby-image was deprecated in Gatsby 3. It ships no AVIF support, uses a different GraphQL fragment schema, and lacks the loading and fetchpriority props that modern browsers rely on for resource scheduling. Projects that scaffolded before 2022 frequently still have the old package installed alongside the new one, creating confusion about which component is actually rendering.
  • Leaving loading="lazy" as the default on the hero. Both GatsbyImage and StaticImage default to lazy loading. For images below the fold this is correct, but for the largest visible element on the first viewport it adds 200-600ms of latency while the browser waits for the JavaScript bundle to hydrate and trigger the intersection observer before starting the image fetch.
  • No preload link in the document head. Even with loading="eager", the browser cannot discover the image URL until React renders the component tree. A rel=preload tag injected during the Gatsby build allows the browser to begin fetching the image during HTML parsing, before any JavaScript executes.
  • Serving JPEG or PNG instead of AVIF or WebP. AVIF images are typically 40-60% smaller than equivalent-quality JPEG files. On a 4G connection, a 300KB hero JPEG takes roughly 240ms to transfer. The equivalent AVIF at 120KB takes under 100ms. That 140ms difference alone can be the margin between a good and needs-improvement LCP score.
  • Image art direction serving oversized assets on mobile. A 1,200px-wide desktop hero image delivered unchanged to a 390px-wide phone screen wastes bandwidth on pixel data that is never displayed. Without withArtDirection and a properly tuned sizes attribute, mobile LCP is consistently worse than desktop.
  • Build-time image processing blocking incremental builds on large sites. Sites with 5,000+ pages and images frequently disable image optimization entirely because build times become unacceptable. Moving to a custom image CDN restores optimization without the build-time cost.

Step-by-step fix

Step 1: Migrate to gatsby-plugin-image and enable AVIF

If your project was created before Gatsby 4, it likely uses the deprecated gatsby-image package. The modern replacement is gatsby-plugin-image, which ships with AVIF support, improved responsive layout handling, and proper loading and fetchpriority prop support. Start by uninstalling the old package and installing the new stack.

Shell — Install
npm uninstall gatsby-image
npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp

Then update gatsby-config.js (or gatsby-config.ts if using TypeScript) to register all three plugins and configure sharp with AVIF and WebP output. The defaults object in gatsby-plugin-sharp applies globally to every processed image, so you only need to set these options once.

JavaScript — gatsby-config.js
// gatsby-config.js
module.exports = {
  plugins: [
    "gatsby-plugin-image",
    {
      resolve: "gatsby-plugin-sharp",
      options: {
        // Generate AVIF by default, with WebP and original as fallbacks
        defaults: {
          formats: ["auto", "webp", "avif"],
          placeholder: "blurred",
          quality: 80,
          breakpoints: [360, 480, 768, 1024, 1280, 1536],
          backgroundColor: "transparent",
        },
      },
    },
    "gatsby-transformer-sharp",
    {
      resolve: "gatsby-source-filesystem",
      options: {
        name: "images",
        path: `${__dirname}/src/images`,
      },
    },
  ],
};
Breaking change: The GraphQL fragments from gatsby-image (GatsbyImageSharpFluid, GatsbyImageSharpFixed) are not compatible with gatsby-plugin-image. Replace them with the gatsbyImageData field in your page queries. The migration guide in the Gatsby documentation covers every fragment-to-field mapping.

Step 2: Use GatsbyImage or StaticImage with eager loading on the hero

After migrating the package, the most impactful single change is setting loading="eager" on the LCP image. This instructs the browser to fetch the image immediately rather than waiting for an intersection observer trigger. Pair it with fetchpriority="high" to signal that this image has the highest resource priority among all images and competing network requests.

Use StaticImage when the image path is a string literal known at build time (common for hero sections on fixed marketing pages). Use GatsbyImage when the image comes from a CMS or file system query.

JSX — StaticImage for a fixed hero (Before)
// Before: plain img tag, no optimization, lazy by default
import React from "react";

export function Hero() {
  return (
    <section className="hero">
      <img
        src="/images/hero.jpg"
        alt="Dashboard overview"
        className="hero__image"
      />
    </section>
  );
}
JSX — StaticImage with eager loading (After)
// After: StaticImage with AVIF output, eager loading, and high fetchpriority
import React from "react";
import { StaticImage } from "gatsby-plugin-image";

export function Hero() {
  return (
    <section className="hero">
      <StaticImage
        src="../images/hero.jpg"
        alt="Dashboard overview"
        layout="fullWidth"
        loading="eager"
        fetchpriority="high"
        formats={["auto", "webp", "avif"]}
        quality={85}
        placeholder="blurred"
        className="hero__image"
      />
    </section>
  );
}

For CMS-sourced images, use GatsbyImage with a page query. The gatsbyImageData GraphQL field accepts a layout argument and a formats argument directly in the query.

JSX — GatsbyImage with page query (After)
import React from "react";
import { graphql } from "gatsby";
import { GatsbyImage, getImage } from "gatsby-plugin-image";

export default function BlogPost({ data }) {
  const coverImage = getImage(data.markdownRemark.frontmatter.cover);

  return (
    <article>
      <GatsbyImage
        image={coverImage}
        alt={data.markdownRemark.frontmatter.coverAlt}
        loading="eager"
        fetchpriority="high"
        className="post-cover"
      />
      <h1>{data.markdownRemark.frontmatter.title}</h1>
    </article>
  );
}

export const pageQuery = graphql`
  query BlogPostQuery($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      frontmatter {
        title
        coverAlt
        cover {
          childImageSharp {
            gatsbyImageData(
              layout: FULL_WIDTH
              formats: [AUTO, WEBP, AVIF]
              quality: 85
              placeholder: BLURRED
              breakpoints: [360, 768, 1280]
            )
          }
        }
      }
    }
  }
`;

Step 3: Inject a preload link with gatsby-plugin-preload-link-crossorigin

Even with loading="eager", the browser cannot fetch the hero image until the JavaScript bundle executes and React renders the component. On a slow CPU (mobile phone, budget laptop), this delay can be 400-900ms after first HTML byte. A rel=preload tag injected into the <head> at build time eliminates this delay entirely — the browser sees the preload during HTML parsing and immediately schedules the image fetch.

gatsby-plugin-preload-link-crossorigin reads your Gatsby page output and injects the correct preload tags for critical resources. For images, you can also add the preload manually in gatsby-ssr.js for full control over the imagesrcset and imagesizes attributes.

Shell — Install
npm install gatsby-plugin-preload-link-crossorigin
JavaScript — gatsby-config.js (plugin entry)
// gatsby-config.js — add to plugins array
{
  resolve: "gatsby-plugin-preload-link-crossorigin",
  options: {
    // Optional: scope preloading to specific pages
    cachePublicFolder: true,
  },
},

For more precise control, add the preload tag yourself in gatsby-ssr.js. This approach lets you specify the exact AVIF URL and the imagesrcset attribute so browsers that support AVIF start fetching the optimized variant immediately.

JavaScript — gatsby-ssr.js
// gatsby-ssr.js
const React = require("react");

exports.onRenderBody = ({ setHeadComponents }) => {
  setHeadComponents([
    <link
      key="hero-preload"
      rel="preload"
      as="image"
      href="/static/hero-1280.avif"
      imagesrcset="/static/hero-360.avif 360w, /static/hero-768.avif 768w, /static/hero-1280.avif 1280w"
      imagesizes="100vw"
      fetchpriority="high"
      crossOrigin="anonymous"
    />,
  ]);
};
Verify the preload is working by opening Chrome DevTools Network panel, filtering by Img type, and checking that the hero image shows Initiator: Other (preload) rather than Initiator: script. The former means the browser fetched it during HTML parsing; the latter means it waited for JavaScript.

Step 4: Configure art-direction with withArtDirection

Art direction means serving a different image crop at different viewport sizes, not just a smaller version of the same image. A hero image optimized for a 1,280px desktop layout typically has a wide aspect ratio that looks poor and wastes bandwidth when shrunk to fit a 390px phone screen. With withArtDirection from gatsby-plugin-image, you can specify separate images (or separate crop parameters) per breakpoint and let the browser pick the correct one using standard picture/source media attributes generated at build time.

JSX — withArtDirection for mobile and desktop crops
import React from "react";
import { graphql } from "gatsby";
import { GatsbyImage, getImage, withArtDirection } from "gatsby-plugin-image";

export default function HeroSection({ data }) {
  const images = withArtDirection(
    // Default: desktop image (used for viewports >= 768px)
    getImage(data.desktopImage),
    [
      {
        // Override: portrait crop for phones
        media: "(max-width: 767px)",
        image: getImage(data.mobileImage),
      },
    ]
  );

  return (
    <section className="hero">
      <GatsbyImage
        image={images}
        alt="Platform dashboard — responsive hero"
        loading="eager"
        fetchpriority="high"
        className="hero__image"
      />
    </section>
  );
}

export const query = graphql`
  query HeroQuery {
    desktopImage: file(relativePath: { eq: "hero-desktop.jpg" }) {
      childImageSharp {
        gatsbyImageData(
          layout: FULL_WIDTH
          formats: [AUTO, WEBP, AVIF]
          quality: 85
          breakpoints: [1024, 1280, 1536]
        )
      }
    }
    mobileImage: file(relativePath: { eq: "hero-mobile.jpg" }) {
      childImageSharp {
        gatsbyImageData(
          layout: FULL_WIDTH
          formats: [AUTO, WEBP, AVIF]
          quality: 85
          breakpoints: [360, 480, 768]
        )
      }
    }
  }
`;

This pattern is most valuable when the desktop image has a 16:9 landscape ratio and the mobile version is a centered 1:1 or 4:3 crop that keeps the subject visible. The bandwidth savings on mobile are substantial: a 1,280px AVIF that would be 90KB on desktop becomes a 390px AVIF of 18KB on the phone, a reduction of 80% per image request.

For more background on how responsive images and srcset interact with LCP at the browser level, see the responsive images and LCP guide and the image optimization for LCP guide.

Step 5: Configure a custom image CDN for large-scale Gatsby sites

Sites with thousands of pages — large e-commerce catalogs, documentation portals, news archives — face a build performance ceiling with local image processing. Sharp runs each image through format conversion and resizing on the build machine's CPU. With 10,000 product images at five breakpoints in three formats, that is 150,000 individual processing operations per build. Even with Gatsby's persistent LMDB cache, this adds 15-40 minutes to cold builds and significantly slows deploys after image content changes.

Gatsby's Image CDN feature, available in Gatsby 5 and supported natively on Gatsby Cloud, defers image transformation to a remote CDN. The build emits CDN URLs with transformation parameters instead of running Sharp locally. Images are transformed on first request and cached at the CDN edge, just like a standard image CDN, but fully integrated with the Gatsby data layer and GraphQL schema.

Shell — Enable Image CDN on Gatsby Cloud
# On Gatsby Cloud, set this environment variable in the site dashboard
# or in your CI environment
GATSBY_CLOUD_IMAGE_CDN=1

For self-hosted deployments, configure the CDN adapter manually. Below is an example wiring Cloudinary as the image CDN using the gatsby-plugin-cloudinary package alongside the official gatsby-source-cloudinary.

Shell — Install Cloudinary integration
npm install gatsby-transformer-cloudinary gatsby-source-cloudinary
JavaScript — gatsby-config.js with Cloudinary CDN
// gatsby-config.js
require("dotenv").config();

module.exports = {
  plugins: [
    "gatsby-plugin-image",
    {
      resolve: "gatsby-transformer-cloudinary",
      options: {
        cloudName: process.env.CLOUDINARY_CLOUD_NAME,
        apiKey: process.env.CLOUDINARY_API_KEY,
        apiSecret: process.env.CLOUDINARY_API_SECRET,
        // Upload local images to Cloudinary at build time
        uploadFolder: "gatsby-site",
        // Use Cloudinary's f_auto,q_auto transformation for format negotiation
        transformations: ["f_auto", "q_auto"],
        // Generate AVIF via f_avif for supporting browsers
        overwriteExisting: false,
      },
    },
    {
      resolve: "gatsby-source-cloudinary",
      options: {
        cloudName: process.env.CLOUDINARY_CLOUD_NAME,
        apiKey: process.env.CLOUDINARY_API_KEY,
        apiSecret: process.env.CLOUDINARY_API_SECRET,
        resourceType: "image",
        prefix: "gatsby-site/",
        maxResults: 500,
      },
    },
  ],
};

With this configuration, Cloudinary handles all image transformation and caches results at its global CDN edge network. Build time for the example site dropped from 22 minutes to 4 minutes after this migration. LCP improved by a further 120ms because Cloudinary edge nodes were geographically closer to test users than the Netlify CDN origin used previously.

For sites using Imgix instead of Cloudinary, the gatsby-source-imgix package provides equivalent functionality. The configuration structure is similar, with an imgixParams option replacing Cloudinary's transformations array. Refer to the image optimization guide for a comparison of CDN options and their trade-offs.

An important build-time optimization for all large Gatsby sites is enabling the LMDB data store, which persists image processing results between builds so that unchanged images are not reprocessed.

Shell — Enable persistent cache
# .env.production
# Enable the LMDB data store for persistent build caching
GATSBY_EXPERIMENTAL_LMDB_STORE=1
# Increase Node.js heap for large image processing jobs
NODE_OPTIONS=--max-old-space-size=4096

Verification

After implementing all five steps, measure LCP using both lab tools and field data. Lab tools give you immediate feedback during development; field data from real users is the signal Google actually uses for Core Web Vitals ranking.

Lab measurement

Run a Lighthouse audit from Chrome DevTools (Lighthouse tab, Mobile preset, Simulated throttling) after a production build. Gatsby development mode bypasses image optimization, so always measure a built and served site, not gatsby develop. Use gatsby build && gatsby serve locally, or push to a preview deployment and measure there.

In the Lighthouse report, check three things: the LCP element identified in the Diagnostics section (should be your hero image, not a text node), the LCP time breakdown showing a short load delay and short render delay, and the "Largest Contentful Paint image was not lazily loaded" audit passing.

WebPageTest at webpagetest.org provides a filmstrip view that makes it easy to see exactly when the hero image first appears on screen. Run the test from the Dulles, Virginia node on a Moto G4 profile (which mirrors Chrome's lab condition). Compare the filmstrip frame where the hero becomes visible to the total page load time.

Field measurement with CrUX

Chrome User Experience Report (CrUX) data is available with a 28-day lag through PageSpeed Insights at pagespeed.web.dev. After deploying your Gatsby optimizations, check back in 4 weeks to see the field LCP distribution shift. The 75th percentile is the threshold Google uses for Core Web Vitals pass/fail status in Search Console.

Enable Web Vitals reporting in your Gatsby site using the gatsby-plugin-web-vitals package or a manual implementation. This sends CWV data to your analytics platform on every page load, giving you real-user LCP distributions broken down by page path, device type, and network speed.

JavaScript — gatsby-browser.js Web Vitals reporting
// gatsby-browser.js
export function onRouteUpdate() {
  if (typeof window === "undefined") return;

  import("web-vitals").then(({ onLCP, onCLS, onINP }) => {
    const sendToAnalytics = (metric) => {
      // Replace with your analytics endpoint or gtag call
      if (window.gtag) {
        window.gtag("event", metric.name, {
          event_category: "Web Vitals",
          event_label: metric.id,
          value: Math.round(
            metric.name === "CLS" ? metric.value * 1000 : metric.value
          ),
          non_interaction: true,
        });
      }
    };
    onLCP(sendToAnalytics);
    onCLS(sendToAnalytics);
    onINP(sendToAnalytics);
  });
}

Quick checklist

  • gatsby-image replaced with gatsby-plugin-image
  • gatsby-plugin-sharp configured with formats: ["auto", "webp", "avif"]
  • Hero GatsbyImage or StaticImage has loading="eager" and fetchpriority="high"
  • gatsby-plugin-preload-link-crossorigin installed, or manual preload in gatsby-ssr.js
  • Art-direction configured with withArtDirection for mobile/desktop crops
  • Preload verified in Network panel (Initiator shows "preload", not "script")
  • Large sites: custom image CDN configured or GATSBY_CLOUD_IMAGE_CDN=1 set
  • Web Vitals reporting enabled in gatsby-browser.js for field data collection

Common pitfalls

  • Applying eager loading to multiple images. Setting loading="eager" on every above-the-fold image cancels out the benefit. The browser treats all eager images equally, which can hurt LCP by splitting bandwidth and fetch priority across multiple requests. Apply fetchpriority="high" only to the single largest visible element. All other images should retain the default lazy loading behavior.
  • Measuring in gatsby develop mode. Development mode disables most image optimization, serves full-resolution JPEGs, and skips the build-time preload injection. LCP measurements from development are misleading and typically 2-3x worse than production. Always run gatsby build && gatsby serve before taking Lighthouse measurements.
  • Forgetting to update GraphQL queries after migration. After removing gatsby-image, any remaining GatsbyImageSharpFluid or GatsbyImageSharpFixed fragment references will throw a GraphQL error at build time. Run gatsby build locally and check the terminal output for fragment errors before deploying.
  • Setting quality too high for AVIF. AVIF's perceptual quality curve is different from JPEG's. AVIF at quality 85 produces output that is visually equivalent to JPEG at quality 95, and is typically 50% smaller. Setting AVIF quality above 85 produces files nearly as large as JPEG with no perceptible visual benefit. The default quality: 80 in gatsby-plugin-sharp is a reasonable starting point for most photography.
  • Not testing on real mobile hardware. Lighthouse's mobile simulation uses CPU throttling to approximate a mid-range Android phone, but real devices have additional constraints around GPU memory, thermal throttling, and network variability. Test your optimized Gatsby site on at least one physical Android device using Chrome DevTools remote debugging before considering LCP work complete.

Frequently asked questions

A Gatsby site with gatsby-plugin-image configured for AVIF output, eager loading on the hero, and a preload link injected at build time should consistently achieve LCP under 1.8 seconds on a fast connection. With a custom image CDN serving images from an edge network, scores under 1.5 seconds are achievable. Scores above 3 seconds in Gatsby almost always trace back to missing eager loading, unoptimized image formats, or slow TTFB from the hosting infrastructure. Compare your Gatsby results against the broader ecosystem in the complete LCP guide.

StaticImage accepts a string literal src at build time and requires no GraphQL query — it is processed entirely during the build. Use it for images where the path is hardcoded in the component file. GatsbyImage works with a gatsbyImageData object returned from a GraphQL page query or static query, making it the right choice for blog post cover images, product images from a CMS, or any image sourced dynamically. The performance characteristics are identical once the build completes — the difference is purely about how the image data is supplied to the component.

Lazy loading is the correct default for the majority of images on a page — those below the initial viewport. The plugin applies it universally to avoid degrading performance for sites where developers do not think about individual image loading strategies. For the LCP image specifically, override this by passing loading="eager" and fetchpriority="high" directly to the GatsbyImage or StaticImage component. Apply the override only to the single hero image visible above the fold. If you have a carousel with three hero images, mark only the first (visible) slide as eager.

The plugin injects a rel=preload link into the document <head> during the Gatsby build. The browser reads this link during initial HTML parsing and immediately queues the image fetch before any JavaScript has executed or any React component has rendered. Without the preload, the browser cannot discover the LCP image URL until the Gatsby/React runtime has hydrated the page tree — a delay of 300-900ms on slow CPUs. The preload link short-circuits this by making the image URL visible to the browser's preload scanner, which runs in a separate thread during HTML parsing. This is one of the highest-impact optimizations for Gatsby's LCP score and costs nothing at runtime.

Use a custom image CDN integration — gatsby-transformer-cloudinary for Cloudinary or gatsby-source-imgix for Imgix — to offload image transformation from the local build. Gatsby 5's Image CDN feature delegates the transformation entirely to the CDN, so the build only emits CDN URLs with parameters rather than processing every image with Sharp. Enable the LMDB persistent cache with GATSBY_EXPERIMENTAL_LMDB_STORE=1 to prevent re-processing unchanged images between deploys. On CI, ensure the .cache and public directories are preserved between runs. Together these changes routinely cut build times by 70-80% on large sites.

Continue learning