Tampermonkey Web Workers Break in Dev: Same-Origin Traps and Vite Workarounds
Userscript developers who want background computation via Web Workers lose the fast dev loop when workers silently fail. The same-origin constraint and Vite's dev-mode behavior combine into a debugging dead end that wastes hours on what looks like a configuration problem but is really a fundamental mismatch between how userscripts and bundlers resolve worker URLs.
Creating a Web Worker from a Tampermonkey script during development hits a wall of silent failures. The `new Worker()` call resolves paths against the host page's origin, not the userscript's source, so the request either 404s into an HTML document or hangs indefinitely. Vite's `?worker&inline` syntax, which bundles the worker into a blob at build time, doesn't bundle in dev mode — it emits a wrapper that still fetches an external URL, which Tampermonkey's sandbox blocks.
Eight combinations of JS/TS and four import strategies were tested on a live site. Only the manual Blob-inlining approach worked in dev for plain JS. TypeScript workers failed because the browser received raw TS with type annotations it can't parse. The breakthrough leverages Vite's dev server behavior: requesting a `.ts` file from the Vite server triggers automatic type erasure, returning clean JavaScript. A `fetch` to that URL, followed by `new Blob()` and `URL.createObjectURL()`, produces a worker that runs in dev while preserving TypeScript source and type checking.
For workers that import other modules, the fetch trick breaks because the inlined code can't resolve dependencies. A custom Vite plugin using rolldown bundles the worker into an IIFE on the fly and injects it as an inline data URI, replacing Vite's external URL wrapper. This handles complex workers and automatically revokes the blob URL after creation.
Vite's dev-time type erasure is an underused escape hatch: fetching a `.ts` file from the dev server gives you clean JS without a build step, which is useful beyond worker loading.
The same-origin restriction on workers is often conflated with CORS, but they are entirely separate mechanisms — a server can be CORS-permissive and still block cross-origin worker instantiation.
Tampermonkey's execution environment sits in a limbo between a browser extension and a page script, so assumptions that hold for SPA development (like `import.meta.url` or Vite's dev server being reachable) break silently.
Many 'worker doesn't work' bugs trace back to the host page returning an HTML error page with a 200 status, which the Worker constructor accepts and then fails to parse — a failure mode that produces confusing error messages.
The fetch-and-inline pattern is a generalizable trick for any scenario where you need to load a module-transformed resource at runtime without a bundler's help.
The fetch-based workaround breaks as soon as the worker module imports other files. A plugin remains the necessary fix for that case. Vite 8's new bundledDev feature is floated as a potential alternative.
if your work.ts imports other files, it won't work
I tried it, yes, you still have to use a plugin to handle this
vite8 recently released a bundledDev, you can try it