Performance

How We Boosted Gatsby Render Performance by 40%

BBharath Bheemireddy8 min read

A deep dive into the caching, image pipeline, and bundle-splitting strategies behind a real-world Gatsby performance win.

A few years ago I inherited a Gatsby site that took 27 seconds to build a single page in development and lost a noticeable amount of LCP in production. Over six weeks of focused work, our team brought build times down by 40% and shaved 1.2s off LCP. Here's the actual playbook.

Step 1: Measure first, optimize second

Before touching code, we instrumented every layer. Gatsby has the perfect built-in tools — just nobody uses them.

GATSBY_PROFILE=true gatsby build --profile
# Generates a flamegraph at .cache/profiles/build-XX.json
# Open in Chrome DevTools → Performance tab

# Plus the runtime bundle:
GATSBY_PROFILE_BUILD_VITALS=true gatsby build

The flamegraph revealed our actual bottleneck: 60% of build time was Sharp processing 4MB hero images that ultimately rendered at 800px. We were optimizing the wrong things.

Step 2: Fix the image pipeline

gatsby-plugin-image with the right configuration was the single biggest win. The old site used <img> tags pointing at raw originals. We migrated to GatsbyImage with proper responsive variants.

// gatsby-config.js
plugins: [{
  resolve: 'gatsby-plugin-sharp',
  options: {
    defaults: {
      formats: ['auto', 'webp', 'avif'],
      placeholder: 'blurred',
      quality: 70,                     // 70 looks identical to 90 to humans
      breakpoints: [400, 768, 1024],   // matches our 3 layouts only
      backgroundColor: 'transparent',
    },
  },
}]
  • AVIF + WebP fallbacks cut hero payload by 64%
  • Blurred placeholders eliminated LCP shift
  • Constraining breakpoints to the 3 we actually used dropped build artifacts by 18%

Step 3: Aggressive build caching

Gatsby's incremental builds work great — when you let them. The default GitHub Actions cache config wasn't restoring `.cache/` correctly, so every CI build was cold.

- uses: actions/cache@v4
  with:
    path: |
      .cache
      public
      node_modules/.cache
    key: gatsby-${{ hashFiles('**/yarn.lock') }}-${{ github.sha }}
    restore-keys: |
      gatsby-${{ hashFiles('**/yarn.lock') }}-
      gatsby-

Result: incremental CI builds went from 4m20s to 1m45s. Local hot reload from 27s to 8s.

Step 4: Bundle splitting that pays for itself

Audit the bundle before adding splitting. We ran webpack-bundle-analyzer and found one route imported moment.js for a single date format — a 230KB cost. Replacing it with date-fns/format saved 220KB. Often the best 'optimization' is just deleting something.

// Before — pulls all of moment
import moment from 'moment'
const pretty = moment(d).format('MMM DD, YYYY')

// After — tree-shakable
import { format } from 'date-fns'
const pretty = format(d, 'MMM dd, yyyy')
// 230KB → 4KB

Step 5: Smarter GraphQL queries

Gatsby's GraphQL layer is powerful but easy to abuse. Page queries that pull every field of every node make the data layer crawl. Two patterns helped:

  • Page query → request ONLY the fields the page renders. Use fragments to share.
  • Static query → for components that need data on every page (nav, footer), use a single static query, not a page query × N.

The 40% number

Combining all five steps:

  • Production build: 6m12s → 3m41s (–40.6%)
  • Largest Contentful Paint (p75): 2.8s → 1.6s (–43%)
  • Lighthouse Performance score: 71 → 96
  • Total bundle (gzipped): 412KB → 248KB (–40%)
Performance work isn't a sprint. It's a slow, measured grind where each 5% win compounds. Profile first. Always.
B

Bharath Bheemireddy

Technical Lead @ Evoke Technologies

Get in touch