Skip to content

perf(cloudflare): reduce Worker startup module closure - #3199

Merged
ascorbic merged 6 commits into
mainfrom
codex/reduce-worker-startup
Sep 19, 2026
Merged

ascorbic merged 6 commits into
mainfrom
codex/reduce-worker-startup

Conversation

@ascorbic

Copy link
Copy Markdown
Member

What does this PR do?

Reduces the module graph evaluated when an EmDash Cloudflare Worker starts. The Worker entry exports PluginBridge directly, loads scheduled maintenance only when a Cron Trigger runs, and loads the bridge's repository runtime on the first sandbox RPC through a narrow emdash/plugins/host entry. Existing emdash and @emdash-cms/cloudflare/worker exports remain compatible.

Adds a production-build guard that walks static imports from the generated Worker entry. CI now rejects more than 30 eager modules, more than 2 MB of eager JavaScript, or eager WXR, Tiptap/ProseMirror, plugin-registry, and media-maintenance sources.

The query-count harness now disables Astro 7.3's separate dev and preview background modes so local agent runs capture the same query events as CI.

Discussion: #3143

Measured on the production-built Cloudflare starter with Wrangler 4.125.0, using one discarded warm-up and nine startup samples:

Metric Before After Change
Eager modules 144 18 -87.5%
Eager bytes 5,721,716 1,734,286 -69.7%
Startup active CPU median 44.0 ms 19.0 ms -56.8%
Startup CPU range 41.0–45.3 ms 18.7–20.1 ms

The narrow bridge runtime adds 928 KB on the first sandbox RPC, keeping startup plus first plugin use at approximately 2.66 MB instead of loading the 5.72 MB root barrel. Testing Cloudflare's new_module_registry flag increased the optimized median to 35.4 ms, so this PR does not enable it.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

The localization, new-feature, and screenshot items are not applicable: this changes Worker startup timing without adding admin UI or a new product capability.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

Not applicable — no UI changes.

Validated on the exact merged origin/main head with:

  • pnpm install --frozen-lockfile
  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm --dir packages/cloudflare test — 436 passed
  • focused core content-access tests — 102 passed
  • production workerd bridge tests — 31 passed
  • pnpm query-counts --target d1 — count and SQL snapshots match
  • pnpm --dir templates/starter-cloudflare build
  • pnpm startup:check — 18 modules, 1,734,286 bytes
  • wrangler check startup — 19.0 ms median across nine measured runs

The package API linter (publint) passes for both changed packages. The secondary attw --pack process crashes after packing with Cannot read properties of undefined (reading 'filename') for both packages and does not report a package diagnostic.

@changeset-bot

changeset-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1772c0a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@emdash-cms/cloudflare Patch
emdash Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@3199

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@3199

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@3199

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@3199

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@3199

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@3199

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@3199

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@3199

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@3199

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@3199

@emdash-cms/plugin-test

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-test@3199

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@3199

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@3199

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@3199

@emdash-cms/registry-loader

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-loader@3199

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@3199

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@3199

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@3199

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@3199

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@3199

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@3199

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@3199

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@3199

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@3199

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@3199

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@3199

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@3199

commit: 1772c0a

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://codex-reduce-worker-startup.try.emdashcms.com, https://codex-reduce-worker-startup-emdash-playground.emdash-cms.workers.dev (commit 1772c0a)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1cb19c7a.try.emdashcms.com, https://1cb19c7a-emdash-playground.emdash-cms.workers.dev 1772c0a 2026-09-18T12:40:27.087Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://ac01ec9f.try.emdashcms.com, https://ac01ec9f-emdash-playground.emdash-cms.workers.dev 45b69b0 2026-09-18T12:20:34.909Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1774d1f3.try.emdashcms.com, https://1774d1f3-emdash-playground.emdash-cms.workers.dev 23b229f 2026-09-18T11:40:00.721Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://2d8501a0.try.emdashcms.com, https://2d8501a0-emdash-playground.emdash-cms.workers.dev 8c6f07e 2026-09-18T10:52:32.272Z Visit the dashboard ↗

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot added size/L and removed size/XL labels Sep 18, 2026
@eisenbruch

Copy link
Copy Markdown
Contributor

Tested on our real Worker rather than the starter, since this is the site that motivated the thread. Short version: it works, and it removes the workaround we had been carrying. Details, with the caveat that our own entry was already lazy, so our headline numbers move less than yours and that is expected.

What we are

equineinfoexchange.com, launched yesterday: Astro 7 + EmDash 0.38.0 on Workers/D1/R2/KV, ~9,700 directory listings, ~3,400 HTML pages. We had already worked around the eager closure on our side, by making our build inject the scheduled handler with a dynamic import() rather than a static one. So we start from 21 eager modules, not 144.

The measurement that matters

Because our entry was already lazy, the honest test is what a static import costs, which is what an ordinary site does and what we had to stop doing:

our Worker, scheduled imported statically eager modules eager bytes
0.38.0 137 5,712,000
#3199 preview 22 1,500,000

Same entry, same build, only the packages differ. That is the whole of our workaround made unnecessary — one extra module and 0.02 MB over our hand-lazied build. We can delete the injection hack and go back to a plain static import.

With our existing lazy entry, before and after are unchanged, as expected: 21 modules / 1.48 MB → 21 / 1.50 MB, startup active CPU median 29.5 ms → 31.0 ms over nine runs each (min 26.6/27.6, max 34.3/52.4). No regression, no gain, because we were already at the floor. Wrangler 4.x, wrangler check startup, one warm-up discarded.

The PluginBridge change is the part I had concluded was unfixable from outside — a Durable Object class has to be a module-scope export, so no amount of deferring on our side could help. Good to be wrong about that.

Two things you may want to know

1. emdash/dist/astro/middleware.mjs goes from 161 KB to 43 KB, because it stops inlining the runtime. That is a nice second-order confirmation the split does what it says. It also moves mergedData / validateContentData / getCollectionWithFields out of that module into emdash-runtime-*.mjs. Only relevant to people patching built dist files, which is us: we carry local patches for #3129 and #3142 that target astro/middleware.mjs, and they will need retargeting when this lands. Not a blocker and not a request — the patches are ours and temporary — but worth knowing that the entry's contents change shape, not just its import graph.

2. @emdash-cms/plugin-forms is unaffected. It does not reference plugins/host or the sandbox, and it stays where it was in the eager closure (1 of 21 modules).

Caveats

Our four remaining local patches could not be applied to the preview build, because they target content-hashed dist filenames (api-BvxJmnFY.mjs and friends) that are rebuilt under new hashes. I checked each one and the upstream code they anchor to is still present under the new names, so this is a packaging artifact of preview builds rather than a conflict with this PR. The numbers above are therefore from an unpatched preview install.

Tested on our staging environment, not production.

The CI guard is the part I would have asked for if you had not included it. The regression we hit was invisible precisely because nothing failed — the build passed, the deploy passed, every page served, and the only symptom was cold pages taking 1–3 seconds. A threshold that fails the build is what stops that recurring.

Happy to re-run any of this, or to test again once it is on a release we can install normally.

@ascorbic

Copy link
Copy Markdown
Member Author

Thanks for checking. I'll merge this and it'll be in the next release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants