webpack()
Extends the webpack config Next.js generates, returning the modified object.
webpack?: (config: any, context: { buildId: string; dev: boolean; isServer: boolean; defaultLoaders: { babel: any }; nextRuntime?: 'nodejs' | 'edge'; webpack: typeof import('webpack') }) => anyParameters
| Parameter | Type | Description |
|---|---|---|
$config | any | The webpack configuration object Next.js has already assembled. Mutate it or build a new one from it. |
$context | object | Second argument describing which of the three compilations is running. |
$context.buildId | String | The build id, used as a unique identifier between builds. |
$context.dev | Boolean | Indicates if the compilation will be done in development. |
$context.isServer | Boolean | `true` for server-side compilation, `false` for client-side compilation. |
$context.defaultLoaders | Object | Default loaders used internally by Next.js. Contains `babel`: the default `babel-loader` configuration. |
$context.nextRuntimeoptional | String | undefined | The target runtime for server-side compilation, either `"edge"` or `"nodejs"`; `undefined` for client-side compilation. |
$context.webpack | typeof 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 buildfails when it finds awebpackfunction. Runnext build --webpackto keep webpack,next build --turbopackto build anyway and ignore the hook, or port the customisation to theturbopackoption. - 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
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 nextConfigWhen 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.
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 nextConfigBranching 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.
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 nextConfigKeeping 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.
{
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start"
}
}
Discussion