Skip to content

perf: reduce memory usage and improve speed during dev, by caching payload config & sanitization #9501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
79e221d
perf: speed up createClientConfig (4.1s => 50ms with 400 fields) by r…
AlessioGr Nov 22, 2024
c5789d6
perf: properly cache createClientConfig in both prod & dev. It now on…
AlessioGr Nov 22, 2024
c768fb0
perf: do not await dependency checker. Doesn't matter when the result…
AlessioGr Nov 22, 2024
c40ff01
remove unnecessary awaits
AlessioGr Nov 22, 2024
9c79c51
fix: skip blocks and tabs from just being copied over
AlessioGr Nov 22, 2024
8147299
perf: remove createClientField call in renderField, cache schemaMap a…
AlessioGr Nov 23, 2024
7aa3453
perf: lexical: handle clientField and clientSchemaMap generation in p…
AlessioGr Nov 23, 2024
44e1bd4
turn down shameful performance warning
AlessioGr Nov 23, 2024
952fd26
fix incorrect client tab handling
AlessioGr Nov 23, 2024
4c6e9a1
fix auth property never added
AlessioGr Nov 23, 2024
c588c7e
perf(next): speed up version fetching by disabling pagination and lim…
AlessioGr Nov 23, 2024
7f88161
do the same for globals
AlessioGr Nov 23, 2024
81763b6
perf(next): completely skip query to find out if published document e…
AlessioGr Nov 23, 2024
8cba6c5
simplify getVersions
AlessioGr Nov 23, 2024
dac7f0b
add comment
AlessioGr Nov 23, 2024
815c5b5
fix: fallback `version` if not selected
r1tsuu Nov 23, 2024
a75e426
Merge remote-tracking branch 'origin/main' into perf/clientconfig
AlessioGr Nov 24, 2024
616de3c
perf(richtext-lexical): remove deep copying in adapter, ensure defaul…
AlessioGr Nov 25, 2024
8409de7
remove more deep copying in lexical
AlessioGr Nov 25, 2024
e264631
add new e2e test suite that tests new user registration
AlessioGr Nov 25, 2024
5f8f9f7
fix: hidden or disabled fields were included in clientConfig
AlessioGr Nov 25, 2024
d4a6735
remove console.log
AlessioGr Nov 25, 2024
6187427
small optimization
AlessioGr Nov 25, 2024
f0fecc4
chore: add missing void to checkPayloadDependencies
AlessioGr Nov 25, 2024
9d0ef67
perf: cache payload config during dev, ensure sanitization only runs …
AlessioGr Nov 25, 2024
b534fb4
fix initPayloadInt config loading
AlessioGr Nov 25, 2024
9da0158
Merge remote-tracking branch 'origin/main' into perf/clientconfig
AlessioGr Nov 25, 2024
c372c04
Merge remote-tracking branch 'origin/perf/clientconfig' into perf/cac…
AlessioGr Nov 25, 2024
3545514
fix config loading
AlessioGr Nov 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ jobs:
- admin__e2e__3
- admin-root
- auth
- auth-basic
- field-error-states
- fields-relationship
- fields
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/layouts/Root/checkDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const customReactVersionParser: CustomVersionParser = (version) => {

let checkedDependencies = false

export const checkDependencies = async () => {
export const checkDependencies = () => {
if (
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
Expand All @@ -26,7 +26,7 @@ export const checkDependencies = async () => {
checkedDependencies = true

// First check if there are mismatching dependency versions of next / react packages
await payloadCheckDependencies({
void payloadCheckDependencies({
dependencyGroups: [
{
name: 'react',
Expand Down
18 changes: 9 additions & 9 deletions packages/next/src/layouts/Root/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
import type { ConfigImport, ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'

import { rtlLanguages } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { getPayload, parseCookies } from 'payload'
import { getConfig, getPayload, parseCookies } from 'payload'
import React from 'react'

import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
import { getClientConfig } from '../../utilities/getClientConfig.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
import { initReq } from '../../utilities/initReq.js'
Expand All @@ -24,18 +24,18 @@ export const metadata = {

export const RootLayout = async ({
children,
config: configPromise,
config: configImport,
importMap,
serverFunction,
}: {
readonly children: React.ReactNode
readonly config: Promise<SanitizedConfig>
readonly config: ConfigImport
readonly importMap: ImportMap
readonly serverFunction: ServerFunctionClient
}) => {
await checkDependencies()
void checkDependencies()

const config = await configPromise
const config = await getConfig(configImport)

const headers = await getHeaders()
const cookies = parseCookies(headers)
Expand All @@ -54,7 +54,7 @@ export const RootLayout = async ({

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

const { i18n, permissions, req, user } = await initReq(config)
const { i18n, permissions, user } = await initReq(config)

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

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

const clientConfig = await getClientConfig({
const clientConfig = getClientConfig({
config,
i18n,
importMap,
Expand Down
23 changes: 0 additions & 23 deletions packages/next/src/utilities/getClientConfig.ts

This file was deleted.

11 changes: 5 additions & 6 deletions packages/next/src/utilities/initReq.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions, User } from 'payload'
import type { ConfigImport, PayloadRequest, SanitizedPermissions, User } from 'payload'

import { initI18n } from '@payloadcms/translations'
import { headers as getHeaders } from 'next/headers.js'
import { createLocalReq, getPayload, parseCookies } from 'payload'
import { createLocalReq, getConfig, getPayload, parseCookies } from 'payload'
import { cache } from 'react'

import { getRequestLanguage } from './getRequestLanguage.js'
Expand All @@ -15,10 +15,9 @@ type Result = {
user: User
}

export const initReq = cache(async function (
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
): Promise<Result> {
const config = await configPromise
export const initReq = cache(async function (configImport: ConfigImport): Promise<Result> {
const config = await getConfig(configImport)

const payload = await getPayload({ config })

const headers = await getHeaders()
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/views/Account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const Account: React.FC<AdminViewProps> = async ({
await getVersions({
id: user.id,
collectionConfig,
doc: data,
docPermissions,
locale: locale?.code,
payload,
Expand Down
105 changes: 69 additions & 36 deletions packages/next/src/views/Document/getVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { sanitizeID } from '@payloadcms/ui/shared'

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

// TODO: in the future, we can parallelize some of these queries
// this will speed up the API by ~30-100ms or so
// Note from the future: I have attempted parallelizing these queries, but it made this function almost 2x slower.
export const getVersions = async ({
id: idArg,
collectionConfig,
doc,
docPermissions,
globalConfig,
locale,
payload,
user,
}: Args): Result => {
const id = sanitizeID(idArg)
let publishedQuery
let publishedDoc
let hasPublishedDoc = false
let mostRecentVersionIsAutosaved = false
let unpublishedVersionCount = 0
Expand Down Expand Up @@ -70,37 +79,49 @@ export const getVersions = async ({
}

if (versionsConfig?.drafts) {
publishedQuery = await payload.find({
collection: collectionConfig.slug,
depth: 0,
locale: locale || undefined,
user,
where: {
and: [
{
or: [
// Find out if a published document exists
if (doc?._status === 'published') {
publishedDoc = doc
} else {
publishedDoc = (
await payload.find({
collection: collectionConfig.slug,
depth: 0,
limit: 1,
locale: locale || undefined,
pagination: false,
select: {
updatedAt: true,
},
user,
where: {
and: [
{
_status: {
equals: 'published',
},
or: [
{
_status: {
equals: 'published',
},
},
{
_status: {
exists: false,
},
},
],
},
{
_status: {
exists: false,
id: {
equals: id,
},
},
],
},
{
id: {
equals: id,
},
},
],
},
})
})
)?.docs?.[0]
}

if (publishedQuery.docs?.[0]) {
if (publishedDoc) {
hasPublishedDoc = true
}

Expand All @@ -109,6 +130,9 @@ export const getVersions = async ({
collection: collectionConfig.slug,
depth: 0,
limit: 1,
select: {
autosave: true,
},
user,
where: {
and: [
Expand All @@ -130,7 +154,7 @@ export const getVersions = async ({
}
}

if (publishedQuery.docs?.[0]?.updatedAt) {
if (publishedDoc?.updatedAt) {
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
collection: collectionConfig.slug,
user,
Expand All @@ -148,7 +172,7 @@ export const getVersions = async ({
},
{
updatedAt: {
greater_than: publishedQuery.docs[0].updatedAt,
greater_than: publishedDoc.updatedAt,
},
},
],
Expand All @@ -159,6 +183,7 @@ export const getVersions = async ({

;({ totalDocs: versionCount } = await payload.countVersions({
collection: collectionConfig.slug,
depth: 0,
user,
where: {
and: [
Expand All @@ -173,15 +198,23 @@ export const getVersions = async ({
}

if (globalConfig) {
// Find out if a published document exists
if (versionsConfig?.drafts) {
publishedQuery = await payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
locale,
user,
})

if (publishedQuery?._status === 'published') {
if (doc?._status === 'published') {
publishedDoc = doc
} else {
publishedDoc = await payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
locale,
select: {
updatedAt: true,
},
user,
})
}

if (publishedDoc?._status === 'published') {
hasPublishedDoc = true
}

Expand All @@ -204,7 +237,7 @@ export const getVersions = async ({
}
}

if (publishedQuery?.updatedAt) {
if (publishedDoc?.updatedAt) {
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
depth: 0,
global: globalConfig.slug,
Expand All @@ -218,7 +251,7 @@ export const getVersions = async ({
},
{
updatedAt: {
greater_than: publishedQuery.updatedAt,
greater_than: publishedDoc.updatedAt,
},
},
],
Expand Down
41 changes: 3 additions & 38 deletions packages/next/src/views/Document/handleServerFunction.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,11 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
ClientConfig,
Data,
DocumentPreferences,
FormState,
ImportMap,
PayloadRequest,
SanitizedConfig,
VisibleEntities,
} from 'payload'
import type { Data, DocumentPreferences, FormState, PayloadRequest, VisibleEntities } from 'payload'

import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'

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

let cachedClientConfig = global._payload_clientConfig

if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}

export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
const { config, i18n, importMap } = args

if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
}

cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})

return cachedClientConfig
}

type RenderDocumentResult = {
data: any
Document: React.ReactNode
Expand Down
Loading
Loading