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.
This commit is contained in:
Jacob Fletcher
2024-11-14 13:22:42 -05:00
committed by GitHub
parent e75527b0a1
commit 2d7626c3e9
8 changed files with 61 additions and 32 deletions

View File

@@ -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<LivePreviewConfig, ServerOnlyLivePreviewProperties>
preview?: boolean
@@ -76,6 +75,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
'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 = ({
}
}
if (description) {
clientCollection.admin.description = description
}
if (
'livePreview' in clientCollection.admin &&

View File

@@ -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) {

View File

@@ -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<ClientBlock>({
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,
})
}

View File

@@ -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 || [],

View File

@@ -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<T extends JsonValue>(value: T): T {
export function deepCopyObjectSimple<T extends JsonValue>(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<T extends JsonValue>(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
}

View File

@@ -0,0 +1,3 @@
export function removeUndefined<T extends object>(obj: T): T {
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
}

View File

@@ -7,6 +7,8 @@ import {
} from 'payload/shared'
import React from 'react'
import { removeUndefined } from '../../utilities/removeUndefined.js'
export const getFromImportMap = <TOutput,>(args: {
importMap: ImportMap
PayloadComponent: PayloadComponent
@@ -66,7 +68,13 @@ export const RenderServerComponent: React.FC<{
if (typeof Component === 'function') {
const isRSC = isReactServerComponentOrFunction(Component)
return <Component {...clientProps} {...(isRSC ? serverProps : {})} />
// prevent $undefined from being passed through the rsc requests
const sanitizedProps = removeUndefined({
...clientProps,
...(isRSC ? serverProps : {}),
})
return <Component {...sanitizedProps} />
}
if (typeof Component === 'string' || isPlainObject(Component)) {
@@ -79,18 +87,17 @@ export const RenderServerComponent: React.FC<{
if (ResolvedComponent) {
const isRSC = isReactServerComponentOrFunction(ResolvedComponent)
return (
<ResolvedComponent
{...clientProps}
{...(isRSC ? serverProps : {})}
{...(isRSC && typeof Component === 'object' && Component?.serverProps
// 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
: {})}
/>
)
: {}),
...(typeof Component === 'object' && Component?.clientProps ? Component.clientProps : {}),
})
return <ResolvedComponent {...sanitizedProps} />
}
}

View File

@@ -0,0 +1,3 @@
export function removeUndefined<T extends object>(obj: T): T {
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
}