Loading Scripts with importScripts
Use importScripts in a classic MV3 service worker: when it is allowed, path resolution, load order, why it fails after startup, mixing with ES modules, and when to switch to a bundler or module worker instead.
Table of Contents
The background code is split across several files — a messaging router, a storage layer, a vendored library — and in MV2 the manifest listed them all in background.scripts. MV3 accepts a single service_worker file. Without a bundler, the classic way to pull in the others is importScripts("router.js", "storage.js"), and it works — until someone calls it inside an event handler and gets “Failed to execute ‘importScripts’ on ‘WorkerGlobalScope’: Module scripts don’t support importScripts()”, or a different error saying it was called after installation. This guide covers when importScripts is the right tool, the rules it follows in extension workers, and when to move to modules or a bundle. It belongs to service worker fundamentals.
How importScripts works in an extension worker
importScripts(...urls) synchronously fetches and executes classic scripts in the worker’s global scope, in the order given, sharing globals with the calling script. It exists only in classic workers — those declared without "type": "module" — and throws in module workers. For service workers, the specification restricts it to the worker’s initial script evaluation (and, for previously imported URLs, the install phase); calls made later, such as inside an onMessage listener, fail because a service worker’s script set must be known up front. Paths resolve relative to the worker script’s location within the extension package. Imported files are not subject to network fetching — they load from the package — but a missing file or an exception in any imported script fails the whole worker start.
Step-by-step: using it correctly
1. Declare a classic worker
1{ "background": { "service_worker": "sw.js" } } // no "type": "module"
Execution context: the manifest. Omitting type makes the worker classic, where importScripts exists and import statements are a syntax error. Choose one model for the whole worker — mixing produces registration failures, as described in fixing “service worker registration failed”.
2. Import everything at the top, in dependency order
1// sw.js
2importScripts(
3 "vendor/idb-keyval.js", // defines self.idbKeyval
4 "lib/storage.js", // uses idbKeyval, defines self.Store
5 "lib/router.js", // uses Store, defines self.Router
6);
7
8chrome.runtime.onMessage.addListener(self.Router.handle);
9chrome.alarms.onAlarm.addListener(self.Router.onAlarm);
Execution context: the service worker’s first synchronous evaluation. Each imported file runs to completion before the next starts, so order expresses dependencies. Imported scripts communicate through globals on self; keep them namespaced (one global per file) to avoid collisions. Listener registration still happens at the top level of sw.js, after the imports, in the same synchronous pass.
3. Do not call it lazily
1// WRONG — throws after initial evaluation
2chrome.runtime.onMessage.addListener((msg) => {
3 if (msg.type === "pdf") { importScripts("lib/pdf.js"); parsePdf(msg.data); }
4});
5
6// RIGHT — import at the top even if rarely used, or move the work to an offscreen document
7importScripts("lib/pdf.js");
Execution context: the service worker. There is no lazy loading in service workers — neither importScripts after startup nor dynamic import(). If a rarely used library is large enough to slow every cold start, move the work that needs it into an offscreen document or extension page, which can load scripts on demand, as covered in code splitting and dynamic imports in MV3.
4. Make imported files worker-safe
1// lib/storage.js — classic script, no window, no document, one namespaced global
2(function (global) {
3 const Store = {
4 async get(key) { return (await chrome.storage.local.get(key))[key]; },
5 async set(key, value) { await chrome.storage.local.set({ [key]: value }); },
6 };
7 global.Store = Store;
8})(globalThis);
Execution context: a classic script imported by the worker. An exception in any imported file aborts the worker’s start, so imported code must not touch window or document and must not throw at load time. Wrapping each file in an IIFE that exposes one global keeps internals private, the classic-script equivalent of a module.
5. Handle third-party libraries carefully
Libraries published as UMD or IIFE bundles usually work with importScripts, exposing a global. Libraries published only as ES modules do not — they contain export statements, a syntax error in a classic script. For those, switch the worker to "type": "module", or bundle. Check that a library does not assume a window (common in UMD builds that test typeof window).
6. Know when to migrate to modules or a bundle
importScripts suits small extensions with a handful of hand-written files and no build step. Once you have npm dependencies, TypeScript, or more than a few files, a module worker (import/export with static imports) gives proper scoping, and a bundler gives tree-shaking and a single file with predictable start-up. The migration is mechanical: replace importScripts calls with import statements, replace globals with exports, and set "type": "module". See using ES modules in an MV3 service worker.
7. Test start-up after every change
Any error in any imported file prevents the worker from starting. After changing imports, reload the extension, check the Errors view on chrome://extensions, and trigger an event from a cold start. A CI step that loads the worker’s files in order in a worker-like environment catches load-time exceptions before release.
Common mistakes
importScriptsin a module worker. It throws; useimport.- Calling it inside handlers. Only allowed during initial evaluation.
- Wrong order. A file using a global defined by a later file fails.
- Imported files touching
window. The whole worker fails to start. - ES-module-only libraries.
exportis a syntax error in classic scripts.
Keep imported files small and side-effect free
Every imported file runs on every cold start, so top-level work in those files — building lookup tables, reading storage, parsing large constants — adds directly to wake-up latency. Define functions and constants at load time and defer anything expensive until a handler needs it. A worker whose imports finish in a few milliseconds keeps every event, from alarms to popup messages, feeling instant.
Cross-browser variation
- Chrome / Edge:
importScriptsin classic extension service workers during initial evaluation. - Firefox: the MV3 background is typically an event page declared with
background.scripts, which loads files in order — the manifest replacesimportScripts. In a Firefox service worker background, the same rules as Chrome apply. - Safari: classic workers support
importScripts; module workers require recent versions.
Verification
- Reload the extension and confirm no errors; the worker starts.
- Stop the worker and trigger an event; confirm all imported globals exist.
- Temporarily move an
importScriptscall into a handler and confirm it throws — then move it back. - Remove an imported file from the build and confirm registration fails with a clear error, proving the test catches missing files.
FAQ
Can importScripts load files from a URL?
Only from the extension package. Loading remote scripts is remote code and blocked.
Does importScripts slow the cold start?
Each file is read and evaluated on every start. A few small files are negligible; many large ones add up — bundling helps.
Can I conditionally import based on browser?
Yes, with a synchronous condition at the top level, before listener registration. It must still happen during initial evaluation.
Is importScripts deprecated for extensions?
No. It remains supported for classic workers. Module workers are simply the more modern option, and bundlers make the choice largely invisible.
How do I share code between the worker and extension pages with importScripts?
Write the shared file as a classic script that attaches one global, import it in the worker with importScripts, and include it in pages with a <script> tag before the page’s own script. A bundler removes this duplication.
Related
- Using ES modules in an MV3 service worker — the module alternative.
- Bundling with esbuild or Rollup — producing a single worker file.
- Reducing service worker cold start latency — the start-up cost of imports.
- Service worker fundamentals — the parent topic.