Firefox Worker Debugging
Firefox DevTools provides a first-class debugger for dedicated Web Workers, accessible directly from the Debugger panel without any extensions or flags. This guide is part of Debugging, Profiling & Production Optimization and covers the complete workflow: locating worker threads, setting breakpoints, reading scope, evaluating expressions in the worker’s context, stepping through postMessage handlers, and profiling CPU usage per thread.
The symptom that drives engineers here is familiar: a worker silently swallows an exception, returns a wrong value, or stalls under load — and the usual console-based workflow gives no visibility into what is happening inside the isolated thread.
Prerequisites
Before following these steps, confirm the following:
- Firefox 110 or later (worker thread list in Debugger is stable from Firefox 99; profiler improvements landed in 110).
- DevTools opened before the worker is created. Firefox registers threads as they spawn; workers that started before DevTools opened may not appear in the thread list.
- If you are debugging a service worker, use
about:debugginginstead of the per-page Debugger panel (covered below in Step 1b). - For bundled workers (Vite, webpack), source maps must be present — either inline (
//# sourceMappingURL=data:...) or as separate.mapfiles served alongside the bundle. Without them, breakpoints land on minified output.
The unified Debugger panel that shows workers in a thread list was stabilized in Firefox 99. If you are on an older version, update before following this guide. Firefox Developer Edition tracks the same features one release ahead and is a good choice for Worker debugging work.
Step 1 — Open DevTools and Navigate to the Debugger
Press F12 (Windows/Linux) or Cmd+Opt+I (macOS) to open DevTools. Click the Debugger tab in the toolbar.
If the page has not yet created any workers, the Sources sidebar will show only the main-document scripts. The worker entries appear dynamically as workers are spawned.
Step 1b — Service workers: use about:debugging
Dedicated workers live inside the per-page Debugger. Service workers are global browser processes not tied to a single tab, so Firefox exposes them through a separate surface:
- Type
about:debuggingin the address bar. - Click This Firefox in the left sidebar.
- Under Service Workers, find your origin and click Inspect next to the active worker registration.
A new DevTools window opens showing the service worker’s Debugger, Console, and Storage panels in isolation. Changes to the service worker script are reflected after you click Force update and reload the controlled page.
A service worker only appears under about:debugging while it is active (installed and controlling at least one client). If it is in the waiting state, you must either close all controlled tabs or click skipWaiting in the registration panel before the new version activates and appears.
Step 2 — Locate the Worker in the Threads/Sources List
With DevTools open, trigger the code path that calls new Worker(...). Watch the Sources sidebar in the Debugger: a Workers heading appears, and your worker’s script URL is listed underneath it.
For module workers ({ type: 'module' }), the source tree expands to show the entry module and any imported modules. For classic script workers, a single file is listed.
Blob workers (new Worker(URL.createObjectURL(blob))) appear with a blob: URL. If you are generating the worker source at runtime, add a //# sourceURL=my-worker.js comment inside the blob string — Firefox uses this annotation as the display name in the Sources list.
// main.js — annotate blob workers for the Debugger
const code = `
//# sourceURL=my-worker.js
self.onmessage = ({ data }) => {
const result = heavyTransform(data);
self.postMessage(result);
};
function heavyTransform(v) { return v * 2; }
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
Without a //# sourceURL annotation, Firefox displays blob workers as debugger eval code in stack traces and the Sources list. The annotation costs zero bytes at runtime and makes breakpoint targets human-readable.
Step 3 — Set Breakpoints in Worker Scripts
Click on the worker entry in the Sources sidebar to open its source in the editor pane. Set a breakpoint by clicking the line number gutter — a blue dot marker confirms the breakpoint is active.
Firefox supports the same breakpoint types for worker scripts as for main-thread scripts:
| Breakpoint type | How to set | Use case |
|---|---|---|
| Line breakpoint | Click line number | Pause at a specific statement |
| Conditional breakpoint | Right-click → Add condition | Pause only when an expression is true |
| Log point | Right-click → Add log | Print to console without pausing |
| Event listener breakpoint | Debugger → Event Listener Breakpoints | Pause on message events |
For postMessage handlers, the most practical approach is the Event listener breakpoint. In the right-side panel of the Debugger, expand Event Listener Breakpoints → Worker and check message. Firefox will pause inside onmessage every time a message arrives at the worker, regardless of which line it lands on.
// worker.ts — structured message handler; set a breakpoint on line 6
interface ParseRequest {
type: 'PARSE';
csv: string;
}
self.onmessage = ({ data }: MessageEvent<ParseRequest>) => {
if (data.type === 'PARSE') { // ← breakpoint here
const rows = parseCSV(data.csv);
self.postMessage({ type: 'RESULT', rows });
}
};
function parseCSV(raw: string): string[][] {
return raw.split('\n').map(line => line.split(','));
}
Pausing a worker thread does not pause the main thread. The main thread continues executing, so postMessage calls accumulate in the worker's message queue while you inspect variables. When you resume, all queued messages process in order. This is useful for observing message batching behavior.
Step 4 — Inspect Worker Scope and Variables
When execution pauses inside a worker, the Scopes pane in the right sidebar populates with the full lexical environment at that stack frame. This is the core topic explored in depth in Inspecting Worker Scopes in Firefox DevTools.
The scope hierarchy shown is:
- Block —
letandconstdeclarations in the current block. - Local — function-level
vardeclarations and parameters. - Closure — variables captured from enclosing functions.
- Module — exported/imported bindings (module workers only).
- Worker — the worker’s global object (
self), which containsonmessage,postMessage,importScripts,caches, and any global variables the worker script defines.
Expand any scope node to read live values. Primitive values show inline; objects and arrays expand to their properties. You can also hover over a variable name in the source editor to see a tooltip with its current value.
Step 5 — Step Through postMessage Handlers
Stepping controls in Firefox work identically for worker threads and main-thread scripts. The toolbar buttons are:
- F8 / Resume — continue execution until the next breakpoint.
- F10 / Step Over — execute the current line and pause on the next.
- F11 / Step Into — enter the function called on the current line.
- Shift+F11 / Step Out — run to the end of the current function and pause in the caller.
To trace the full round-trip of a postMessage call:
- Set a breakpoint in the main-thread code where
worker.postMessage(...)is called. - Set a second breakpoint inside the worker’s
onmessagehandler. - Resume after the first breakpoint — the main thread delivers the message to the worker’s queue and the worker’s
onmessagefires on the worker thread. - Firefox automatically switches the Debugger’s active thread context to the worker when execution pauses there.
The active thread context is shown in a dropdown at the top of the Debugger panel. You can switch between main thread and worker threads manually to inspect both call stacks simultaneously.
// main.ts — round-trip tracing
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
worker.onmessage = ({ data }: MessageEvent<{ rows: string[][] }>) => {
// Breakpoint A: inspect the returned rows here
console.table(data.rows);
};
function triggerParse(csv: string): void {
// Breakpoint B: confirm the message is sent with the right payload
worker.postMessage({ type: 'PARSE', csv });
}
When you pause in a worker, the main thread continues. If you then resume from the worker breakpoint, the main thread may have advanced past its own breakpoint. Use the thread-context dropdown to switch between threads and inspect each call stack independently.
Step 6 — Evaluate Expressions in the Worker Console Context
The DevTools Console panel has a context picker in its toolbar — a dropdown that defaults to Top (the main document). To evaluate expressions inside a running worker:
- Open the Console panel.
- Click the context picker dropdown.
- Select the worker by its script name or URL.
Any expression you type now runs in the worker’s global scope. You can read self.postMessage, call functions defined in the worker script, inspect module-level state, or run performance microbenchmarks inside the worker’s heap.
// Expressions typed in the worker console context:
self.constructor.name // → "DedicatedWorkerGlobalScope"
typeof importScripts // → "function" (classic workers only)
typeof document // → "undefined" — DOM is not available
performance.now() // → time in ms since worker started
The worker does not need to be paused for the console context switch to work. This makes it useful for reading live state without interrupting execution — for example, to observe how a counter variable changes over time.
If the worker is paused at a breakpoint, console expressions evaluate in the current stack frame's scope — exactly like Chrome DevTools. If the worker is running, expressions evaluate at module scope. This distinction matters when you need to access a local variable: pause first, then evaluate.
Step 7 — Profile Worker Threads in the Firefox Profiler
The Firefox Profiler captures CPU samples from all threads, including workers. Each thread appears as a separate horizontal lane in the timeline.
To profile a worker:
- Open the Performance panel in DevTools.
- Click Start Recording.
- Trigger the workload (e.g., send a large payload to the worker).
- Click Stop Recording.
- The profiler opens as a separate tab at
profiler.firefox.com.
In the profiler timeline, worker threads are labeled with their script URL. The Call Tree view aggregates samples by function; the Flame Graph shows the full call stack for each sample period.
Useful profiler features for workers:
- Thread filter — click a thread lane header and press “Focus on thread” to hide all other threads. This isolates worker CPU samples from main-thread rendering noise.
- Markers — add
performance.mark()calls inside the worker to insert named markers in the profiler timeline. These appear as colored ticks on the worker’s lane. - CPU usage % per thread — hover over a thread lane to see the percentage of samples in each function category.
// worker.ts — instrumented with profiler markers
self.onmessage = ({ data }: MessageEvent<{ csv: string }>) => {
performance.mark('parse-start');
const rows = parseCSV(data.csv);
performance.mark('parse-end');
performance.measure('parse-duration', 'parse-start', 'parse-end');
self.postMessage({ rows });
};
Parsing a 5 MB CSV (≈100,000 rows) typically consumes 40–80 ms of worker CPU time on a mid-range laptop. The Firefox Profiler measures this at roughly 0.5–1 ms per 1,000 rows for a naive split-based parser. Replacing the parser with a compiled WebAssembly module typically reduces this to 0.05–0.1 ms per 1,000 rows.
Verification & Measurement
After setting up breakpoints and profiling, confirm your setup is working correctly:
- Verify thread registration — Open the Debugger → Sources list. The worker’s URL must appear under the Workers heading. If it does not, the worker was created before DevTools opened; reload with DevTools already open.
- Verify breakpoints — Send a test message. Execution must pause in the worker pane with the correct line highlighted. If the breakpoint is greyed out (hollow circle), the source file was not matched — check that the
sourceURLannotation or source map path is correct. - Verify console context — Type
self.constructor.namein the Console with the worker context selected. It must return"DedicatedWorkerGlobalScope", not"Window". - Verify profiler thread — After recording, confirm the worker thread lane is labeled with the correct script URL. Sample count should be non-zero if the worker executed during the recording window.
- Measure baseline — Add
performance.mark/performance.measurecalls and record a profiler trace before optimizing. Record a second trace after changes and compare flame chart shapes and sample counts.
Failure Modes
Worker does not appear in Sources list
Cause: The worker was created before DevTools was opened. Fix: Reload the page with DevTools already open. The page must create the worker after DevTools attaches.
Breakpoints are not hit
Cause 1: Source map mismatch. The source file in the Debugger shows the compiled output but breakpoints are set against the original source. Ensure the source map path in the .js file points to an accessible .map file.
Cause 2: The worker script URL does not match what Firefox loaded. Check the URL in the Sources list against the path passed to new Worker(...).
Cause 3: The message that would trigger the handler was sent before the breakpoint was set.
Console context shows wrong scope
Cause: The context picker reverts to Top after page navigation or worker restart. Fix: Re-select the worker in the context picker after each reload.
Profiler shows no worker thread
Cause: The worker terminated before the profiler stopped recording, or no messages were sent to the worker during the recording window. Fix: Extend the recording duration or add a keep-alive loop in the worker during profiling. Confirm the worker is alive by checking the Sources list.
about:debugging shows no service workers
Cause: The service worker registration failed (check the Console for errors) or the page is not served over HTTPS (service workers require a secure origin, except on localhost).
Browser Compatibility & Tooling Comparison
| Feature | Firefox | Chrome | Safari | Edge |
|---|---|---|---|---|
| Worker thread in Debugger Sources list | Firefox 99+ | Chrome 38+ | Safari 16+ | Edge 79+ |
| Breakpoints in worker scripts | Firefox 46+ | Chrome 38+ | Safari 16+ | Edge 79+ |
| Console context picker for workers | Firefox 56+ | Chrome 72+ | Not available | Edge 79+ |
about:debugging for service workers |
All modern | chrome://serviceworker-internals/ |
Not equivalent | Same as Chrome |
| Profiler per-thread flame chart | Firefox 55+ | Chrome DevTools Performance | Not available | Edge DevTools Performance |
performance.mark in workers |
Firefox 41+ | Chrome 43+ | Safari 11+ | Edge 16+ |
| Source maps in worker scripts | Firefox 48+ | Chrome 38+ | Safari 16+ | Edge 79+ |
Blob worker //# sourceURL annotation |
Firefox 48+ | Chrome 38+ | Safari 16+ | Edge 79+ |
For a detailed feature-by-feature comparison with actionable guidance on when to use each browser’s tools, see Comparing Chrome and Firefox Worker Tooling.
The key practical difference is the profiler: Firefox Profiler (accessible at profiler.firefox.com) is a standalone shareable tool with superior per-thread filtering. Chrome’s Performance panel is integrated into DevTools and ties more tightly to the rendering pipeline. If your bottleneck is in worker CPU time, profile in Firefox first.
For teams already familiar with Chrome DevTools, the Chrome DevTools Worker Debugging reference covers the equivalent workflow. Checking postMessage Bottleneck Analysis is also useful when worker pauses look healthy but end-to-end latency is still high — often the bottleneck is in serialization cost, not worker computation.