Skip to content

perf(core): lazily load sandboxed plugins#1379

Draft
scottbuscemi wants to merge 2 commits into
mainfrom
perf/lazy-sandboxed-plugins
Draft

perf(core): lazily load sandboxed plugins#1379
scottbuscemi wants to merge 2 commits into
mainfrom
perf/lazy-sandboxed-plugins

Conversation

@scottbuscemi

Copy link
Copy Markdown
Collaborator

What does this PR do?

Lazily loads sandboxed plugins so they no longer block the first request of a cold isolate.

Previously, every build-time sandboxed plugin was provisioned on the Worker Loader during runtime init, so the first request of each cold isolate — including anonymous reads — waited on plugins it never exercised. A write-only plugin (declaring only content:afterSave/afterDelete, for example) could add over a second to content reads that never trigger it.

Plugins are now registered (cheap, synchronous) at init and loaded on first use of an extension point they actually declare:

  • Build-time: the integration reads each plugin's dist/manifest.json and emits its declared hooks/routes onto the entry (SandboxedPluginEntry gains hooks?/routes?). Plugins without a manifest are treated as "unknown" and loaded eagerly to stay correct.
  • create() registers build-time entries instead of loading them, and seeds route-auth metadata from declared routes so a route can be authorized without provisioning the isolate.
  • ensureSandboxedPluginLoaded() loads a plugin on first use, memoizes per isolate, dedupes concurrent first-loads, and re-checks enabled state after load.
  • Hook/route dispatch iterates only candidates whose declared metadata includes the event and loads only those, preserving config order. A public render loads no plugin that doesn't declare page:metadata.

Marketplace/registry plugins remain eagerly loaded for now (deferring them needs an R2 manifest-only fetch — follow-up). No public API or configuration change.

Closes #

Type of change

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

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change) — sandbox-lazy-load + virtual-modules-sandbox suites (13 passed)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). n/a — no admin UI strings.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion. n/a — performance improvement, no API change.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (opencode)

Screenshots / test output

sandbox-lazy-load.test.ts / virtual-modules-sandbox.test.ts — 13 passed
pnpm lint — 0 diagnostics; pnpm typecheck — clean

Eagerly loading every build-time sandboxed plugin during runtime init
blocked the first request of each cold isolate — including reads — on the
Worker Loader. A write-only plugin (e.g. webhook-notifier, which only
declares content:afterSave/afterDelete) added ~1.4s to anonymous content
reads that never exercise it.

Defer provisioning until a plugin's extension point is actually used:

- Build-time: generateSandboxedPluginsModule now reads each plugin's
  built dist/manifest.json and emits its declared hooks/routes onto the
  entry (undefined when no manifest → treated as "unknown" and loaded to
  stay correct). SandboxedPluginEntry gains hooks?/routes?.
- create() registers build-time entries (cheap, sync) instead of
  loading them, and seeds route-auth metadata from declared routes so a
  route can be authorized without provisioning the isolate.
- New ensureSandboxedPluginLoaded() loads a plugin on first use,
  memoizes it per isolate, dedupes concurrent first-loads, and re-checks
  enabled state after the load. ensureSandboxRunner() extracted/shared.
- Hook dispatch (content:before/after*, page:metadata) and route
  dispatch iterate candidates whose declared hooks include the event and
  load only those, preserving config order. A public render loads no
  plugin that doesn't declare page:metadata.

Marketplace/registry plugins remain eagerly loaded at cold start
(unchanged) but are registered into the unified dispatch; deferring
their load needs an R2 manifest-only fetch and is left as a follow-up.

Tests: a write-only plugin isn't loaded on a public render; a
page:metadata plugin is loaded and invoked; unknown-metadata plugins
still load; ensureSandboxedPluginLoaded loads once and dedupes; disabled
plugins don't load; the generator emits hooks/routes from manifest.json.
@changeset-bot

changeset-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 157d684

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

This PR includes changesets to release 14 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash 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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 157d684 Jun 08 2026, 05:10 AM

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 704 lines across 5 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
emdash-playground 157d684 Jun 08 2026, 05:13 AM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 157d684 Jun 08 2026, 05:11 AM

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

npm i https://pkg.pr.new/emdash@1379

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 157d684

@ascorbic

ascorbic commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Over a second to load a plugin seems pretty crazy. We should really investigate why that's taking so long, and see how it could be optimised too, because we want pages that use plugins to be fast too.

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