Where dynamic import() works
The platform rules, before any bundler enters the picture:
The service worker restriction is the one that surprises people: dynamic
import() is unavailable inside service workers at the platform level, in any browser. Static import statements at the top of the worker are fine (Extension.js bundles them), but you cannot defer a chunk load until an event fires.
Pattern 1: split by surface, not by chunk
The service worker should orchestrate, not compute. Keep its static imports to glue code (routing messages, registering listeners) and move anything heavy to a surface where lazy loading works:- UI-adjacent work belongs in the page that needs it. A popup that formats code imports the formatter in the popup, not the worker.
- Background-only heavy work (parsing, encoding, inference) belongs in an offscreen document, created on demand and closed when done. The worker stays a thin message router.
background.ts
import() the heavy library the first time it is asked, and the cost is paid once per document lifetime instead of once per worker wake-up.
Pattern 2: lazy import inside extension pages
Inside any extension page, plain dynamic import works and the bundler splits the chunk automatically:popup.ts
Pattern 3: lazy import in content scripts
Content scripts execute in the page, so their lazily loaded chunks must be fetchable by that page. Two steps:- Expose the chunk paths via
web_accessible_resources. - Import through
chrome.runtime.getURLso the URL resolves to your extension origin:
content.ts
What to keep out of the worker entirely
- Anything DOM-shaped (
DOMParser, canvas, audio): the worker has no DOM at all; use an offscreen document. - Multi-megabyte wasm: instantiate it in an offscreen document or an extra page, and cache results in
chrome.storageso restarts are cheap. - One-off tooling (imports, migrations, exports): a dedicated page under
pages/keeps it out of every startup path.

