Engineering

Does obfuscation break Web Workers and Service Workers?

Not inherently — a worker runs ordinary JavaScript, and ordinary JavaScript survives protection. What breaks is the assumption that your app is one file. A worker is a separate script with its own global scope, loaded by URL at runtime, and every one of those properties has a consequence for how you protect it. Four things go wrong in practice, and all four are avoidable once you know where to look.

A worker is a second entry point, not part of your bundle

This is the root of most worker protection problems. new Worker("./analytics-worker.js") does not import anything at build time — it is a runtime fetch of a URL. Your bundler knows about the main bundle; unless you configured it to, it may not know that string is a module to build at all.

So the first question is not "will protection break my worker" but "is my worker even being protected". Three ways it can be missed:

  • It never entered the build. The worker file is copied to the output directory as a static asset, so it ships as readable source next to your carefully protected bundle. This is the most common outcome, and the most embarrassing — the code you most wanted to hide is often the heavy computation you moved into a worker precisely because it mattered.
  • Your protection step globs the wrong thing. A pattern like dist/main.*.js protects the bundle and skips dist/worker.js. Check what your protection step actually matched rather than trusting the pattern.
  • The worker is inlined as a blob. Some bundlers turn a worker into a Blob URL built from a string in the main bundle. That string gets protected along with everything else, which is fine — but see the CSP note below, because blob workers have their own policy requirements.

Verify by opening the shipped worker file directly in a browser tab. If you can read your own function names, it was not protected.

The postMessage boundary is a public contract

Everything a worker exchanges with the main thread goes through the structured clone algorithm, and structured clone copies property names. That makes your message shapes an interface between two separately protected files — exactly the situation where renaming properties bites.

// main.js
worker.postMessage({ kind: "resize", width: 1280, height: 720 });

// worker.js
self.onmessage = function (e) {
  if (e.data.kind === "resize") { /* ... */ }
};

If property renaming runs over both files in one project, kind becomes the same generated name on both sides and this keeps working. If the two files are protected in separate runs, each run picks its own names, the main thread sends { m1: "resize" }, the worker looks for m2, and the message is silently ignored. No error — just a feature that stopped happening.

Two rules follow. First, protect the main bundle and its workers in one project and one build, so cross-file naming decisions agree. Second, if you cannot do that, treat the message shape as an external API and exclude those names, the same way you would for a JSON contract with a server. The reasoning is the same as in Protect Members — renaming a property that crosses a boundary is a breaking change.

Note that string values are safe here. "resize" can be moved into a string table or encoded on both sides independently, because the comparison happens on the runtime value, not the source text. It is the key names that must agree.

There is no DOM, and some protections assume there is

A worker's global is self, not window. There is no document, no localStorage, and no navigator in the shape page code expects. That matters for two things:

  • Runtime defense features that inspect the page. Domain locking reads the current host, and anti-debug checks often reference page objects. In a worker context those references may be absent — and a defensive check that throws because document is undefined is a self-inflicted outage, not a protection. If you enable runtime defense, configure workers separately and test them in a worker, not on a page.
  • Your own environment sniffing. Code that branches on typeof window is common and fine, but combined with aggressive control-flow transforms it is worth confirming the branch still resolves the way you expect.

Service workers are stricter still: no DOM at all, an event-driven lifecycle that can be terminated and restarted between events, and no synchronous storage. Any protection layer that keeps state in a module-level variable and expects it to persist will be surprised by a restarted service worker.

CSP and importScripts

Workers get their own policy treatment, and this is where the option choices matter most.

  • Do not use eval-based options in a worker. Self-compression and eval-based dispatch both rebuild code from a string, which needs unsafe-eval. Workers are frequently governed by a stricter effective policy than the page, and a Chrome extension service worker forbids it outright. Structural transforms — control-flow flattening, string tables, member indirection, name mangling — emit ordinary code and are the right choice here. The general version of this argument is in does obfuscation break Content Security Policy.
  • importScripts() takes a URL, and URLs are strings. Moving that literal into a string table is safe — the value at runtime is unchanged. What is not safe is renaming a global that an imported script defines, or that your script expects an imported one to define. Scripts loaded with importScripts share the worker's global scope, so they are a cross-file naming contract in exactly the way Replace Globals warns about.
  • Blob workers need worker-src blob: in your policy. That is a bundler decision rather than an obfuscation one, but it surfaces at the same moment — when you first test the protected build under a real policy — so it gets blamed on protection.

How to test it in ten minutes

Workers fail quietly, so a checklist beats poking at the UI:

  • Confirm the worker file was protected. Open the shipped file. Look for generated names.
  • Confirm it loads. The Network panel should show a 200 for the worker URL, and the Application panel should list a running service worker. A 404 here means your protection step renamed or moved the file.
  • Confirm messages round-trip. Log the received e.data on both sides once. Mismatched property names are visible immediately and invisible otherwise.
  • Watch the console for CSP violations specifically, not just errors. A blocked eval reports as a policy violation, not an exception you can catch.
  • Exercise a service worker restart by stopping it in the Application panel and triggering an event. State-dependent protection bugs only appear on the second start.

The general principle is the one from verifying an obfuscator did not break your code: protected output that loads is not protected output that works, and a worker is the easiest place in a web app for the difference to hide.

The short version

Obfuscation does not break workers. Treating a worker as an afterthought does. Put the worker in the same protection project as the bundle that talks to it, keep eval-based options off, treat message property names and shared globals as the cross-file contracts they are, and test the worker in a worker rather than assuming page behavior transfers.

Related reading