Skip to content

Commit fd0ff51

Browse files
authored
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.
1 parent 67a9d66 commit fd0ff51

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1509
-691
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ jobs:
296296
- admin__e2e__3
297297
- admin-root
298298
- auth
299+
- auth-basic
299300
- field-error-states
300301
- fields-relationship
301302
- fields

packages/next/src/layouts/Root/checkDependencies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const customReactVersionParser: CustomVersionParser = (version) => {
1717

1818
let checkedDependencies = false
1919

20-
export const checkDependencies = async () => {
20+
export const checkDependencies = () => {
2121
if (
2222
process.env.NODE_ENV !== 'production' &&
2323
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
@@ -26,7 +26,7 @@ export const checkDependencies = async () => {
2626
checkedDependencies = true
2727

2828
// First check if there are mismatching dependency versions of next / react packages
29-
await payloadCheckDependencies({
29+
void payloadCheckDependencies({
3030
dependencyGroups: [
3131
{
3232
name: 'react',

packages/next/src/layouts/Root/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import type { ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
33

44
import { rtlLanguages } from '@payloadcms/translations'
55
import { RootProvider } from '@payloadcms/ui'
6+
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
67
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
78
import { getPayload, parseCookies } from 'payload'
89
import React from 'react'
910

1011
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
11-
import { getClientConfig } from '../../utilities/getClientConfig.js'
1212
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
1313
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
1414
import { initReq } from '../../utilities/initReq.js'
@@ -33,7 +33,7 @@ export const RootLayout = async ({
3333
readonly importMap: ImportMap
3434
readonly serverFunction: ServerFunctionClient
3535
}) => {
36-
await checkDependencies()
36+
checkDependencies()
3737

3838
const config = await configPromise
3939

@@ -54,7 +54,7 @@ export const RootLayout = async ({
5454

5555
const payload = await getPayload({ config, importMap })
5656

57-
const { i18n, permissions, req, user } = await initReq(config)
57+
const { i18n, permissions, user } = await initReq(config)
5858

5959
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
6060
? 'RTL'
@@ -86,7 +86,7 @@ export const RootLayout = async ({
8686

8787
const navPrefs = await getNavPrefs({ payload, user })
8888

89-
const clientConfig = await getClientConfig({
89+
const clientConfig = getClientConfig({
9090
config,
9191
i18n,
9292
importMap,

packages/next/src/utilities/getClientConfig.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/next/src/views/Account/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const Account: React.FC<AdminViewProps> = async ({
102102
await getVersions({
103103
id: user.id,
104104
collectionConfig,
105+
doc: data,
105106
docPermissions,
106107
locale: locale?.code,
107108
payload,

packages/next/src/views/Document/getVersions.ts

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import { sanitizeID } from '@payloadcms/ui/shared'
1010

1111
type Args = {
1212
collectionConfig?: SanitizedCollectionConfig
13+
/**
14+
* Optional - performance optimization.
15+
* If a document has been fetched before fetching versions, pass it here.
16+
* If this document is set to published, we can skip the query to find out if a published document exists,
17+
* as the passed in document is proof of its existence.
18+
*/
19+
doc?: Record<string, any>
1320
docPermissions: SanitizedDocumentPermissions
1421
globalConfig?: SanitizedGlobalConfig
1522
id?: number | string
@@ -27,17 +34,19 @@ type Result = Promise<{
2734

2835
// TODO: in the future, we can parallelize some of these queries
2936
// this will speed up the API by ~30-100ms or so
37+
// Note from the future: I have attempted parallelizing these queries, but it made this function almost 2x slower.
3038
export const getVersions = async ({
3139
id: idArg,
3240
collectionConfig,
41+
doc,
3342
docPermissions,
3443
globalConfig,
3544
locale,
3645
payload,
3746
user,
3847
}: Args): Result => {
3948
const id = sanitizeID(idArg)
40-
let publishedQuery
49+
let publishedDoc
4150
let hasPublishedDoc = false
4251
let mostRecentVersionIsAutosaved = false
4352
let unpublishedVersionCount = 0
@@ -70,37 +79,49 @@ export const getVersions = async ({
7079
}
7180

7281
if (versionsConfig?.drafts) {
73-
publishedQuery = await payload.find({
74-
collection: collectionConfig.slug,
75-
depth: 0,
76-
locale: locale || undefined,
77-
user,
78-
where: {
79-
and: [
80-
{
81-
or: [
82+
// Find out if a published document exists
83+
if (doc?._status === 'published') {
84+
publishedDoc = doc
85+
} else {
86+
publishedDoc = (
87+
await payload.find({
88+
collection: collectionConfig.slug,
89+
depth: 0,
90+
limit: 1,
91+
locale: locale || undefined,
92+
pagination: false,
93+
select: {
94+
updatedAt: true,
95+
},
96+
user,
97+
where: {
98+
and: [
8299
{
83-
_status: {
84-
equals: 'published',
85-
},
100+
or: [
101+
{
102+
_status: {
103+
equals: 'published',
104+
},
105+
},
106+
{
107+
_status: {
108+
exists: false,
109+
},
110+
},
111+
],
86112
},
87113
{
88-
_status: {
89-
exists: false,
114+
id: {
115+
equals: id,
90116
},
91117
},
92118
],
93119
},
94-
{
95-
id: {
96-
equals: id,
97-
},
98-
},
99-
],
100-
},
101-
})
120+
})
121+
)?.docs?.[0]
122+
}
102123

103-
if (publishedQuery.docs?.[0]) {
124+
if (publishedDoc) {
104125
hasPublishedDoc = true
105126
}
106127

@@ -109,6 +130,9 @@ export const getVersions = async ({
109130
collection: collectionConfig.slug,
110131
depth: 0,
111132
limit: 1,
133+
select: {
134+
autosave: true,
135+
},
112136
user,
113137
where: {
114138
and: [
@@ -130,7 +154,7 @@ export const getVersions = async ({
130154
}
131155
}
132156

133-
if (publishedQuery.docs?.[0]?.updatedAt) {
157+
if (publishedDoc?.updatedAt) {
134158
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
135159
collection: collectionConfig.slug,
136160
user,
@@ -148,7 +172,7 @@ export const getVersions = async ({
148172
},
149173
{
150174
updatedAt: {
151-
greater_than: publishedQuery.docs[0].updatedAt,
175+
greater_than: publishedDoc.updatedAt,
152176
},
153177
},
154178
],
@@ -159,6 +183,7 @@ export const getVersions = async ({
159183

160184
;({ totalDocs: versionCount } = await payload.countVersions({
161185
collection: collectionConfig.slug,
186+
depth: 0,
162187
user,
163188
where: {
164189
and: [
@@ -173,15 +198,23 @@ export const getVersions = async ({
173198
}
174199

175200
if (globalConfig) {
201+
// Find out if a published document exists
176202
if (versionsConfig?.drafts) {
177-
publishedQuery = await payload.findGlobal({
178-
slug: globalConfig.slug,
179-
depth: 0,
180-
locale,
181-
user,
182-
})
183-
184-
if (publishedQuery?._status === 'published') {
203+
if (doc?._status === 'published') {
204+
publishedDoc = doc
205+
} else {
206+
publishedDoc = await payload.findGlobal({
207+
slug: globalConfig.slug,
208+
depth: 0,
209+
locale,
210+
select: {
211+
updatedAt: true,
212+
},
213+
user,
214+
})
215+
}
216+
217+
if (publishedDoc?._status === 'published') {
185218
hasPublishedDoc = true
186219
}
187220

@@ -204,7 +237,7 @@ export const getVersions = async ({
204237
}
205238
}
206239

207-
if (publishedQuery?.updatedAt) {
240+
if (publishedDoc?.updatedAt) {
208241
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
209242
depth: 0,
210243
global: globalConfig.slug,
@@ -218,7 +251,7 @@ export const getVersions = async ({
218251
},
219252
{
220253
updatedAt: {
221-
greater_than: publishedQuery.updatedAt,
254+
greater_than: publishedDoc.updatedAt,
222255
},
223256
},
224257
],

packages/next/src/views/Document/handleServerFunction.tsx

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,11 @@
1-
import type { I18nClient } from '@payloadcms/translations'
2-
import type {
3-
ClientConfig,
4-
Data,
5-
DocumentPreferences,
6-
FormState,
7-
ImportMap,
8-
PayloadRequest,
9-
SanitizedConfig,
10-
VisibleEntities,
11-
} from 'payload'
1+
import type { Data, DocumentPreferences, FormState, PayloadRequest, VisibleEntities } from 'payload'
122

3+
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
134
import { headers as getHeaders } from 'next/headers.js'
14-
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
5+
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
156

167
import { renderDocument } from './index.js'
178

18-
let cachedClientConfig = global._payload_clientConfig
19-
20-
if (!cachedClientConfig) {
21-
cachedClientConfig = global._payload_clientConfig = null
22-
}
23-
24-
export const getClientConfig = (args: {
25-
config: SanitizedConfig
26-
i18n: I18nClient
27-
importMap: ImportMap
28-
}): ClientConfig => {
29-
const { config, i18n, importMap } = args
30-
31-
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
32-
return cachedClientConfig
33-
}
34-
35-
cachedClientConfig = createClientConfig({
36-
config,
37-
i18n,
38-
importMap,
39-
})
40-
41-
return cachedClientConfig
42-
}
43-
449
type RenderDocumentResult = {
4510
data: any
4611
Document: React.ReactNode

packages/next/src/views/Document/index.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import type {
2-
AdminViewProps,
3-
Data,
4-
PayloadComponent,
5-
ServerProps,
6-
ServerSideEditViewProps,
7-
} from 'payload'
1+
import type { AdminViewProps, Data, PayloadComponent, ServerSideEditViewProps } from 'payload'
82

93
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
104
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
@@ -137,6 +131,7 @@ export const renderDocument = async ({
137131
getVersions({
138132
id: idFromArgs,
139133
collectionConfig,
134+
doc,
140135
docPermissions,
141136
globalConfig,
142137
locale: locale?.code,

0 commit comments

Comments
 (0)