Gatsby Webpack Configuration: A Complete Guide to Customizing Your Build Pipeline

Gatsby generates distinct webpack configurations for four build stages—develop, develop-html, build-javascript, and build-html—and exposes the onCreateWebpackConfig API in gatsby-node.js to customize loaders, plugins, and optimization settings without ejecting.

Gatsby's build system relies heavily on webpack to bundle JavaScript, CSS, and assets across multiple build stages. Understanding how Gatsby webpack configuration works is essential for optimizing bundle sizes, adding custom loaders, or integrating specialized build tools. This guide examines the core architecture in packages/gatsby/src/utils/webpack.config.js and demonstrates how to safely customize your build pipeline using the public Node API.

The Four Build Stages

Gatsby constructs separate webpack configurations for four distinct stages. Each stage serves a specific purpose in the build lifecycle:

  • develop – Client-side development server with hot reloading and React Fast Refresh.
  • develop-html – Server-side rendering for the development environment.
  • build-javascript – Production client bundle generation with code splitting and optimization.
  • build-html – Static HTML generation for server-side rendering during production builds.

The stage is detected at runtime in packages/gatsby/src/utils/webpack.config.js and determines entry points, output paths, and plugin selection.

Core Configuration Architecture

Gatsby's webpack configuration is assembled from reusable "atoms" rather than static objects. This modular approach allows the framework to generate stage-specific configs while maintaining consistency.

webpack.config.js

The main entry point resides in packages/gatsby/src/utils/webpack.config.js. This module exports an async function that orchestrates the entire configuration:

module.exports = async (program, directory, suppliedStage, port, { parentSpan } = {}) => {
  const stage = suppliedStage
  const { rules, loaders, plugins } = createWebpackUtils(stage, program)
  
  // Configuration assembly logic
  const publicPath = getPublicPath({ assetPrefix, pathPrefix, ...program })
  
  // Entry, output, module, resolve, and plugin configuration
  // ...
  
  // User customization hook
  await apiRunnerNode(`onCreateWebpackConfig`, {
    getConfig,
    stage,
    rules,
    loaders,
    plugins,
    parentSpan,
  })
  
  return getConfig()
}

Key responsibilities include:

  • Stage detection – Determines build context (development vs. production, client vs. server).
  • Entry point generation – Creates stage-specific entries (app for client, render-page for SSR).
  • Output configuration – Sets paths, filenames, and public paths for assets.
  • Module rules – Configures loaders for JavaScript, CSS, images, and fonts.
  • Plugin injection – Adds React Refresh, MiniCssExtract, and optimization plugins.
  • Externals – Prevents bundling of React and Node.js built-ins during SSR stages.
  • Caching – Configures filesystem caching keyed by stage.

webpack-utils.ts

The createWebpackUtils function in packages/gatsby/src/utils/webpack-utils.ts returns three namespaces of factory functions:

  • loaders – Factory helpers for individual loaders (e.g., loaders.js(), loaders.css()).
  • rules – Complete rule definitions (e.g., rules.js(), rules.cssModules()).
  • plugins – Plugin constructors (e.g., plugins.extractText(), plugins.fastRefresh()).

These utilities ensure consistency across stages while allowing granular customization. For example, the production flag is derived as const PRODUCTION = !stage.includes('develop'), and SSR detection uses const isSSR = stage.includes('html').

webpack-plugins.ts

Specialized plugin implementations reside in packages/gatsby/src/utils/webpack-plugins.ts. These include custom Gatsby-specific plugins for virtual modules, partial hydration shims, and framework-specific optimizations.

Customizing Your Webpack Configuration

Gatsby exposes the onCreateWebpackConfig API in gatsby-node.js (or plugin files) for safe customization without ejecting. This hook receives the current stage and utility functions.

Available Actions

Two primary methods modify the configuration:

  • setWebpackConfig – Deep merges a partial configuration into Gatsby's generated config using webpack-merge. Preferred for most use cases.
  • replaceWebpackConfig – Replaces the entire configuration object. Use sparingly, as it bypasses Gatsby's defaults.

Stage-Specific Customization

Always guard customizations by stage to avoid breaking server-side rendering:

exports.onCreateWebpackConfig = ({ actions, stage }) => {
  // Only apply to client-side builds
  if (stage === "develop" || stage === "build-javascript") {
    // Client-specific configuration
  }
}

Practical Customization Examples

Adding a Custom Loader

To handle SVG files as React components, add svg-react-loader to the client bundle:

// gatsby-node.js
exports.onCreateWebpackConfig = ({ actions, stage }) => {
  if (stage === "develop" || stage === "build-javascript") {
    actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /\.svg$/,
            use: [
              {
                loader: require.resolve("svg-react-loader"),
              },
            ],
          },
        ],
      },
    })
  }
}

The stage guard ensures this rule only affects the client bundle. Applying it to SSR stages (build-html, develop-html) would fail because svg-react-loader expects a browser environment.

Integrating a Bundle Analyzer

Add webpack-bundle-analyzer to inspect production chunks:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

exports.onCreateWebpackConfig = ({ actions, stage }) => {
  if (stage === "build-javascript") {
    actions.setWebpackConfig({
      plugins: [new BundleAnalyzerPlugin({ analyzerMode: "static" })],
    })
  }
}

Modifying Existing Rules for SSR

Enable CSS Modules during the build-html stage by mutating the existing rule:

exports.onCreateWebpackConfig = ({ actions, stage, getConfig }) => {
  if (stage === "build-html") {
    const config = getConfig()
    
    // Locate the CSS Modules rule
    const cssModulesRule = config.module.rules.find(
      r => r.oneOf && r.oneOf.some(inner => inner.test && inner.test.test('.module.css'))
    )
    
    if (cssModulesRule) {
      cssModulesRule.oneOf.forEach(inner => {
        if (inner.test && inner.test.test('.module.css')) {
          inner.use = inner.use || []
          inner.use.push({
            loader: require.resolve("css-loader"),
            options: { modules: true },
          })
        }
      })
    }
    
    actions.replaceWebpackConfig(config)
  }
}

Tip: Because Gatsby constructs a complex module.rules structure, prefer adding new rules with setWebpackConfig rather than mutating Gatsby's internal rule structure. Use replaceWebpackConfig only when you need full control over the configuration object.

Reusing Gatsby's Internal Atoms

Import createWebpackUtils to leverage Gatsby's loader factories in your customizations:

const { createWebpackUtils } = require("gatsby/src/utils/webpack-utils")

exports.onCreateWebpackConfig = ({ actions, stage, ...args }) => {
  const { loaders, rules } = createWebpackUtils(stage, args.program)

  actions.setWebpackConfig({
    module: {
      rules: [
        {
          ...rules.js(),
          use: [
            loaders.babel({ /* custom Babel options */ }),
            { loader: "my-special-loader" },
          ],
        },
      ],
    },
  })
}

Note: Importing from gatsby/src/... works in development but may break in production builds. For maximum compatibility, rely on the public setWebpackConfig API.

Key Source Files

Understanding the internal architecture helps when debugging complex customizations:

File Role
packages/gatsby/src/utils/webpack.config.js Core configuration generator that orchestrates stage-specific entries, output, and plugins.
packages/gatsby/src/utils/webpack-utils.ts Factory functions for loaders, rules, and plugins used across all stages.
packages/gatsby/src/utils/webpack-plugins.ts Built-in plugin implementations including React Refresh and MiniCssExtract.
packages/gatsby/src/utils/get-public-path.js Computes publicPath from assetPrefix and pathPrefix settings.
packages/gatsby/src/utils/browserslist.js Provides target browser lists for PostCSS and Babel.

Summary

  • Gatsby generates four distinct webpack configurations for different build stages: develop, develop-html, build-javascript, and build-html.
  • The core logic resides in packages/gatsby/src/utils/webpack.config.js, which assembles configurations using reusable atoms from webpack-utils.ts.
  • Customize builds via onCreateWebpackConfig in gatsby-node.js using setWebpackConfig for merges or replaceWebpackConfig for full control.
  • Always guard customizations by stage to prevent breaking server-side rendering with browser-specific loaders.
  • Reuse Gatsby's internal utilities via createWebpackUtils when you need to extend existing loader configurations while maintaining framework consistency.

Frequently Asked Questions

How do I add a custom webpack loader to Gatsby?

Use the onCreateWebpackConfig API in your gatsby-node.js file with the setWebpackConfig action. Always wrap loader additions in a stage check (stage === "develop" || stage === "build-javascript") to ensure they only run in browser bundles, preventing errors during server-side rendering.

What is the difference between setWebpackConfig and replaceWebpackConfig?

setWebpackConfig performs a deep merge of your partial configuration with Gatsby's generated config using webpack-merge, making it the safer choice for most customizations. replaceWebpackConfig completely overwrites the entire configuration object and should only be used when you need full control over every aspect of the webpack setup.

Which build stages does Gatsby use for webpack?

Gatsby uses four distinct stages: develop (client-side development with hot reloading), develop-html (server-side rendering for development), build-javascript (production client bundle with code splitting), and build-html (static HTML generation for production). Each stage receives a tailored webpack configuration optimized for its specific environment.

How can I modify existing webpack rules without breaking Gatsby's defaults?

Retrieve the current config using getConfig(), locate the specific rule by testing its test property or structure, modify the rule in place, then use replaceWebpackConfig to apply changes. However, for maintainability, prefer adding new rules with setWebpackConfig rather than mutating Gatsby's internal rule structure.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →