Files
payloadcms/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts
Alessio Gravili fd0ff51296 perf: faster page navigation by speeding up createClientConfig, speed up version fetching, speed up lexical init. Up to 100x faster (#9457)
If you had a lot of fields and collections, createClientConfig would be
extremely slow, as it was copying a lot of memory. In my test config
with a lot of fields and collections, it took 4 seconds(!!).

And not only that, it also ran between every single page navigation.

This PR significantly speeds up the createClientConfig function. In my
test config, its execution speed went from 4 seconds to 50 ms.
Additionally, createClientConfig is now properly cached in both dev &
prod. It no longer runs between every single page navigation. Even if
you trigger a full page reload, createClientConfig will be cached and
not run again. Despite that, HMR remains fully-functional.

This will make payload feel noticeably faster for large configs -
especially if it contains a lot of richtext fields, as it was previously
deep-copying the relatively large richText editor configs over and over
again.

## Before - 40 sec navigation speed

https://github.com/user-attachments/assets/fe6b707a-459b-44c6-982a-b277f6cbb73f

## After - 1 sec navigation speed

https://github.com/user-attachments/assets/384fba63-dc32-4396-b3c2-0353fcac6639

## Todo

- [x] Implement ClientSchemaMap and cache it, to remove
createClientField call in our form state endpoint
- [x] Enable schemaMap caching for dev
- [x] Cache lexical clientField generation, or add it to the parent
clientConfig

## Lexical changes

Red: old / removed
Green: new

![CleanShot 2024-11-22 at 21 07
41@2x](https://github.com/user-attachments/assets/f8321218-763c-4120-9353-076c381f33fb)

### Speed up version queries

This PR comes with performance optimizations for fetching versions
before a document is loaded. Not only does it use the new select API to
limit the fields it queries, it also completely skips a database query
if the current document is published.

### Speed up lexical init

Removes a bunch of unnecessary deep copying of lexical objects which
caused higher memory usage and slower load times. Additionally, the
lexical default config sanitization now happens less often.
2024-11-26 14:31:14 -07:00

128 lines
3.0 KiB
TypeScript

import type { I18n } from '@payloadcms/translations'
import type { Field, FieldSchemaMap, SanitizedConfig } from 'payload'
import { MissingEditorProp } from 'payload'
import { getFieldPaths, tabHasName } from 'payload/shared'
type Args = {
config: SanitizedConfig
fields: Field[]
i18n: I18n<any, any>
parentIndexPath: string
parentSchemaPath: string
schemaMap: FieldSchemaMap
}
export const traverseFields = ({
config,
fields,
i18n,
parentIndexPath,
parentSchemaPath,
schemaMap,
}: Args) => {
for (const [index, field] of fields.entries()) {
const { indexPath, schemaPath } = getFieldPaths({
field,
index,
parentIndexPath: 'name' in field ? '' : parentIndexPath,
parentPath: '',
parentSchemaPath,
})
schemaMap.set(schemaPath, field)
switch (field.type) {
case 'array':
case 'group':
traverseFields({
config,
fields: field.fields,
i18n,
parentIndexPath: '',
parentSchemaPath: schemaPath,
schemaMap,
})
break
case 'blocks':
field.blocks.map((block) => {
const blockSchemaPath = `${schemaPath}.${block.slug}`
schemaMap.set(blockSchemaPath, block)
traverseFields({
config,
fields: block.fields,
i18n,
parentIndexPath: '',
parentSchemaPath: blockSchemaPath,
schemaMap,
})
})
break
case 'collapsible':
case 'row':
traverseFields({
config,
fields: field.fields,
i18n,
parentIndexPath: indexPath,
parentSchemaPath,
schemaMap,
})
break
case 'richText':
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
if (typeof field.editor.generateSchemaMap === 'function') {
field.editor.generateSchemaMap({
config,
field,
i18n,
schemaMap,
schemaPath,
})
}
break
case 'tabs':
field.tabs.map((tab, tabIndex) => {
const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
field: {
...tab,
type: 'tab',
},
index: tabIndex,
parentIndexPath: indexPath,
parentPath: '',
parentSchemaPath,
})
schemaMap.set(tabSchemaPath, tab)
traverseFields({
config,
fields: tab.fields,
i18n,
parentIndexPath: tabHasName(tab) ? '' : tabIndexPath,
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
schemaMap,
})
})
break
}
}
}