perf(ui): skip rendering custom components hidden by admin.condition#16819
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.
…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.
…rendering-conditions-3x
Mirror the early-exit guard from perf/server-component-rendering-conditions: - Preserve value/initialValue on the minimal state entry so the client can surface field values when conditions flip back to true without waiting on another round-trip. - Drop the now-redundant `passesCondition === false` write inside the tabs branch. The early-exit captures that case before reaching this branch, so the inner conditional is unreachable. - Match comment wording with the conditions branch for cleaner cross-branch diffs.
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.
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.
|
paulpopus
approved these changes
Jun 2, 2026
Contributor
|
🚀 This is included in version v3.85.1 |
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.
Identical to #16780, back-ported for 3.x.