From 2d7626c3e9a6fafef03e0012f832f712c6eb3dea Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 14 Nov 2024 13:22:42 -0500 Subject: [PATCH] perf: removes undefined props from rsc requests (#9195) This is in effort to reduce overall HTML bloat, undefined props still go through the request as `$undefined` and must be explicitly omitted. --- .../payload/src/collections/config/client.ts | 15 +++++---- packages/payload/src/config/client.ts | 2 +- packages/payload/src/fields/config/client.ts | 23 ++++++++----- packages/payload/src/globals/config/client.ts | 2 +- .../payload/src/utilities/deepCopyObject.ts | 12 +++++-- .../payload/src/utilities/removeUndefined.ts | 3 ++ .../elements/RenderServerComponent/index.tsx | 33 +++++++++++-------- packages/ui/src/utilities/removeUndefined.ts | 3 ++ 8 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 packages/payload/src/utilities/removeUndefined.ts create mode 100644 packages/ui/src/utilities/removeUndefined.ts diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index d835ab660..e01ad1f57 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -20,7 +20,7 @@ export type ServerOnlyCollectionProperties = keyof Pick< export type ServerOnlyCollectionAdminProperties = keyof Pick< SanitizedCollectionConfig['admin'], - 'baseListFilter' | 'hidden' + 'baseListFilter' | 'components' | 'hidden' > export type ServerOnlyUploadProperties = keyof Pick< @@ -34,7 +34,6 @@ export type ServerOnlyUploadProperties = keyof Pick< export type ClientCollectionConfig = { admin: { - components: null description?: StaticDescription livePreview?: Omit preview?: boolean @@ -76,6 +75,7 @@ const serverOnlyUploadProperties: Partial[] = [ const serverOnlyCollectionAdminProperties: Partial[] = [ 'hidden', 'baseListFilter', + 'components', // 'preview' is handled separately // `livePreview` is handled separately ] @@ -89,7 +89,10 @@ export const createClientCollectionConfig = ({ defaultIDType: Payload['config']['db']['defaultIDType'] i18n: I18nClient }): ClientCollectionConfig => { - const clientCollection = deepCopyObjectSimple(collection) as unknown as ClientCollectionConfig + const clientCollection = deepCopyObjectSimple( + collection, + true, + ) as unknown as ClientCollectionConfig clientCollection.fields = createClientFields({ clientFields: clientCollection?.fields || [], @@ -150,8 +153,6 @@ export const createClientCollectionConfig = ({ clientCollection.admin.preview = true } - clientCollection.admin.components = null - let description = undefined if (collection.admin?.description) { @@ -165,7 +166,9 @@ export const createClientCollectionConfig = ({ } } - clientCollection.admin.description = description + if (description) { + clientCollection.admin.description = description + } if ( 'livePreview' in clientCollection.admin && diff --git a/packages/payload/src/config/client.ts b/packages/payload/src/config/client.ts index b91374bd7..031f12d32 100644 --- a/packages/payload/src/config/client.ts +++ b/packages/payload/src/config/client.ts @@ -79,7 +79,7 @@ export const createClientConfig = ({ i18n: I18nClient }): ClientConfig => { // We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client - const clientConfig = deepCopyObjectSimple(config) as unknown as ClientConfig + const clientConfig = deepCopyObjectSimple(config, true) as unknown as ClientConfig for (const key of serverOnlyConfigProperties) { if (key in clientConfig) { diff --git a/packages/payload/src/fields/config/client.ts b/packages/payload/src/fields/config/client.ts index 8038990eb..ac0f70b8f 100644 --- a/packages/payload/src/fields/config/client.ts +++ b/packages/payload/src/fields/config/client.ts @@ -18,6 +18,7 @@ import type { Payload } from '../../types/index.js' import { MissingEditorProp } from '../../errors/MissingEditorProp.js' import { fieldAffectsData } from '../../fields/config/types.js' import { flattenTopLevelFields } from '../../index.js' +import { removeUndefined } from '../../utilities/removeUndefined.js' // Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people // to accidentally use it instead of ClientField and get confused @@ -99,8 +100,9 @@ export const createClientField = ({ clientField.admin.style = { ...clientField.admin.style, '--field-width': clientField.admin.width, - width: undefined, // avoid needlessly adding this to the element's style attribute } + + delete clientField.admin.style.width // avoid needlessly adding this to the element's style attribute } else { if (!(clientField.admin.style instanceof Object)) { clientField.admin.style = {} @@ -137,19 +139,24 @@ export const createClientField = ({ if (incomingField.blocks?.length) { for (let i = 0; i < incomingField.blocks.length; i++) { const block = incomingField.blocks[i] - const clientBlock: ClientBlock = { + + // prevent $undefined from being passed through the rsc requests + const clientBlock = removeUndefined({ slug: block.slug, - admin: { - components: {}, - custom: block.admin?.custom, - }, fields: field.blocks?.[i]?.fields || [], imageAltText: block.imageAltText, imageURL: block.imageURL, + }) satisfies ClientBlock + + if (block.admin?.custom) { + clientBlock.admin = { + custom: block.admin.custom, + } } if (block.labels) { clientBlock.labels = {} as unknown as LabelsClient + if (block.labels.singular) { if (typeof block.labels.singular === 'function') { clientBlock.labels.singular = block.labels.singular({ t: i18n.t }) @@ -184,6 +191,7 @@ export const createClientField = ({ case 'radio': + // eslint-disable-next-line no-fallthrough case 'select': { const field = clientField as RadioFieldClient | SelectFieldClient @@ -206,7 +214,6 @@ export const createClientField = ({ break } - case 'richText': { if (!incomingField?.editor) { throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor @@ -218,6 +225,7 @@ export const createClientField = ({ break } + case 'tabs': { const field = clientField as unknown as TabsFieldClient @@ -325,7 +333,6 @@ export const createClientFields = ({ }, hidden: true, label: 'ID', - localized: undefined, }) } diff --git a/packages/payload/src/globals/config/client.ts b/packages/payload/src/globals/config/client.ts index 614d63e4b..e9af523b7 100644 --- a/packages/payload/src/globals/config/client.ts +++ b/packages/payload/src/globals/config/client.ts @@ -49,7 +49,7 @@ export const createClientGlobalConfig = ({ global: SanitizedConfig['globals'][0] i18n: I18nClient }): ClientGlobalConfig => { - const clientGlobal = deepCopyObjectSimple(global) as unknown as ClientGlobalConfig + const clientGlobal = deepCopyObjectSimple(global, true) as unknown as ClientGlobalConfig clientGlobal.fields = createClientFields({ clientFields: clientGlobal?.fields || [], diff --git a/packages/payload/src/utilities/deepCopyObject.ts b/packages/payload/src/utilities/deepCopyObject.ts index f914380f3..34dff51c9 100644 --- a/packages/payload/src/utilities/deepCopyObject.ts +++ b/packages/payload/src/utilities/deepCopyObject.ts @@ -98,12 +98,12 @@ Benchmark: https://github.com/AlessioGr/fastest-deep-clone-json/blob/main/test/b * `Set`, `Buffer`, ... are not allowed. * @returns The cloned JSON value. */ -export function deepCopyObjectSimple(value: T): T { +export function deepCopyObjectSimple(value: T, filterUndefined = false): T { if (typeof value !== 'object' || value === null) { return value } else if (Array.isArray(value)) { return value.map((e) => - typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e), + typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e, filterUndefined), ) as T } else { if (value instanceof Date) { @@ -112,7 +112,13 @@ export function deepCopyObjectSimple(value: T): T { const ret: { [key: string]: T } = {} for (const k in value) { const v = value[k] - ret[k] = typeof v !== 'object' || v === null ? v : (deepCopyObjectSimple(v as T) as any) + if (filterUndefined && v === undefined) { + continue + } + ret[k] = + typeof v !== 'object' || v === null + ? v + : (deepCopyObjectSimple(v as T, filterUndefined) as any) } return ret as unknown as T } diff --git a/packages/payload/src/utilities/removeUndefined.ts b/packages/payload/src/utilities/removeUndefined.ts new file mode 100644 index 000000000..56bf556e6 --- /dev/null +++ b/packages/payload/src/utilities/removeUndefined.ts @@ -0,0 +1,3 @@ +export function removeUndefined(obj: T): T { + return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T +} diff --git a/packages/ui/src/elements/RenderServerComponent/index.tsx b/packages/ui/src/elements/RenderServerComponent/index.tsx index 11b41dd55..7f08d07db 100644 --- a/packages/ui/src/elements/RenderServerComponent/index.tsx +++ b/packages/ui/src/elements/RenderServerComponent/index.tsx @@ -7,6 +7,8 @@ import { } from 'payload/shared' import React from 'react' +import { removeUndefined } from '../../utilities/removeUndefined.js' + export const getFromImportMap = (args: { importMap: ImportMap PayloadComponent: PayloadComponent @@ -66,7 +68,13 @@ export const RenderServerComponent: React.FC<{ if (typeof Component === 'function') { const isRSC = isReactServerComponentOrFunction(Component) - return + // prevent $undefined from being passed through the rsc requests + const sanitizedProps = removeUndefined({ + ...clientProps, + ...(isRSC ? serverProps : {}), + }) + + return } if (typeof Component === 'string' || isPlainObject(Component)) { @@ -79,18 +87,17 @@ export const RenderServerComponent: React.FC<{ if (ResolvedComponent) { const isRSC = isReactServerComponentOrFunction(ResolvedComponent) - return ( - - ) + // prevent $undefined from being passed through rsc requests + const sanitizedProps = removeUndefined({ + ...clientProps, + ...(isRSC ? serverProps : {}), + ...(isRSC && typeof Component === 'object' && Component?.serverProps + ? Component.serverProps + : {}), + ...(typeof Component === 'object' && Component?.clientProps ? Component.clientProps : {}), + }) + + return } } diff --git a/packages/ui/src/utilities/removeUndefined.ts b/packages/ui/src/utilities/removeUndefined.ts new file mode 100644 index 000000000..56bf556e6 --- /dev/null +++ b/packages/ui/src/utilities/removeUndefined.ts @@ -0,0 +1,3 @@ +export function removeUndefined(obj: T): T { + return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T +}