Skip to content

feat(next): prevent admin panel errors when cacheComponents is enabled#16020

Merged
AlessioGr merged 7 commits into
mainfrom
feat/cache-components
Mar 26, 2026
Merged

feat(next): prevent admin panel errors when cacheComponents is enabled#16020
AlessioGr merged 7 commits into
mainfrom
feat/cache-components

Conversation

@AlessioGr

@AlessioGr AlessioGr commented Mar 20, 2026

Copy link
Copy Markdown
Member

Fixes #8897, partially addresses cache component support (#14460)

Adds initial support for Next.js cacheComponents so users who enable it for their frontend don't get errors from the Payload admin panel. This PR addresses the obvious breakage but does not guarantee full compatibility - see the "Known Limitations" section below.

When cacheComponents is enabled in next.config, Next.js throws "Data that blocks navigation was accessed outside of <Suspense>" errors because the admin layout reads cookies, headers, and does auth queries at the top level. This prevents users from enabling cacheComponents at all if Payload is in the same Next.js app.

The fix has two parts. First, withPayload now detects cacheComponents in the Next.js config and sets a PAYLOAD_CACHE_COMPONENTS_ENABLED env var. Second, RootLayout reads that env var and conditionally wraps its content in <Suspense fallback={null}> above the <html> tag, which suppresses the errors. When cacheComponents is not enabled, the Suspense is not used at all and behavior is identical to before.

Known Limitations

Page flash on hard refresh

When cacheComponents is enabled, hard refresh shows a brief gray flash before the admin panel appears. Without cacheComponents there is no flash. There is no per-route opt-out for this behavior. Related issue: vercel/next.js#86739

HTTP status codes (404 returns 200)

With cacheComponents, notFound() returns HTTP 200 instead of 404. This happens because the Suspense boundary above <html> causes Next.js to commit response headers (with status 200) before notFound() runs inside the suspended content. The not-found UI still renders correctly - only the HTTP status code is wrong. This is a documented Next.js streaming limitation.

DOM accumulation breaks Playwright selectors

When cacheComponents is enabled, Next.js wraps route segments in React's <Activity> component, keeping up to 3 previously visited pages in the DOM with display: none !important instead of unmounting them. This means Playwright selectors like page.locator('#field-title') resolve to multiple elements (the visible one and hidden copies from cached pages), causing strict mode violations. This is a known issue affecting all Next.js apps using cacheComponents with Playwright.

Because of this, we cannot reliably run our e2e test suite with cacheComponents enabled. Adapting the test suite would require rewriting a large number of selectors across hundreds of tests - most of our e2e tests use page.locator() with ID selectors, which would all break when Activity duplicates the DOM. Until the Next.js team provides a per-route opt-out for Activity (which they are actively exploring), we cannot guarantee full admin panel compatibility beyond the initial error suppression this PR provides.


@github-actions

github-actions Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 984.93 KB 🆕 Added
packages/payload/meta_index.json esbuild/index.js 1.34 MB 🆕 Added
packages/payload/meta_shared.json esbuild/exports/shared.js 190.93 KB 🆕 Added
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 280.56 KB 🆕 Added
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.18 MB 🆕 Added
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 16.32 KB 🆕 Added
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████▋ }}}$ 82.5%, 808.32 KB
dist/views/Version ${{\color{Goldenrod}{ █▎ }}}$ 5.3%, 51.49 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 21.37 KB
dist/views/Document ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 16.59 KB
dist/views/List ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 11.38 KB
dist/views/Root ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 9.03 KB
dist/views/Versions ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 6.17 KB
dist/views/API ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 6.13 KB
dist/elements/Nav ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 5.96 KB
dist/views/Account ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 5.55 KB
dist/elements/DocumentHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 4.81 KB
dist/views/Login ${{\color{Goldenrod}{ }}}$ 0.4%, 4.40 KB
dist/layouts/Root ${{\color{Goldenrod}{ }}}$ 0.3%, 3.18 KB
dist/views/ForgotPassword ${{\color{Goldenrod}{ }}}$ 0.3%, 3.09 KB
dist/views/CreateFirstUser ${{\color{Goldenrod}{ }}}$ 0.3%, 2.81 KB
dist/templates/Default ${{\color{Goldenrod}{ }}}$ 0.3%, 2.64 KB
dist/views/BrowseByFolder ${{\color{Goldenrod}{ }}}$ 0.3%, 2.61 KB
dist/views/CollectionFolders ${{\color{Goldenrod}{ }}}$ 0.2%, 2.44 KB
dist/views/ResetPassword ${{\color{Goldenrod}{ }}}$ 0.2%, 2.40 KB
dist/views/Logout ${{\color{Goldenrod}{ }}}$ 0.2%, 1.94 KB
(other) ${{\color{Goldenrod}{ ████▍ }}}$ 17.5%, 171.93 KB

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████ }}}$ 68.0%, 908.21 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.3%, 43.59 KB
dist/collections/operations ${{\color{Goldenrod}{ ▊ }}}$ 3.0%, 39.92 KB
dist/versions/migrations ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 18.50 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 15.63 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.19 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.13 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 12.90 KB
dist/queues/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 12.80 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.54 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.87 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.65 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.35 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.69 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.67 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.54 KB
dist/collections/endpoints ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 6.23 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ }}}$ 0.4%, 5.80 KB
dist/auth/strategies ${{\color{Goldenrod}{ }}}$ 0.4%, 5.50 KB
dist/queues/config ${{\color{Goldenrod}{ }}}$ 0.4%, 5.34 KB
(other) ${{\color{Goldenrod}{ ████████ }}}$ 32.0%, 428.28 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ███████████████████▊ }}}$ 79.3%, 148.51 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▍ }}}$ 5.6%, 10.54 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 2.54 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.42 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.28 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 1.04 KB
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 943 B
dist/folders/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 916 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.4%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.4%, 713 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 651 B
dist/collections/config ${{\color{Goldenrod}{ }}}$ 0.3%, 570 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
dist/auth/sessions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 525 B
dist/fields/getFieldPaths.js ${{\color{Goldenrod}{ }}}$ 0.3%, 485 B
dist/errors/APIError.js ${{\color{Goldenrod}{ }}}$ 0.2%, 438 B
dist/utilities/getSafeRedirect.js ${{\color{Goldenrod}{ }}}$ 0.2%, 423 B
(other) ${{\color{Goldenrod}{ █████▏ }}}$ 20.7%, 38.75 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███▏ }}}$ 12.8%, 35.38 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▉ }}}$ 11.5%, 32.00 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▏ }}}$ 8.8%, 24.36 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 23.70 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▋ }}}$ 6.8%, 18.99 KB
dist/features/link ${{\color{Goldenrod}{ █▋ }}}$ 6.6%, 18.24 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▍ }}}$ 5.8%, 16.08 KB
dist/features/upload ${{\color{Goldenrod}{ █▎ }}}$ 5.0%, 13.77 KB
dist/features/textState ${{\color{Goldenrod}{ █ }}}$ 4.0%, 11.08 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.3%, 9.03 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▊ }}}$ 3.1%, 8.49 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 7.39 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 7.15 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 7.05 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.08 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.00 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.46 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.22 KB
dist/lexical/theme ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.62 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.59 KB
(other) ${{\color{Goldenrod}{ █████████████████████▊ }}}$ 87.2%, 241.93 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████▎ }}}$ 49.3%, 579.02 KB
dist/elements/FolderView ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 29.37 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 27.80 KB
dist/views/Edit ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 17.30 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 17.29 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 15.85 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.78 KB
dist/elements/Table ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.77 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 14.24 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 13.89 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 10.36 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 9.09 KB
dist/providers/Folders ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.47 KB
dist/elements/HTMLDiff ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.38 KB
dist/elements/ListHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.99 KB
dist/fields/Array ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.73 KB
dist/views/CollectionFolder ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.50 KB
dist/views/List ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.35 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.33 KB
dist/elements/LivePreview ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.02 KB
(other) ${{\color{Goldenrod}{ ████████████▋ }}}$ 50.7%, 594.78 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ █████ }}}$ 20.0%, 3.12 KB
../../node_modules ${{\color{Goldenrod}{ ████▎ }}}$ 17.0%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▍ }}}$ 9.8%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 862 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █▎ }}}$ 5.2%, 814 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █▏ }}}$ 4.9%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 756 B
dist/elements/Translation ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 399 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 339 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 329 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 301 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 159 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 152 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 147 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 146 B
(other) ${{\color{Goldenrod}{ ████████████████████ }}}$ 80.0%, 12.51 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

@kevineinarsson

Copy link
Copy Markdown

vercel/next.js#91662 probably related to failures.

When a collection item contained a relationship table, the server function that loads the relationship returns text/html instead of text/x-component.

16.2.1.canary-3 works though.

@AlessioGr AlessioGr changed the title feat(next): add cacheComponents compatibility for admin panel feat(next): prevent admin panel errors when cacheComponents is enabled Mar 21, 2026
@AlessioGr AlessioGr enabled auto-merge (squash) March 21, 2026 00:39
apiRoute,
path: `${docEndpoint}?${params}`,
serverURL: serverURL || window.location.origin,
serverURL: origin,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing window.* here breaks with cache components enabled:

Error: Switched to client rendering because the server rendering errored:
window is not defined
   at .next/dev/server/chunks/ssr/0tmr_@payloadcms_next_dist_04sey_p._.js:1010
      1008 |         apiRoute,
      1009 |         path: `${docEndpoint}?${params}`,
    > 1010 |         serverURL: serverURL || window.location.origin
           |                                 ^
      1011 |     });

@themavik themavik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

playwright.config now loads repo-root .env after test.env; duplicate keys in .env will override test.env and can make local E2E differ from CI in non-obvious ways.

@AlessioGr AlessioGr merged commit 1ecd7dd into main Mar 26, 2026
156 checks passed
@AlessioGr AlessioGr deleted the feat/cache-components branch March 26, 2026 17:17
@github-actions

github-actions Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

🚀 This is included in version v3.81.0

milamer pushed a commit to milamer/payload that referenced this pull request Apr 20, 2026
payloadcms#16020)

Fixes payloadcms#8897, addresses
payloadcms#14460

Adds initial support for Next.js `cacheComponents` so users who enable
it for their frontend don't get errors from the Payload admin panel.
This PR addresses the obvious breakage but does not guarantee full
compatibility - see the "Known Limitations" section below.

When `cacheComponents` is enabled in `next.config`, Next.js throws "Data
that blocks navigation was accessed outside of `<Suspense>`" errors
because the admin layout reads cookies, headers, and does auth queries
at the top level. This prevents users from enabling `cacheComponents` at
all if Payload is in the same Next.js app.

The fix has two parts. First, `withPayload` now detects
`cacheComponents` in the Next.js config and sets a
`PAYLOAD_CACHE_COMPONENTS_ENABLED` env var. Second, `RootLayout` reads
that env var and conditionally wraps its content in `<Suspense
fallback={null}>` above the `<html>` tag, which suppresses the errors.
When `cacheComponents` is not enabled, the Suspense is not used at all
and behavior is identical to before.

## Known Limitations

These are all caused by Next.js's `cacheComponents` and likely cannot be
fixed from our side.

### Page flash on hard refresh

When `cacheComponents` is enabled, hard refresh shows a brief gray flash
before the admin panel appears. Without `cacheComponents` there is no
flash. There is no per-route opt-out for this behavior. Related issue:
vercel/next.js#86739

### HTTP status codes (404 returns 200)

With `cacheComponents`, `notFound()` returns HTTP 200 instead of 404.
This happens because the Suspense boundary above `<html>` causes Next.js
to commit response headers (with status 200) before `notFound()` runs
inside the suspended content. The not-found UI still renders correctly -
only the HTTP status code is wrong. This is a [documented Next.js
streaming
limitation](https://nextjs.org/docs/app/api-reference/file-conventions/loading#status-codes).

### DOM accumulation breaks Playwright selectors

When `cacheComponents` is enabled, Next.js wraps route segments in
React's `<Activity>` component, keeping up to 3 previously visited pages
in the DOM with `display: none !important` instead of unmounting them.
This means Playwright selectors like `page.locator('#field-title')`
resolve to multiple elements (the visible one and hidden copies from
cached pages), causing strict mode violations. This is a [known
issue](vercel/next.js#86577) affecting all
Next.js apps using `cacheComponents` with Playwright.

Because of this, we cannot reliably run our e2e test suite with
`cacheComponents` enabled. Adapting the test suite would require
rewriting a large number of selectors across hundreds of tests - most of
our e2e tests use `page.locator()` with ID selectors, which would all
break when Activity duplicates the DOM. Until the Next.js team provides
a per-route opt-out for Activity (which they are [actively
exploring](vercel/next.js#86577 (comment))),
we cannot _guarantee_ full admin panel compatibility beyond the initial
error suppression this PR provides.
@thernstig

Copy link
Copy Markdown

@AlessioGr should the note here be updated?

https://payloadcms.com/docs/getting-started/installation

Cache Components: While Next.js cacheComponents can be enabled alongside Payload without causing errors in the admin panel, full compatibility is not guaranteed. See this GitHub pull request for the latest status.

@AlessioGr

Copy link
Copy Markdown
Member Author

@thernstig I think the note is still accurate. What would you update it to?

@thernstig

thernstig commented May 29, 2026

Copy link
Copy Markdown

@AlessioGr nevermind, I thought somehow this fixed compability as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot build with dynamicIO enabled.

5 participants