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
- Replace
gatsby-imagewithgatsby-plugin-imageand enable AVIF output ingatsby-plugin-sharp. - Add
loading="eager"andfetchpriority="high"to the heroGatsbyImageorStaticImage. - Install
gatsby-plugin-preload-link-crossoriginto inject arel=preloadtag at build time. - Use
withArtDirectionto serve appropriately cropped images at each breakpoint. - 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-imagepackage. The originalgatsby-imagewas deprecated in Gatsby 3. It ships no AVIF support, uses a different GraphQL fragment schema, and lacks theloadingandfetchpriorityprops 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. BothGatsbyImageandStaticImagedefault 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. Arel=preloadtag 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
withArtDirectionand a properly tunedsizesattribute, 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.
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.
// 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`,
},
},
],
};
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.
// 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>
);
}
// 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.
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.
npm install gatsby-plugin-preload-link-crossorigin
// 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.
// 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"
/>,
]);
};
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.
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.
# 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.
npm install gatsby-transformer-cloudinary gatsby-source-cloudinary
// 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.
# .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.
// 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-imagereplaced withgatsby-plugin-image -
gatsby-plugin-sharpconfigured withformats: ["auto", "webp", "avif"] -
Hero
GatsbyImageorStaticImagehasloading="eager"andfetchpriority="high" -
gatsby-plugin-preload-link-crossorigininstalled, or manual preload ingatsby-ssr.js -
Art-direction configured with
withArtDirectionfor mobile/desktop crops - Preload verified in Network panel (Initiator shows "preload", not "script")
-
Large sites: custom image CDN configured or
GATSBY_CLOUD_IMAGE_CDN=1set -
Web Vitals reporting enabled in
gatsby-browser.jsfor 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. Applyfetchpriority="high"only to the single largest visible element. All other images should retain the default lazy loading behavior. - Measuring in
gatsby developmode. 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 rungatsby build && gatsby servebefore taking Lighthouse measurements. - Forgetting to update GraphQL queries after migration. After removing
gatsby-image, any remainingGatsbyImageSharpFluidorGatsbyImageSharpFixedfragment references will throw a GraphQL error at build time. Rungatsby buildlocally and check the terminal output for fragment errors before deploying. - Setting
qualitytoo 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 defaultquality: 80in 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.
Related resources
Complete LCP Guide
The comprehensive reference for understanding and optimizing Largest Contentful Paint across all frameworks.
GuideImage Optimization Guide
AVIF, WebP, responsive srcset, CDN configuration, and format selection for maximum LCP improvement.
FixResponsive Images and LCP
How srcset, sizes, and picture elements interact with the browser's preload scanner to affect LCP.
FixImage Optimization for LCP
Cross-framework image optimization techniques applicable to Gatsby, Next.js, React, and plain HTML.
Continue learning
Fix LCP in Next.js
Next.js-specific optimizations: next/image priority prop, ISR, next/font, and React Server Components.
FixFix LCP in React
Framework-agnostic React patterns for hero image preloading, lazy hydration, and bundle splitting.
GuideLCP Deep Dive
Thresholds, measurement methodology, and the full taxonomy of LCP sub-parts.
FixImage Optimization for LCP
Cross-framework image optimization guide covering AVIF, WebP, CDN selection, and cache strategy.