perf: up to 3x initial load time improvements across plugins and s3 storage#16518
Open
perf: up to 3x initial load time improvements across plugins and s3 storage#16518
Conversation
Contributor
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
AlessioGr
reviewed
May 7, 2026
|
|
||
| // The AWS SDK is ~5MB. Skip loading it entirely when the plugin is disabled | ||
| // so users with `enabled: false` (e.g. dev/test toggles) don't pay the cost. | ||
| let S3Constructor: typeof import('@aws-sdk/client-s3').S3 | undefined |
Member
There was a problem hiding this comment.
type-only imports should be stripped during tsc, so for simplicity I think we can leave this
AlessioGr
reviewed
May 7, 2026
|
|
||
| // Lazy-load aws-sdk and presigner only when actually generating a signed URL — | ||
| // keeps the ~5MB SDK out of the cold-boot module graph for users who don't trigger this route. | ||
| const [{ PutObjectCommand }, { getSignedUrl }] = await Promise.all([ |
Member
There was a problem hiding this comment.
- Can you test if this even has any effect after Next.js builds and bundles the app? Would be pointless to do if this improvement is during development only
AlessioGr
reviewed
May 7, 2026
| } | ||
| const getCollectionConfig = (slug: string | undefined) => | ||
| slug ? collectionConfigBySlug.get(slug) : undefined | ||
| const getGlobalConfig = (slug: string | undefined) => |
Member
There was a problem hiding this comment.
Unnecessary code / function call overhead, Map.get returns undefined if not found already.
AlessioGr
reviewed
May 7, 2026
AlessioGr
reviewed
May 7, 2026
| Parameters<GenerateTitle>[0], | ||
| 'collectionConfig' | 'globalConfig' | 'req' | ||
| > = await req.json?.() | ||
| endpoints: (() => { |
Member
There was a problem hiding this comment.
if it's just gonna be creating the 2 maps, can we move that in the plugin function body instead of creating and calling this inline function?
…zedConfig Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
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.
Summary
packages/payload/src/config/sanitize.tsSetthat was never read)packages/plugin-seo/src/index.tspackages/plugin-ecommerce/src/index.tspackages/plugin-import-export/src/index.tsSet.has+ single-pass label/hook attachmentpackages/plugin-search/src/index.ts@aws-sdk/client-s3(only when needed)packages/storage-s3/src/{index,adapter,generateSignedURL}.tsDetails
1.
sanitize.ts— remove dead upload-adapter dedup looppackages/payload/src/config/sanitize.tshad two consecutive blocks computing upload-adapter dedup. The first built a localSet<string>and never read from it; the second rebuilt the same data inline. Removed the unused block.2.
plugin-seo— Map-indexed endpoint lookupsThe four
/plugin-seo/generate-*endpoint handlers each performed twoArray.find(c => c.slug === ...)calls per request — once for the collection, once for the global. Each call is O(N) overconfig.collections/config.globals.Built a
Map<slug, config>once per plugin init and replaced the four.find()pairs withMap.get(slug)lookups. Per-request cost goes from O(N + G) to O(1).3.
plugin-ecommerce— collapse double translation mergeThe plugin previously did
deepMergeSimple(translations, incomingConfig.i18n.translations)followed by anObject.entries(translations).forEach(...)loop that re-set theplugin-ecommercenamespace per-locale. Two full passes over the language table.Collapsed into one pass that walks
supportedLanguagesand writes theplugin-ecommercenamespace directly. Same observable behavior (plugin'splugin-ecommercestrings still win over user-provided ones).4.
plugin-import-export— filter-first translation mergeWas building an intermediate
simplifiedTranslationsviaObject.entries(translations).reduce(...)over all 31 language entries before merging. Now iterates onlysupportedLanguagesand skips the unused tables.5.
plugin-search— Set.has + single-pass label collectionTwo improvements:
enabledCollections.indexOf(collection.slug) > -1(O(M) per collection) →enabledSlugSet.has(collection.slug)(O(1)).for (const collection of collections)pass collects labels for the search-enabled subset; was a separatefilter().map().Object.fromEntries(...)pass.6.
storage-s3— lazy-load@aws-sdk/client-s3The package's entry file did
import * as AWS from '@aws-sdk/client-s3'at module top, plus the helper files (generateSignedURL.ts,getFile.ts,uploadFile.ts) eagerly imported their AWS pieces. As a result, the SDK (~5–7 MB unpacked) was loaded into the module graph at process boot regardless of whetherenabled: falseor whether any upload route was ever hit.After this PR:
@aws-sdk/*imports inindex.tsandgenerateSignedURL.tsareimport typeonly.index.tslazy-imports theS3constructor inside the plugin function only whenenabled !== false.adapter.tsdynamic-imports thedeleteFile/uploadFile/getFilehelpers (which still eagerly use AWS internally) on first invocation ofhandleDelete/handleUpload/staticHandler.generateSignedURL.ts's handler dynamic-importsPutObjectCommandandgetSignedUrlon first request.Net effect: AWS SDK never loads at boot. For users who disable the plugin or never hit upload paths, it doesn't load at all. The
s3Storageplugin function is nowasync, which is supported by thePlugintype.Tests
Added two regression tests pinning the new translation-scoping behavior:
test/plugin-ecommerce/int.spec.ts— assertsplugin-ecommercekeys present only insupportedLanguagesbuckets.test/plugin-import-export/int.spec.ts— same shape.The full int suite for
plugin-seo,plugin-import-export,plugin-ecommerce,plugin-redirects,plugin-search,storage-s3,_community, andaccess-controlpasses (312 tests). All packages typecheck clean.Benchmark results
Methodology: bench scripts in
test/_perf-benchmarks/(untracked). Each bench was run onmain(before) and on this branch (after). 50–200 iterations + warmup.process.hrtime.bigint()-based timing.plugin translation merges (mean ms per plugin call)
plugin-search end-to-end (mean ms per plugin call)
plugin-seo endpoint lookup (K=1000 requests, mean ms)
storage-s3 cold-boot (mean over 8 fresh child processes)
enabled: falseimport timeenabled: falseimport heap deltaenabled: falseaws-sdk loaded after importenabled: trueimport timeenabled: trueaws-sdk loaded after importenabled: trueplugin call (first invocation)For enabled S3 the AWS load cost is shifted from boot to the first invocation that needs it (one-time per process). Net positive for serverless cold-start; identical steady-state.
sanitize dead-loop removal
Within measurement noise across N=50/100/200 (1.01–1.04×). Kept because the loop was confirmed dead and the change is a small simplification.