perf(ui): skip rendering custom components hidden by admin.condition#16780
Merged
Conversation
… admin.condition Initial form-state build was eagerly rendering every field's custom components (Field, BeforeInput, AfterInput, Description, Error, Label, RowLabel), including server components, even when the field's admin.condition resolved false. The rendered React element was baked into fieldState.customComponents and only hidden client-side by WatchCondition — work was already done. Gate the renderFieldFn call in addFieldStatePromise on `passesCondition !== false`. When the condition later flips true via onChange, lastRenderedPath is undefined so renderField produces a fresh element with a current timestamp. Note: inline function conditions only. Path-valued string refs (`./Path#export`) are not resolved server-side in iterateFields yet — those still pre-render.
Contributor
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
…s collection Keeps base Posts collection from accruing condition-specific fixtures.
Cleaner than matching against a magic title string.
…n.condition Previously only the render call was gated on passesCondition; the switch still ran filterOptions resolves (DB queries on relationship/upload), blocks validation, and full recursion into hidden subtrees. Add an early-exit guard so a failing condition writes a minimal state entry and returns, skipping access checks, validation, switch processing, filterOptions, and child iteration. The client re-requests form state on condition flips, so the minimal entry is sufficient.
Now that the tabs branch unconditionally writes state[path] with passesCondition, the early-exit guard no longer needs to exclude tabs and tab field types. Dropping the exclusion skips child iteration for hidden tabs containers and tabs whose admin.condition fails. The tab branch is also simplified — incoming passesCondition is guaranteed true past the guard, so tab visibility depends solely on its own admin.condition.
The short-circuit at the top of addFieldStatePromise skips the tab branch when passesCondition is false, but the tab branch is where state[field.id] gets written, and the Tabs component on the client reads tab visibility from that key. Without the write, hidden tabs fall back to passesCondition ?? true and render visible. Mirror the state[field.id] write in the short-circuit so tabs that fail their admin.condition pick up the --hidden class as expected.
The previous fix mirrored the state[field.id] write inside the short-circuit, which required developers to remember that tab fields have a dual-keyed state contract (state[path] AND state[field.id]). Move that responsibility back to where it belongs — the tab branch — and exclude tab from the short-circuit. The tab branch now: - Uses the incoming passesCondition resolved by iterateFields instead of re-evaluating field.admin.condition. - Writes state[field.id] unconditionally. - Returns early when the tab is hidden, skipping recursion into descendants (matches the short-circuit's intent without duplicating its body). Net effect is the same perf win with one tab-specific concern living in one place.
Reorganize the tab branch so the flow reads linearly: 1. Strip-unselected check (return if not selected). 2. Write state[field.id] (the visibility marker the Tabs component reads). 3. Return early if the tab is hidden — no recursion needed. 4. Resolve child permissions and select scope. 5. Recurse into children. Previously the permissions/select setup ran before the hidden-tab early return, which was wasted work and obscured the control flow.
When a tab fails its admin.condition, the tab branch writes state[field.id] but skips recursion into descendants. If the tab later flips visible (or the tab itself was first surfaced after a previously hidden ancestor became visible), the server rebuilds state and writes a fresh state[field.id]. Without the addedByServer flag, mergeServerFormState skips brand-new entries that aren't already on the client, leaving the Tabs component reading a stale (or missing) value and rendering the tab with the wrong visibility. Mirror the addedByServer logic used for non-tab fields so newly minted tab entries actually reach the client.
paulpopus
approved these changes
Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Form state was eagerly rendering custom components even when the field's
admin.conditionevaluates to false. The rendered React component was only being hidden client-side, but should have deferred rendering entirely.This has potentially serious performance implications for two reasons:
Now, custom server components DO NOT render unless they pass conditions, as expected. Custom components will now only render if they will be mounted to the page.
Before:
Screen.Recording.2026-05-29.at.10.21.27.AM.mp4
After:
Screen.Recording.2026-05-29.at.10.19.26.AM.mp4