fix(templates): reduce with-cloudflare-d1 Worker bundle for free-tier 3 MiB limit#17397
Open
SybyAbraham wants to merge 2 commits into
Open
fix(templates): reduce with-cloudflare-d1 Worker bundle for free-tier 3 MiB limit#17397SybyAbraham wants to merge 2 commits into
SybyAbraham wants to merge 2 commits into
Conversation
…ages The with-cloudflare-d1 template on main references several APIs that have not been published to npm, making it impossible to build when installed standalone via create-payload-app: 1. payload build (build script) - not available in any published payload CLI version. Changed to next build. 2. Config.storage (payload.config.ts) - unreleased API not in any published @payloadcms/* package. Changed to plugins, which has always existed. (A codemod migrate-storage-adapters-to-config exists upstream for this transition.) 3. generatePayloadViewport (layout.tsx) - unreleased export not in any published @payloadcms/next version. Changed to metadata, which has always been exported. (A codemod migrate-next-generate-viewport-export exists upstream.) 4. Missing CSS/SCSS type declarations - the monorepo provides these via tools/assets.d.ts (wired through tsconfig.base.json), but standalone templates lack this file. Added src/assets.d.ts with the same declarations. 5. isCLI detection - switching from payload build to next build means payload/bin.js is no longer in process.argv, so isCLI was always false during build. This caused the config to use getCloudflareContext() (production path requiring CLOUDFLARE_API_TOKEN) instead of getCloudflareContextFromWrangler() (local wrangler path). Extended isCLI to also detect next/dist/bin/next. 6. realpath crash - the realpath() helper returned undefined when fs.existsSync() is false (the case for every process.argv entry in the Workers runtime), and the subsequent .endsWith() call on undefined threw. Wrapped in try/catch with a null guard. 7. remoteBindings during build - getCloudflareContextFromWrangler() was called with remoteBindings: isProduction, which during next build (NODE_ENV=production) tried to connect to real Cloudflare D1/R2 resources. Since this function is only called for CLI/build/dev (never production runtime), changed to remoteBindings: false. Also changed the D1 binding in wrangler.jsonc from remote: true to remote: false so the template builds out of the box without pre-configured Cloudflare resources. 8. engines.node - lowered from >=24.15.0 to >=22.0.0 for compatibility with Cloudflare Workers Builds (Node 22.16.0). Bumped @payloadcms/* dependencies from 3.82.1 to 3.86.0 (latest published). Verified with a clean next build: TypeScript compilation passes, all 7 routes generate successfully.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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.
Problem
The
with-cloudflare-d1template's Worker bundle is 3,364 KiB gzip (measured viawrangler deploy --dry-runtotal upload) — 292 KiB over the Cloudflare Workers free-tier 3 MiB (3,072 KiB) compressed size limit. This causes the template to fail to deploy on the Cloudflare Workers free tier —wrangler deployrejects the upload for exceeding the 3 MiB compressed-size limit.Root causes
1.
@vercel/og(~2.9 MiB uncompressed, ~744 KiB gzip delta)Payload's REST handler (
@payloadcms/next/dist/routes/rest/index.js) statically imports its OG image endpoint (./og/index.js), which importsnext/og.js(ImageResponse). That module re-exports fromnext/dist/server/og/image-response.js, which contains aNEXT_RUNTIMEternary that dynamically imports either@vercel/og/index.edge.js(edge) or@vercel/og/index.node.js(node).Next.js's NFT traces the node entry (
index.node.js) for API route bundles that include the REST handler. OpenNext'spatchVercelOgLibrary(source) detects@vercel/ogusage by scanning those.nft.jsontraces for@vercel/og/index.node.js. When found (useOg === true) it copies the edge variant (index.edge.js) into the output, patches the fallback font fetch to a.binmodule import (patchVercelOgFallbackFont), and rewrites the node imports to edge imports in the route files (patchVercelOgImport). The full set of@vercel/ogassets — both JS variants (~721 KiB each),resvg.wasm(1.4 MiB),yoga.wasm(88 KiB), and the fallback TTF (28 KiB), ~2.9 MiB uncompressed across six files — ends up in the.open-nextoutput through this copy step plus Next.js's own NFT standalone tracing, and Wrangler bundles the reachable subset into the Worker.When
useOg === false(no@vercel/ogtraced), a separate change (#1221, shipped in@opennextjs/cloudflare@1.19.4) aliases the edge entry —next/dist/compiled/@vercel/og/index.edge.js, which Next.js'sexternalImporthelper emits as a dynamic import even in apps that never usenext/og— to athrow.jsshim instead of leaving itexternal, so Wrangler no longer drags the library andresvg.wasminto the bundle.@vercel/ogis functional on Workers via thepatchVercelOgLibrarypatches above, but the ~744 KiB gzip cost is significant for a template targeting the free-tier 3 MiB limit.The import is unconditional: the OG image endpoint is registered regardless of
admin.meta.defaultOGImageType, so@vercel/ogenters the bundle even when the feature is configured'off'. With'off', the endpoint returns a 400 response before reaching@vercel/og— meaning the ~2.9 MiB of@vercel/ogcode ships in the bundle but is never executed.2.
drizzle-kit/api(7.3 MiB — safety net, not a measured contributor)drizzle-kit/apiis transitively imported by@payloadcms/drizzle/sqlite→requireDrizzleKit→require('drizzle-kit/api'). It is only used by the Payload CLI for migrations — never at Worker runtime.As of Payload 3.86.0,
withPayload()'s built-in externalization keepsdrizzle-kit/apiout of the bundle in normal webpack builds, so the baseline numbers measured here don't include it. The stub in this PR exists because that externalization has regressed before (#16470) and isn't guaranteed to survive OpenNext's esbuild repackaging — it's insurance, not the source of the savings. The proper fix is to lazy-loaddrizzle-kitinside the function body (#17009, still open).Solution
OG image endpoint stub
This template disables OG image generation (
admin.meta.defaultOGImageType: 'off') and replaces the endpoint module with a stub that mirrors that 400 response — eliminating ~744 KiB gzip from the bundle:stubs/payload-og-endpoint.js— exportsruntime,contentType, andgenerateOGImage(returnsResponse.json({ error: 'Open Graph images are disabled' }, { status: 400 })), matching the real module's API surface and'off'response exactlynext.config.ts— resolves the real endpoint path viacreateRequire(import.meta.url)and aliases it to the stub in both webpackresolve.aliasand turbopackresolveAliassrc/payload.config.ts— setsadmin.meta.defaultOGImageType: 'off'for runtime consistencyWith the stub in place, the import chain
routes/rest/index.js→./og/index.js→next/og.js→@vercel/ogis severed at the source. NFT no longer traces@vercel/og,patchVercelOgLibraryreportsuseOg === false, and no@vercel/ogfiles appear in the.open-nextoutput.To re-enable OG image generation, remove the alias from
next.config.tsand removedefaultOGImageTypefromsrc/payload.config.ts. The feature works on Workers via OpenNext's compatibility patches, but the bundle will exceed the free-tier 3 MiB limit — a Paid Workers plan is required.drizzle-kit/api stub
stubs/drizzle-kit-api.js— throws on call, aliased via both turbopack and webpackREADME documentation
The README documents the OG image trade-off, explains the stub, and provides instructions for re-enabling the feature (with the caveat about the free-tier size limit).
Measured results
All measurements on the template with identical dependencies (Next 15.4.11, OpenNext 1.20.1, Payload 3.86.0), using
wrangler deploy --dry-runtotal upload gzip — the metric Cloudflare actually enforces.@vercel/ogfiles in.open-next@vercel/ogThe "WASM/TTF stubbing only" column shows an intermediate approach (truncating
@vercel/ogasset files post-build) that was considered but not adopted: it zeroes the WASM and font files but leaves the ~721 KiB of@vercel/ogJS in the bundle. The endpoint stub removes the entire dependency chain.Runtime verification (live deploy to Cloudflare Workers, fresh D1 database with migrations applied):
GET /→ 200 (homepage renders)GET /api/og?title=Test→ 400 +{"error":"Open Graph images are disabled"}(byte-for-byte parity with real Payload'off'behavior)GET /admin→ 200 (admin panel loads)What this PR does not do
@vercel/oghandling. The NFT-trace detection and edge-copy/patch logic live inpatchVercelOgLibrary(longstanding adapter code, predating #1221); #1221 is the separateuseOg === falseshim that aliases the unused edge-entry dynamic import tothrow.js. This PR removes the import that causes the NFT trace, sopatchVercelOgLibraryreportsuseOg === falseand its copy/patch steps don't run — and Add icons & colors to fields for easier navigation #1221's shim then keeps the dangling edge dynamic import from pulling@vercel/ogback in.@payloadcms/next— the REST handler statically imports the OG endpoint regardless ofdefaultOGImageType. A proper fix would move the import behind a runtime check or offer an explicitwithPayload()option (e.g.{ excludeOGImageEndpoint: true }). That's a Payload core change affecting every deploy target, not just Cloudflare — tracked as a follow-up.Related
@vercel/og(aliases the edge-entry dynamic import tothrow.jswhenuseOg === false); the NFT-trace detection and edge-copy/patch logic live inpatchVercelOgLibrary