Skip to content

Commit 2d7626c

Browse files
authored
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.
1 parent e75527b commit 2d7626c

File tree

8 files changed

+61
-32
lines changed

8 files changed

+61
-32
lines changed

packages/payload/src/collections/config/client.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
2020

2121
export type ServerOnlyCollectionAdminProperties = keyof Pick<
2222
SanitizedCollectionConfig['admin'],
23-
'baseListFilter' | 'hidden'
23+
'baseListFilter' | 'components' | 'hidden'
2424
>
2525

2626
export type ServerOnlyUploadProperties = keyof Pick<
@@ -34,7 +34,6 @@ export type ServerOnlyUploadProperties = keyof Pick<
3434

3535
export type ClientCollectionConfig = {
3636
admin: {
37-
components: null
3837
description?: StaticDescription
3938
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
4039
preview?: boolean
@@ -76,6 +75,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
7675
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
7776
'hidden',
7877
'baseListFilter',
78+
'components',
7979
// 'preview' is handled separately
8080
// `livePreview` is handled separately
8181
]
@@ -89,7 +89,10 @@ export const createClientCollectionConfig = ({
8989
defaultIDType: Payload['config']['db']['defaultIDType']
9090
i18n: I18nClient
9191
}): ClientCollectionConfig => {
92-
const clientCollection = deepCopyObjectSimple(collection) as unknown as ClientCollectionConfig
92+
const clientCollection = deepCopyObjectSimple(
93+
collection,
94+
true,
95+
) as unknown as ClientCollectionConfig
9396

9497
clientCollection.fields = createClientFields({
9598
clientFields: clientCollection?.fields || [],
@@ -150,8 +153,6 @@ export const createClientCollectionConfig = ({
150153
clientCollection.admin.preview = true
151154
}
152155

153-
clientCollection.admin.components = null
154-
155156
let description = undefined
156157

157158
if (collection.admin?.description) {
@@ -165,7 +166,9 @@ export const createClientCollectionConfig = ({
165166
}
166167
}
167168

168-
clientCollection.admin.description = description
169+
if (description) {
170+
clientCollection.admin.description = description
171+
}
169172

170173
if (
171174
'livePreview' in clientCollection.admin &&

packages/payload/src/config/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const createClientConfig = ({
7979
i18n: I18nClient
8080
}): ClientConfig => {
8181
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
82-
const clientConfig = deepCopyObjectSimple(config) as unknown as ClientConfig
82+
const clientConfig = deepCopyObjectSimple(config, true) as unknown as ClientConfig
8383

8484
for (const key of serverOnlyConfigProperties) {
8585
if (key in clientConfig) {

packages/payload/src/fields/config/client.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { Payload } from '../../types/index.js'
1818
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
1919
import { fieldAffectsData } from '../../fields/config/types.js'
2020
import { flattenTopLevelFields } from '../../index.js'
21+
import { removeUndefined } from '../../utilities/removeUndefined.js'
2122

2223
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
2324
// to accidentally use it instead of ClientField and get confused
@@ -99,8 +100,9 @@ export const createClientField = ({
99100
clientField.admin.style = {
100101
...clientField.admin.style,
101102
'--field-width': clientField.admin.width,
102-
width: undefined, // avoid needlessly adding this to the element's style attribute
103103
}
104+
105+
delete clientField.admin.style.width // avoid needlessly adding this to the element's style attribute
104106
} else {
105107
if (!(clientField.admin.style instanceof Object)) {
106108
clientField.admin.style = {}
@@ -137,19 +139,24 @@ export const createClientField = ({
137139
if (incomingField.blocks?.length) {
138140
for (let i = 0; i < incomingField.blocks.length; i++) {
139141
const block = incomingField.blocks[i]
140-
const clientBlock: ClientBlock = {
142+
143+
// prevent $undefined from being passed through the rsc requests
144+
const clientBlock = removeUndefined<ClientBlock>({
141145
slug: block.slug,
142-
admin: {
143-
components: {},
144-
custom: block.admin?.custom,
145-
},
146146
fields: field.blocks?.[i]?.fields || [],
147147
imageAltText: block.imageAltText,
148148
imageURL: block.imageURL,
149+
}) satisfies ClientBlock
150+
151+
if (block.admin?.custom) {
152+
clientBlock.admin = {
153+
custom: block.admin.custom,
154+
}
149155
}
150156

151157
if (block.labels) {
152158
clientBlock.labels = {} as unknown as LabelsClient
159+
153160
if (block.labels.singular) {
154161
if (typeof block.labels.singular === 'function') {
155162
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
@@ -184,6 +191,7 @@ export const createClientField = ({
184191

185192
case 'radio':
186193

194+
// eslint-disable-next-line no-fallthrough
187195
case 'select': {
188196
const field = clientField as RadioFieldClient | SelectFieldClient
189197

@@ -206,7 +214,6 @@ export const createClientField = ({
206214

207215
break
208216
}
209-
210217
case 'richText': {
211218
if (!incomingField?.editor) {
212219
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 = ({
218225

219226
break
220227
}
228+
221229
case 'tabs': {
222230
const field = clientField as unknown as TabsFieldClient
223231

@@ -325,7 +333,6 @@ export const createClientFields = ({
325333
},
326334
hidden: true,
327335
label: 'ID',
328-
localized: undefined,
329336
})
330337
}
331338

packages/payload/src/globals/config/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const createClientGlobalConfig = ({
4949
global: SanitizedConfig['globals'][0]
5050
i18n: I18nClient
5151
}): ClientGlobalConfig => {
52-
const clientGlobal = deepCopyObjectSimple(global) as unknown as ClientGlobalConfig
52+
const clientGlobal = deepCopyObjectSimple(global, true) as unknown as ClientGlobalConfig
5353

5454
clientGlobal.fields = createClientFields({
5555
clientFields: clientGlobal?.fields || [],

packages/payload/src/utilities/deepCopyObject.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@ Benchmark: https://github.com/AlessioGr/fastest-deep-clone-json/blob/main/test/b
9898
* `Set`, `Buffer`, ... are not allowed.
9999
* @returns The cloned JSON value.
100100
*/
101-
export function deepCopyObjectSimple<T extends JsonValue>(value: T): T {
101+
export function deepCopyObjectSimple<T extends JsonValue>(value: T, filterUndefined = false): T {
102102
if (typeof value !== 'object' || value === null) {
103103
return value
104104
} else if (Array.isArray(value)) {
105105
return value.map((e) =>
106-
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e),
106+
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e, filterUndefined),
107107
) as T
108108
} else {
109109
if (value instanceof Date) {
@@ -112,7 +112,13 @@ export function deepCopyObjectSimple<T extends JsonValue>(value: T): T {
112112
const ret: { [key: string]: T } = {}
113113
for (const k in value) {
114114
const v = value[k]
115-
ret[k] = typeof v !== 'object' || v === null ? v : (deepCopyObjectSimple(v as T) as any)
115+
if (filterUndefined && v === undefined) {
116+
continue
117+
}
118+
ret[k] =
119+
typeof v !== 'object' || v === null
120+
? v
121+
: (deepCopyObjectSimple(v as T, filterUndefined) as any)
116122
}
117123
return ret as unknown as T
118124
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function removeUndefined<T extends object>(obj: T): T {
2+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
3+
}

packages/ui/src/elements/RenderServerComponent/index.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
} from 'payload/shared'
88
import React from 'react'
99

10+
import { removeUndefined } from '../../utilities/removeUndefined.js'
11+
1012
export const getFromImportMap = <TOutput,>(args: {
1113
importMap: ImportMap
1214
PayloadComponent: PayloadComponent
@@ -66,7 +68,13 @@ export const RenderServerComponent: React.FC<{
6668
if (typeof Component === 'function') {
6769
const isRSC = isReactServerComponentOrFunction(Component)
6870

69-
return <Component {...clientProps} {...(isRSC ? serverProps : {})} />
71+
// prevent $undefined from being passed through the rsc requests
72+
const sanitizedProps = removeUndefined({
73+
...clientProps,
74+
...(isRSC ? serverProps : {}),
75+
})
76+
77+
return <Component {...sanitizedProps} />
7078
}
7179

7280
if (typeof Component === 'string' || isPlainObject(Component)) {
@@ -79,18 +87,17 @@ export const RenderServerComponent: React.FC<{
7987
if (ResolvedComponent) {
8088
const isRSC = isReactServerComponentOrFunction(ResolvedComponent)
8189

82-
return (
83-
<ResolvedComponent
84-
{...clientProps}
85-
{...(isRSC ? serverProps : {})}
86-
{...(isRSC && typeof Component === 'object' && Component?.serverProps
87-
? Component.serverProps
88-
: {})}
89-
{...(typeof Component === 'object' && Component?.clientProps
90-
? Component.clientProps
91-
: {})}
92-
/>
93-
)
90+
// prevent $undefined from being passed through rsc requests
91+
const sanitizedProps = removeUndefined({
92+
...clientProps,
93+
...(isRSC ? serverProps : {}),
94+
...(isRSC && typeof Component === 'object' && Component?.serverProps
95+
? Component.serverProps
96+
: {}),
97+
...(typeof Component === 'object' && Component?.clientProps ? Component.clientProps : {}),
98+
})
99+
100+
return <ResolvedComponent {...sanitizedProps} />
94101
}
95102
}
96103

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function removeUndefined<T extends object>(obj: T): T {
2+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
3+
}

0 commit comments

Comments
 (0)