Skip to content

Commit 31ae27b

Browse files
authored
perf: significantly reduce form state response size by up to 3x (#9388)
This significantly optimizes the form state, reducing its size by up to more than 3x and improving overall response times. This change also has rolling effects on initial page size as well, where the initial state for the entire form is sent through the request. To achieve this, we do the following: - Remove `$undefined` strings that are potentially attached to properties like `value`, `initialValue`, `fieldSchema`, etc. - Remove unnecessary properties like empty `errorPaths` arrays and empty `customComponents` objects, which only need to exist if used - Remove unnecessary properties like `valid`, `passesCondition`, etc. which only need to be returned if explicitly `false` - Remove unused properties like `isSidebar`, which simply don't need to exist at all, as they can be easily calculated during render ## Results The following results were gathered by booting up each test suite listed below using the existing seed data, navigating to a document in the relevant collection, then typing a single letter into the noted field in order to invoke new form-state. The result is then saved to the file system for comparison. | Test Suite | Collection | Field | Before | After | Percentage Change | |------|------|---------|--------|--------|--------| | `field-perf` | `blocks-collection` | `layout.0.field1` | 227kB | 110 kB | ~52% smaller | | `fields` | `array-fields` | `items.0.text` | 14 kB | 4 kB | ~72% smaller | | `fields` | `block-fields` | `blocks.0.richText` | 25 kB | 14 kB | ~44% smaller |
1 parent 8217842 commit 31ae27b

File tree

11 files changed

+159
-97
lines changed

11 files changed

+159
-97
lines changed

packages/next/src/utilities/initPage/handleAdminPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function getRouteInfo({
5151
globalConfig = config.globals.find((global) => global.slug === globalSlug)
5252
}
5353

54-
// If the collection is using a custom ID, we need to determine it's type
54+
// If the collection is using a custom ID, we need to determine its type
5555
if (collectionConfig && payload) {
5656
if (payload.collections?.[collectionSlug]?.customIDType) {
5757
idType = payload.collections?.[collectionSlug].customIDType

packages/payload/src/admin/forms/Field.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {
1818
export type ClientFieldWithOptionalType = MarkOptional<ClientField, 'type'>
1919

2020
export type ClientComponentProps = {
21-
customComponents: FormField['customComponents']
21+
customComponents?: FormField['customComponents']
2222
field: ClientBlock | ClientField | ClientTab
2323
forceRender?: boolean
2424
permissions?: SanitizedFieldPermissions

packages/payload/src/admin/forms/Form.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,13 @@ export type FieldState = {
4545
*/
4646
fieldSchema?: Field
4747
filterOptions?: FilterOptionsResult
48-
initialValue: unknown
49-
isSidebar?: boolean
48+
initialValue?: unknown
5049
passesCondition?: boolean
5150
requiresRender?: boolean
5251
rows?: Row[]
53-
valid: boolean
52+
valid?: boolean
5453
validate?: Validate
55-
value: unknown
54+
value?: unknown
5655
}
5756

5857
export type FieldStateWithoutComponents = Omit<FieldState, 'customComponents'>

packages/payload/src/admin/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,12 @@ export type RenderedField = {
453453
Field: React.ReactNode
454454
indexPath?: string
455455
initialSchemaPath?: string
456+
/**
457+
* @deprecated
458+
* This is a legacy property that will be removed in v4.
459+
* Please use `fieldIsSidebar(field)` from `payload` instead.
460+
* Or check `field.admin.position === 'sidebar'` directly.
461+
*/
456462
isSidebar: boolean
457463
path: string
458464
schemaPath: string

packages/payload/src/config/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,7 @@ export type Config = {
791791
dependencies?: AdminDependencies
792792
/**
793793
* @deprecated
794-
* This option is deprecated and will be removed in the next major version.
794+
* This option is deprecated and will be removed in v4.
795795
* To disable the admin panel itself, delete your `/app/(payload)/admin` directory.
796796
* To disable all REST API and GraphQL endpoints, delete your `/app/(payload)/api` directory.
797797
* Note: If you've modified the default paths via `admin.routes`, delete those directories instead.
@@ -803,7 +803,6 @@ export type Config = {
803803
* @default true
804804
*/
805805
autoGenerate?: boolean
806-
807806
/** The base directory for component paths starting with /.
808807
*
809808
* By default, this is process.cwd()

packages/ui/src/elements/BulkUpload/FormsManager/reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FormState } from 'payload'
1+
import type { FormFieldWithoutComponents, FormState } from 'payload'
22

33
export type State = {
44
activeIndex: number

packages/ui/src/forms/Form/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export const Form: React.FC<FormProps> = (props) => {
228228
// Execute server side validations
229229
if (Array.isArray(beforeSubmit)) {
230230
let revalidatedFormState: FormState
231+
231232
const serializableFields = deepCopyObjectSimpleWithoutReactComponents(
232233
contextRef.current.fields,
233234
)
@@ -242,7 +243,9 @@ export const Form: React.FC<FormProps> = (props) => {
242243
revalidatedFormState = result
243244
}, Promise.resolve())
244245

245-
const isValid = Object.entries(revalidatedFormState).every(([, field]) => field.valid)
246+
const isValid = Object.entries(revalidatedFormState).every(
247+
([, field]) => field.valid !== false,
248+
)
246249

247250
if (!isValid) {
248251
setProcessing(false)
@@ -277,6 +280,7 @@ export const Form: React.FC<FormProps> = (props) => {
277280
const serializableFields = deepCopyObjectSimpleWithoutReactComponents(
278281
contextRef.current.fields,
279282
)
283+
280284
const data = reduceFieldsToValues(serializableFields, true)
281285

282286
for (const [key, value] of Object.entries(overrides)) {

packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts

Lines changed: 103 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
DocumentPreferences,
55
Field,
66
FieldSchemaMap,
7+
FieldState,
78
FormFieldWithoutComponents,
89
FormState,
910
FormStateWithoutComponents,
@@ -139,14 +140,14 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
139140

140141
let fieldPermissions: SanitizedFieldPermissions = true
141142

142-
const fieldState: FormFieldWithoutComponents = {
143-
errorPaths: [],
144-
fieldSchema: includeSchema ? field : undefined,
145-
initialValue: undefined,
146-
isSidebar: fieldIsSidebar(field),
147-
passesCondition,
148-
valid: true,
149-
value: undefined,
143+
const fieldState: FieldState = {}
144+
145+
if (passesCondition === false) {
146+
fieldState.passesCondition = false
147+
}
148+
149+
if (includeSchema) {
150+
fieldState.fieldSchema = field
150151
}
151152

152153
if (fieldAffectsData(field) && !fieldIsHiddenOrDisabled(field)) {
@@ -213,6 +214,10 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
213214
addErrorPathToParentArg(errorPath)
214215
}
215216

217+
if (!fieldState.errorPaths) {
218+
fieldState.errorPaths = []
219+
}
220+
216221
if (!fieldState.errorPaths.includes(errorPath)) {
217222
fieldState.errorPaths.push(errorPath)
218223
fieldState.valid = false
@@ -223,8 +228,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
223228
fieldState.errorMessage = validationResult
224229
fieldState.valid = false
225230
addErrorPathToParent(path)
226-
} else {
227-
fieldState.valid = true
228231
}
229232

230233
switch (field.type) {
@@ -237,14 +240,16 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
237240
row.id = row?.id || new ObjectId().toHexString()
238241

239242
if (!omitParents && (!filter || filter(args))) {
240-
state[parentPath + '.id'] = {
241-
fieldSchema: includeSchema
242-
? field.fields.find((field) => fieldIsID(field))
243-
: undefined,
243+
const idKey = parentPath + '.id'
244+
245+
state[idKey] = {
244246
initialValue: row.id,
245-
valid: true,
246247
value: row.id,
247248
}
249+
250+
if (includeSchema) {
251+
state[idKey].fieldSchema = field.fields.find((field) => fieldIsID(field))
252+
}
248253
}
249254

250255
acc.promises.push(
@@ -280,50 +285,58 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
280285
}),
281286
)
282287

283-
const previousRows = previousFormState?.[path]?.rows || []
284-
const collapsedRowIDsFromPrefs = preferences?.fields?.[path]?.collapsed
288+
if (!acc.rows) {
289+
acc.rows = []
290+
}
285291

286292
acc.rows.push({
287293
id: row.id,
288-
collapsed: (() => {
289-
// First, check if `previousFormState` has a matching row
290-
const previousRow = previousRows.find((prevRow) => prevRow.id === row.id)
291-
if (previousRow?.collapsed !== undefined) {
292-
return previousRow.collapsed
293-
}
294+
})
294295

295-
// If previousFormState is undefined, check preferences
296-
if (collapsedRowIDsFromPrefs !== undefined) {
297-
return collapsedRowIDsFromPrefs.includes(row.id) // Check if collapsed in preferences
298-
}
296+
const previousRows = previousFormState?.[path]?.rows || []
297+
const collapsedRowIDsFromPrefs = preferences?.fields?.[path]?.collapsed
299298

300-
// If neither exists, fallback to `field.admin.initCollapsed`
301-
return field.admin.initCollapsed
302-
})(),
303-
})
299+
const collapsed = (() => {
300+
// First, check if `previousFormState` has a matching row
301+
const previousRow = previousRows.find((prevRow) => prevRow.id === row.id)
302+
if (previousRow?.collapsed !== undefined) {
303+
return previousRow.collapsed
304+
}
305+
306+
// If previousFormState is undefined, check preferences
307+
if (collapsedRowIDsFromPrefs !== undefined) {
308+
return collapsedRowIDsFromPrefs.includes(row.id) // Check if collapsed in preferences
309+
}
310+
311+
// If neither exists, fallback to `field.admin.initCollapsed`
312+
return field.admin.initCollapsed
313+
})()
314+
315+
if (collapsed) {
316+
acc.rows[acc.rows.length - 1].collapsed = collapsed
317+
}
304318

305319
return acc
306320
},
307321
{
308322
promises: [],
309-
rows: [],
323+
rows: undefined,
310324
},
311325
)
312326

313327
// Wait for all promises and update fields with the results
314328
await Promise.all(promises)
315329

316-
fieldState.rows = rows
330+
if (rows) {
331+
fieldState.rows = rows
332+
}
317333

318334
// Unset requiresRender
319335
// so it will be removed from form state
320336
fieldState.requiresRender = false
321337

322338
// Add values to field state
323-
if (data[field.name] === null) {
324-
fieldState.value = null
325-
fieldState.initialValue = null
326-
} else {
339+
if (data[field.name] !== null) {
327340
fieldState.value = forceFullValue ? arrayValue : arrayValue.length
328341
fieldState.initialValue = forceFullValue ? arrayValue : arrayValue.length
329342

@@ -359,35 +372,48 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
359372
row.id = row?.id || new ObjectId().toHexString()
360373

361374
if (!omitParents && (!filter || filter(args))) {
362-
state[parentPath + '.id'] = {
363-
fieldSchema: includeSchema
364-
? block.fields.find((blockField) => fieldIsID(blockField))
365-
: undefined,
375+
// Handle block `id` field
376+
const idKey = parentPath + '.id'
377+
378+
state[idKey] = {
366379
initialValue: row.id,
367-
valid: true,
368380
value: row.id,
369381
}
370382

371-
state[parentPath + '.blockType'] = {
372-
fieldSchema: includeSchema
373-
? block.fields.find(
374-
(blockField) => 'name' in blockField && blockField.name === 'blockType',
375-
)
376-
: undefined,
383+
if (includeSchema) {
384+
state[idKey].fieldSchema = includeSchema
385+
? block.fields.find((blockField) => fieldIsID(blockField))
386+
: undefined
387+
}
388+
389+
// Handle `blockType` field
390+
const fieldKey = parentPath + '.blockType'
391+
392+
state[fieldKey] = {
377393
initialValue: row.blockType,
378-
valid: true,
379394
value: row.blockType,
380395
}
381396

382-
state[parentPath + '.blockName'] = {
383-
fieldSchema: includeSchema
384-
? block.fields.find(
385-
(blockField) => 'name' in blockField && blockField.name === 'blockName',
386-
)
387-
: undefined,
388-
initialValue: row.blockName,
389-
valid: true,
390-
value: row.blockName,
397+
if (includeSchema) {
398+
state[fieldKey].fieldSchema = block.fields.find(
399+
(blockField) => 'name' in blockField && blockField.name === 'blockType',
400+
)
401+
}
402+
403+
// Handle `blockName` field
404+
const blockNameKey = parentPath + '.blockName'
405+
406+
state[blockNameKey] = {}
407+
408+
if (row.blockName) {
409+
state[blockNameKey].initialValue = row.blockName
410+
state[blockNameKey].value = row.blockName
411+
}
412+
413+
if (includeSchema) {
414+
state[blockNameKey].fieldSchema = block.fields.find(
415+
(blockField) => 'name' in blockField && blockField.name === 'blockName',
416+
)
391417
}
392418
}
393419

@@ -428,16 +454,21 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
428454
}),
429455
)
430456

431-
const collapsedRowIDs = preferences?.fields?.[path]?.collapsed
432-
433457
acc.rowMetadata.push({
434458
id: row.id,
435459
blockType: row.blockType,
436-
collapsed:
437-
collapsedRowIDs === undefined
438-
? field.admin.initCollapsed
439-
: collapsedRowIDs.includes(row.id),
440460
})
461+
462+
const collapsedRowIDs = preferences?.fields?.[path]?.collapsed
463+
464+
const collapsed =
465+
collapsedRowIDs === undefined
466+
? field.admin.initCollapsed
467+
: collapsedRowIDs.includes(row.id)
468+
469+
if (collapsed) {
470+
acc.rowMetadata[acc.rowMetadata.length - 1].collapsed = collapsed
471+
}
441472
}
442473

443474
return acc
@@ -604,8 +635,10 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
604635
}
605636

606637
default: {
607-
fieldState.value = data[field.name]
608-
fieldState.initialValue = data[field.name]
638+
if (data[field.name] !== undefined) {
639+
fieldState.value = data[field.name]
640+
fieldState.initialValue = data[field.name]
641+
}
609642

610643
// Add field to state
611644
if (!filter || filter(args)) {
@@ -621,11 +654,10 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
621654
if (!filter || filter(args)) {
622655
state[path] = {
623656
disableFormData: true,
624-
errorPaths: [],
625-
initialValue: undefined,
626-
passesCondition,
627-
valid: true,
628-
value: undefined,
657+
}
658+
659+
if (passesCondition === false) {
660+
state[path].passesCondition = false
629661
}
630662
}
631663

0 commit comments

Comments
 (0)