webpack()

Extends the webpack config Next.js generates, returning the modified object.

Signature
webpack?: (config: any, context: { buildId: string; dev: boolean; isServer: boolean; defaultLoaders: { babel: any }; nextRuntime?: 'nodejs' | 'edge'; webpack: typeof import('webpack') }) => any

Parameters

ParameterTypeDescription
$configanyThe webpack configuration object Next.js has already assembled. Mutate it or build a new one from it.
$contextobjectSecond argument describing which of the three compilations is running.
$context.buildIdStringThe build id, used as a unique identifier between builds.
$context.devBooleanIndicates if the compilation will be done in development.
$context.isServerBoolean`true` for server-side compilation, `false` for client-side compilation.
$context.defaultLoadersObjectDefault loaders used internally by Next.js. Contains `babel`: the default `babel-loader` configuration.
$context.nextRuntimeoptionalString | undefinedThe target runtime for server-side compilation, either `"edge"` or `"nodejs"`; `undefined` for client-side compilation.
$context.webpacktypeof import('webpack')The webpack instance Next.js uses, so you can construct plugins without installing webpack yourself.

Return value

any (the webpack configuration object)The modified webpack config. You must return it — returning `undefined` breaks the build.

Errors

With Turbopack as the default bundler, `next build` fails when a `webpack` configuration is found, to prevent silently building with settings that were never applied.

Changelog

  • PHP v16.0.0Turbopack became the default bundler for `next dev` and `next build`. If a `webpack` function is present, `next build` fails rather than silently ignoring it — pass `--webpack` to keep webpack or `--turbopack` to build anyway and discard the config.
  • PHP v15.3.0`experimental.turbo` became the top-level `turbopack` option, which is where loader and alias customisations move when you migrate off this hook.

The webpack option is a function that receives the configuration Next.js has already assembled and returns a modified version of it. It is executed three times per build — twice for the server (Node.js and Edge runtimes) and once for the client — so almost every real customisation starts by branching on isServer or nextRuntime.

When to reach for it

Only after confirming Next.js has no first-class option for what you want. CSS, CSS Modules, Sass, MDX (@next/mdx) and bundle analysis (@next/bundle-analyzer) are all supported without touching the bundler. Reach for this hook for genuinely bespoke loaders, DefinePlugin values, or vendor plugins that ship no Next.js integration.

What trips people up

  • Forgetting the final return config. The function's return value is the config; dropping it produces a build with no configuration at all.
  • Since Next.js 16 Turbopack is the default bundler, and next build fails when it finds a webpack function. Run next build --webpack to keep webpack, next build --turbopack to build anyway and ignore the hook, or port the customisation to the turbopack option.
  • If you see that failure but never wrote this option, a plugin wrapper in your config is adding it for you.
  • The shape of the config object is not covered by semver, so a minor Next.js release can move things you depend on.

Example

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack: (
    config,
    { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }
  ) => {
    if (!isServer) {
      config.plugins.push(
        new webpack.DefinePlugin({
          'process.env.BUILD_ID': JSON.stringify(buildId),
        })
      )
    }

    // Important: return the modified config
    return config
  },
}

export default nextConfig

When to use it

  • Register a loader for a file type Next.js does not handle, such as .glsl shaders.
  • Inject build-time constants into the client bundle with webpack.DefinePlugin.
  • Add a vendor plugin (source map upload, license checking) that only ships a webpack integration.
  • Keep an existing webpack setup working while migrating a large app to Turbopack.

Frequently asked questions

Why does next build fail with a message about a custom webpack configuration?

Because Next.js 16 builds with Turbopack by default and refuses to ignore a webpack config silently. Run `next build --webpack` to keep webpack, `next build --turbopack` to build with Turbopack and discard the config, or migrate the customisation to the `turbopack` option. If you never wrote a webpack function yourself, a plugin wrapper in next.config is adding one.

Why is my webpack function running three times?

Because Next.js compiles three targets: the client, the Node.js server, and the Edge runtime. The function is called once per compilation. Use `isServer` to separate client from server and `nextRuntime` ("nodejs" or "edge") to separate the two server passes; pushing a client-only plugin unconditionally is what breaks server builds.

Do I need to install webpack to use this option?

No. The second argument includes a `webpack` property holding the exact webpack instance Next.js uses, so construct plugins from that rather than `require('webpack')`. Importing your own copy can pull in a second webpack version and produce plugins the build rejects.

More examples

Adding a loader that builds on defaultLoaders.babel

This is the pattern the @next/mdx plugin itself uses. Putting options.defaultLoaders.babel first in the `use` array keeps Next.js's own transform in the chain, so the JSX that @mdx-js/loader emits still gets compiled. Dropping it is the usual cause of "Unexpected token <" errors from a custom loader.

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack: (config, options) => {
    config.module.rules.push({
      test: /\.mdx/,
      use: [
        options.defaultLoaders.babel,
        {
          loader: '@mdx-js/loader',
          options: {},
        },
      ],
    })

    return config
  },
}

export default nextConfig

Branching on nextRuntime instead of isServer

Shows why isServer alone is not enough: it is true for both server compilations. nextRuntime distinguishes them — "nodejs" for the Node.js server, "edge" for proxy and Server Components on the Edge runtime, and undefined on the client. Externalising a native module in the Node.js pass while aliasing it away in the Edge pass avoids the "Module not found" failures that appear only in the Edge build.

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack: (config, { isServer, nextRuntime }) => {
    if (isServer && nextRuntime === 'nodejs') {
      config.externals = [...(config.externals ?? []), 'my-native-addon']
    }

    if (isServer && nextRuntime === 'edge') {
      // The Edge compilation has no Node.js built-ins available
      config.resolve.alias = {
        ...config.resolve.alias,
        'my-native-addon': false,
      }
    }

    return config
  },
}

export default nextConfig

Keeping webpack for builds after upgrading to Next.js 16

After the Next.js 16 upgrade, `next build` uses Turbopack and refuses to run when a webpack function is present. This package.json keeps Turbopack for the fast dev loop while pinning production builds to webpack, so an unported config does not block the release. It is a holding pattern, not a destination — the config still needs migrating to the top-level `turbopack` option.

Example · json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start"
  }
}

Discussion

  • Be the first to comment on this lesson.