跪拜 Guibai
← All articles
JavaScript · TypeScript · Frontend

Tampermonkey Web Workers Break in Dev: Same-Origin Traps and Vite Workarounds

By 天平 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
`new Worker(url)` enforces same-origin with the host page, not the script's origin — CORS settings are irrelevant.
A missing worker file often returns a 200 HTML page from the host site, causing a syntax error when the Worker tries to parse `<!DOCTYPE html>` as JavaScript.
Vite's `?worker&inline` only inlines at build time; in dev mode it emits a wrapper that fetches an external URL, which Tampermonkey cannot load.
TypeScript workers fail in the browser because raw `.ts` files contain type annotations that aren't valid JavaScript.
Requesting a `.ts` file from Vite's dev server triggers automatic type erasure, returning plain JavaScript that can be fetched and inlined.
A fetch-to-Blob pattern works for simple workers in dev but breaks when the worker imports other modules or packages.
A Vite plugin using rolldown can bundle the worker into an IIFE on the fly and inject it as an inline data URI, handling dependencies and automatically revoking the blob URL.
`import.meta.url` resolves relative to the script file, not the page URL, but fails in Tampermonkey's IIFE output where module context is absent.
Conclusions

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.

Concepts & terms
Same-origin restriction for Workers
The `Worker` constructor can only load scripts from the same origin as the page that creates it. This is a browser security policy distinct from CORS; even a CORS-enabled cross-origin URL will be rejected.
Blob URL inlining
A technique where JavaScript source code is wrapped in a `Blob`, converted to an object URL via `URL.createObjectURL()`, and passed to `new Worker()`. This bypasses the need for a physical worker file on the server.
Vite type erasure in dev
When Vite's dev server receives a request for a `.ts` file, it strips TypeScript type annotations on the fly and returns plain JavaScript — no full compilation needed. This is how `.ts` imports work in dev mode without a build step.
import.meta.url
A module-scoped value that returns the URL of the current JavaScript module file. Used with `new URL('./relative/path', import.meta.url)` to resolve worker paths relative to the script rather than the page URL, but unavailable in non-module scripts like Tampermonkey's IIFE output.
Vite ?worker&inline
A Vite-specific import suffix that instructs the bundler to inline a web worker as a Blob URL at build time. In development mode, Vite does not bundle and instead generates a wrapper that fetches the worker externally, which fails in restricted environments like Tampermonkey.
From the discussion

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.

The fetch trick fails when the worker module has further imports, because those dependencies aren't resolved or bundled in dev mode.
A plugin-based solution is still required to handle multi-file worker modules during development.
Vite 8's bundledDev may offer a built-in way to bundle worker dependencies in dev, sidestepping the need for a custom plugin.
Featured comments
增量编译

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

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗