perf: reduce memory usage and improve speed during dev, by caching payload config & sanitization#9501
Draft
AlessioGr wants to merge 29 commits into
Draft
perf: reduce memory usage and improve speed during dev, by caching payload config & sanitization#9501AlessioGr wants to merge 29 commits into
AlessioGr wants to merge 29 commits into
Conversation
…emoving deep copying and building client config from the ground up instead
…ly runs once, and does not re-run when refreshing the page or navigating. HMR still works despite caching in dev
… comes back, we should run it in the background
…nd clientSchemaMap, introduce clientSchemaMap
…ayload and cache it. This now only happens once
…iting returned fields using select
…xists, if already-queried document is published
…t editor config is properly cached and only sanitized once for all lexical fields, instead of once per lexical field, as previously this was not cached until the sanitization function finished
GermanJablo
reviewed
Nov 25, 2024
Comment on lines
+10
to
+36
| export async function buildConfig( | ||
| _config: (() => Config) | Config, | ||
| ): Promise<(() => Promise<SanitizedConfig>) | SanitizedConfig> { | ||
| let configFn: (() => Config) | null = null | ||
|
|
||
| if (typeof _config !== 'function') { | ||
| console.warn( | ||
| 'For optimal performance, buildConfig should be called with a function that returns a config object. Otherwise, you will notice increased memory usage and decreased performance during development.', | ||
| ) | ||
| configFn = () => _config | ||
| // We could still return a function that returns the sanitized config here, | ||
| // so that it's cached properly and not loaded multiple times after every page transition. | ||
| // However, in order for this to be backwards compatible, we return the sanitized config directly, the old way. | ||
| // Otherwise, the imported config would suddenly be a function when imported, which may break standalone scripts | ||
| return await loadAndSanitizeConfig(configFn) | ||
| } else { | ||
| configFn = _config | ||
| } | ||
|
|
||
| if (process.env.NODE_ENV === 'production') { | ||
| return await loadAndSanitizeConfig(configFn) | ||
| } else { | ||
| return async () => { | ||
| return await loadAndSanitizeConfig(configFn) | ||
| } | ||
| } | ||
| } |
Contributor
There was a problem hiding this comment.
Can't the performance benefits be made automatic without changing the API by using a cache?
I'm thinking something like this:
export async function buildConfig(
_config: Config,
): Promise<SanitizedConfig> {
// Create a wrapper function that maintains a single instance in dev
let cachedConfig: SanitizedConfig | null = null
let cachedConfigFn: (() => Promise<SanitizedConfig>) | null = null
const getConfigFn = () => async (): Promise<SanitizedConfig> => {
// In development, cache the sanitized config
if (process.env.NODE_ENV === 'development') {
if (!cachedConfig) {
cachedConfig = await loadAndSanitizeConfig(() => _config)
}
return cachedConfig
}
// In production, just process normally since it's already optimized
return await loadAndSanitizeConfig(() => _config)
}
// In development, return a function that returns the cached result
if (process.env.NODE_ENV === 'development') {
if (!cachedConfigFn) {
cachedConfigFn = getConfigFn()
}
return await cachedConfigFn()
}
// In production, just process normally
return await loadAndSanitizeConfig(() => _config)
}
Member
Author
There was a problem hiding this comment.
If we cache it on the module scope then might work yea. But if the config is not passed through as a function returning the config, code in there will still execute despite the config being cached.
E.g. say your config does this:
then yes, the config would only be sanitized once - however, even if the config is cached, it would still execute the lexicalEditor and feature functions + spread defaultEditorFeatures every single time.
AlessioGr
added a commit
that referenced
this pull request
Jan 20, 2026
…15270) This PR fixes broken conditional tabs support in Next.js 16. In Next.js 16, the config is sanitized multiple times during development, with each run assigning a new UUID to `tab.id`. This causes a mismatch between the `tab.id` in form state and the `tab.id` in the client config - likely because the client config is cached, while [config sanitization is not](#9501). Using UUIDs in form state is also inconsistent with the rest of the codebase, where every other field uses its path as a key. This PR replaces the random UUID with the tab's schema path, ensuring a stable ID regardless of how many times config sanitization runs, and aligning tabs closer to how other fields are keyed in form state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Previously, during dev, the payload config would be loaded and sanitized between every single page transition twice. This lead to higher memory usage during dev. This PR makes sure it only happens once when payload is started.
This PR does not change the behavior in prod, as it was already cached nicely there. It will only affect dev
How to use
If you don't change anything, everything will still work and you will get no performance improvements. => Backwards-compatible and non-breaking.
To benefit from improved performance and lower memory usage, simply wrap your payload config argument in a function:
Todo
Get HMR to work