Skip to content

Commit c484a05

Browse files
authored
fix(next): remove turbopack build support to fix bundle size regression (#14696)
## Background The following PRs attempted to add support for Turbopack build: - #14475 - #14473 ## The Fundamental Problem Payload's database adapters (e.g., `@payloadcms/db-postgres`) depend on packages with native dependencies that cannot be bundled (e.g., `drizzle-kit`, which imports `esbuild`). We need to externalize these packages. **Why we can't externalize them directly:** With pnpm, externalizing a package like `drizzle-kit` generates `require('drizzle-kit')` calls in the bundle. However, `drizzle-kit` is not in the user's `package.json` (it's a transitive dependency installed by `db-postgres`). pnpm's strict dependency isolation prevents importing dependencies of dependencies, causing runtime failures. **The attempted workaround:** Instead of externalizing `drizzle-kit`, we tried externalizing the entry-point package `@payloadcms/db-postgres` (which users DO install) via `serverExternalPackages`. This works in development, but creates severe issues in production builds. ## Why the Workaround Failed When you externalize `@payloadcms/db-postgres`: 1. **Everything it imports becomes external**, including `payload` itself 2. This creates **two copies of `payload`**: - One bundled (from user's direct imports) - One external in node_modules (from db-postgres's imports) 3. **Bundle size explodes** due to: - Disabled tree-shaking for externalized packages - Duplicate package installations - Loss of code-splitting optimizations **Another example of duplication:** ``` @payloadcms/richtext-lexical (bundled) → qs-esm (bundled) payload (external) → qs-esm (external) Result: Two copies of qs-esm in production ``` This issue was reported on our discord [here](https://discord.com/channels/967097582721572934/1422639568808841329/1440689060015374437). ## The Solution (This PR) **Short term:** Disable Turbopack build support until Next.js provides a proper solution. ### Why Webpack Works Webpack has `webpack.externals`, which can externalize **any** package regardless of whether it's in the user's `package.json`: - We externalize `drizzle-kit` directly via `webpack.externals` - Webpack generates `require('drizzle-kit')` calls in the bundle - At runtime, Node.js resolves these just fine - we're not yet sure why that is - We avoid externalizing entry-point packages, preventing the duplication problem ### Why Turbopack Build Doesn't Work Turbopack only has `serverExternalPackages` (similar to webpack.externals but with restrictions): - **The constraint**: Packages must be resolvable from the project root (i.e., in the user's `package.json`) - If a package isn't directly installed by the user, Next.js **ignores the externalization rule** and tries to bundle it anyway - This forces us to externalize entry-point packages (`db-postgres`), which causes the duplication and bundle size problems described above ### Why Turbopack Dev Works Turbopack dev has the same `serverExternalPackages` constraint, BUT: - **In dev, we can afford the trade-off** of externalizing entry-point packages because: - Bundle size doesn't matter in development - Faster compilation speed is more important - We're not shipping to production - The duplication problem still exists, but it's acceptable for the dev experience **Changes made:** 1. **Throw error for Turbopack builds** - Prevent production builds with Turbopack until Next.js fixes the underlying issue 2. **Restore webpack.externals** - Use webpack-specific externals for problematic transitive dependencies (`drizzle-kit`, `sharp`, `libsql`, etc.) that aren't in user's package.json 3. **Simplify serverExternalPackages** - Only externalize packages resolvable from project root (`graphql`, `@sentry/nextjs`) 4. **Clean up unnecessary config** - Remove webpack configurations that are no longer justifiable. Any configuration we have left now comes with a comment block explaining why we need it 5. **enable devBundleServerPackages optimization by default** - there have not been any reported issues since this was introduced, and this setting is now **necessary** for turbopack support during dev ## Future In order to properly support Turbopack Build, Next.js will have to implement one of these solutions: - **Option 1**: Implement webpack.externals-like functionality for Turbopack (no package.json constraint) - **Option 2**: Remove the need for declaring all externals as direct dependencies in the application We're tracking Next.js's progress on this issue.
1 parent a3f490b commit c484a05

File tree

1 file changed

+101
-27
lines changed

1 file changed

+101
-27
lines changed

packages/next/src/withPayload.js

Lines changed: 101 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @param {import('next').NextConfig} nextConfig
33
* @param {Object} [options] - Optional configuration options
4-
* @param {boolean} [options.devBundleServerPackages] - Whether to bundle server packages in development mode. @default true
4+
* @param {boolean} [options.devBundleServerPackages] - Whether to bundle server packages in development mode. @default false
55
*
66
* @returns {import('next').NextConfig}
77
* */
@@ -52,6 +52,16 @@ export const withPayload = (nextConfig = {}, options = {}) => {
5252
}
5353
}
5454

55+
const isBuild = process.env.NODE_ENV === 'production'
56+
const isTurbopackNextjs15 = process.env.TURBOPACK === '1'
57+
const isTurbopackNextjs16 = process.env.TURBOPACK === 'auto'
58+
59+
if (isBuild && (isTurbopackNextjs15 || isTurbopackNextjs16)) {
60+
throw new Error(
61+
'Payload does not support using Turbopack for production builds. If you are using Next.js 16, please use `next build --webpack` instead.',
62+
)
63+
}
64+
5565
const poweredByHeader = {
5666
key: 'X-Powered-By',
5767
value: 'Next.js, Payload',
@@ -106,42 +116,62 @@ export const withPayload = (nextConfig = {}, options = {}) => {
106116
]
107117
},
108118
serverExternalPackages: [
119+
// serverExternalPackages = webpack.externals, but with turbopack support and an additional check
120+
// for whether the package is resolvable from the project root
109121
...(nextConfig.serverExternalPackages || []),
110-
// These packages always need to be external, both during dev and production. This is because they install dependencies
111-
// that will error when trying to bundle them (e.g. drizzle-kit, libsql, esbuild etc.).
112-
// We cannot externalize those problem-packages directly. We can only externalize packages that are manually installed
113-
// by the end user. Otherwise, the require('externalPackage') calls generated by the bundler would fail during runtime,
114-
// as you cannot import dependencies of dependencies in a lot of package managers like pnpm. We'd have to force users
115-
// to install the dependencies directly.
116-
// Thus, we externalize the "entry-point" = the package that is installed by the end user, which would be our db adapters.
117-
//
122+
// Can be externalized, because we require users to install graphql themselves - we only rely on it as a peer dependency => resolvable from the project root.
118123
//
119-
// External because it installs mongoose (part of default serverExternalPackages: https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/server-external-packages.json => would throw warning if we don't exclude the entry-point package):
120-
'@payloadcms/db-mongodb',
121-
// External because they install dependencies like drizzle, libsql, esbuild etc.:
122-
'@payloadcms/db-postgres',
123-
'@payloadcms/db-sqlite',
124-
'@payloadcms/db-vercel-postgres',
125-
'@payloadcms/drizzle',
126-
'@payloadcms/db-d1-sqlite',
127-
// External because they install @aws-sdk/client-s3:
128-
'@payloadcms/payload-cloud',
129-
// External, because it installs import-in-the-middle and require-in-the-middle - both in the default serverExternalPackages list.
130-
'@sentry/nextjs',
131-
// Can be externalized, because we require users to install graphql themselves - we only rely on it as a peer dependency.
132124
// WHY: without externalizing graphql, a graphql version error will be thrown
133125
// during runtime ("Ensure that there is only one instance of \"graphql\" in the node_modules\ndirectory.")
134126
'graphql',
135-
// TODO: We need to externalize @payloadcms/storage-s3 as well, once Next.js has the ability to exclude @payloadcms/storage-s3/client from being externalized.
136-
// Do not bundle additional server-only packages during dev to improve compilation speed
137-
...(process.env.NODE_ENV === 'development' && options.devBundleServerPackages === false
138-
? [
127+
// External, because it installs import-in-the-middle and require-in-the-middle - both in the default serverExternalPackages list.
128+
'@sentry/nextjs',
129+
...(process.env.NODE_ENV === 'development' && options.devBundleServerPackages !== true
130+
? /**
131+
* Unless explicitly disabled by the user, by passing `devBundleServerPackages: true` to withPayload, we
132+
* do not bundle server-only packages during dev for two reasons:
133+
*
134+
* 1. Performance: Fewer files to compile means faster compilation speeds.
135+
* 2. Turbopack support: Webpack's externals are not supported by Turbopack.
136+
*
137+
* Regarding Turbopack support: Unlike webpack.externals, we cannot use serverExternalPackages to
138+
* externalized packages that are not resolvable from the project root. So including a package like
139+
* "drizzle-kit" in here would do nothing - Next.js will ignore the rule and still bundle the package -
140+
* because it detects that the package is not resolvable from the project root (= not directly installed
141+
* by the user in their own package.json).
142+
*
143+
* Instead, we can use serverExternalPackages for the entry-point packages that *are* installed directly
144+
* by the user (e.g. db-postgres, which then installs drizzle-kit as a dependency).
145+
*
146+
*
147+
*
148+
* We should only do this during development, not build, because externalizing these packages can hurt
149+
* the bundle size. Not only does it disable tree-shaking, it also risks installing duplicate copies of the
150+
* same package.
151+
*
152+
* Example:
153+
* - @payloadcms/richtext-lexical (in bundle) -> installs qs-esm (bundled because of importer)
154+
* - payload (not in bundle, external) -> installs qs-esm (external because of importer)
155+
* Result: we have two copies of qs-esm installed - one in the bundle, and one in node_modules.
156+
*
157+
* During development, these bundle size difference do not matter much, and development speed /
158+
* turbopack support are more important.
159+
*/
160+
[
139161
'payload',
162+
'@payloadcms/db-mongodb',
163+
'@payloadcms/db-postgres',
164+
'@payloadcms/db-sqlite',
165+
'@payloadcms/db-vercel-postgres',
166+
'@payloadcms/db-d1-sqlite',
167+
'@payloadcms/drizzle',
140168
'@payloadcms/email-nodemailer',
141169
'@payloadcms/email-resend',
142170
'@payloadcms/graphql',
171+
'@payloadcms/payload-cloud',
143172
'@payloadcms/plugin-redirects',
144173
// TODO: Add the following packages, excluding their /client subpath exports, once Next.js supports it
174+
// see: https://github.com/vercel/next.js/discussions/76991
145175
//'@payloadcms/plugin-cloud-storage',
146176
//'@payloadcms/plugin-sentry',
147177
//'@payloadcms/plugin-stripe',
@@ -162,7 +192,51 @@ export const withPayload = (nextConfig = {}, options = {}) => {
162192

163193
return {
164194
...incomingWebpackConfig,
165-
externals: [...(incomingWebpackConfig?.externals || []), 'require-in-the-middle'],
195+
externals: [
196+
...(incomingWebpackConfig?.externals || []),
197+
/**
198+
* See the explanation in the serverExternalPackages section above.
199+
* We need to force Webpack to emit require() calls for these packages, even though they are not
200+
* resolvable from the project root. You would expect this to error during runtime, but Next.js seems to be able to require these just fine.
201+
*
202+
* This is the only way to get Webpack Build to work, without the bundle size caveats of externalizing the
203+
* entry point packages, as explained in the serverExternalPackages section above.
204+
*/
205+
'drizzle-kit',
206+
'drizzle-kit/api',
207+
'sharp',
208+
'libsql',
209+
'require-in-the-middle',
210+
],
211+
resolve: {
212+
...(incomingWebpackConfig?.resolve || {}),
213+
alias: {
214+
...(incomingWebpackConfig?.resolve?.alias || {}),
215+
},
216+
fallback: {
217+
...(incomingWebpackConfig?.resolve?.fallback || {}),
218+
/*
219+
* This fixes the following warning when running next build with webpack (tested on Next.js 16.0.3 with Payload 3.64.0):
220+
*
221+
* ⚠ Compiled with warnings in 8.7s
222+
*
223+
* ./node_modules/.pnpm/mongodb@6.16.0/node_modules/mongodb/lib/deps.js
224+
* Module not found: Can't resolve 'aws4' in '/Users/alessio/Documents/temp/next16p/node_modules/.pnpm/mongodb@6.16.0/node_modules/mongodb/lib'
225+
*
226+
* Import trace for requested module:
227+
* ./node_modules/.pnpm/mongodb@6.16.0/node_modules/mongodb/lib/deps.js
228+
* ./node_modules/.pnpm/mongodb@6.16.0/node_modules/mongodb/lib/client-side-encryption/client_encryption.js
229+
* ./node_modules/.pnpm/mongodb@6.16.0/node_modules/mongodb/lib/index.js
230+
* ./node_modules/.pnpm/mongoose@8.15.1/node_modules/mongoose/lib/index.js
231+
* ./node_modules/.pnpm/mongoose@8.15.1/node_modules/mongoose/index.js
232+
* ./node_modules/.pnpm/@payloadcms+db-mongodb@3.64.0_payload@3.64.0_graphql@16.12.0_typescript@5.7.3_/node_modules/@payloadcms/db-mongodb/dist/index.js
233+
* ./src/payload.config.ts
234+
* ./src/app/my-route/route.ts
235+
*
236+
**/
237+
aws4: false,
238+
},
239+
},
166240
plugins: [
167241
...(incomingWebpackConfig?.plugins || []),
168242
// Fix cloudflare:sockets error: https://github.com/vercel/next.js/discussions/50177

0 commit comments

Comments
 (0)