perf(core): lazily load sandboxed plugins#1379
Conversation
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 detectedLatest commit: 157d684 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 157d684 | Jun 08 2026, 05:10 AM |
Scope checkThis 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. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
emdash-playground | 157d684 | Jun 08 2026, 05:13 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 157d684 | Jun 08 2026, 05:11 AM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
|
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. |
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:
dist/manifest.jsonand emits its declared hooks/routes onto the entry (SandboxedPluginEntrygainshooks?/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.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
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change) — sandbox-lazy-load + virtual-modules-sandbox suites (13 passed)pnpm formathas been runAI-generated code disclosure
Screenshots / test output