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:debugging instead 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 .map files served alongside the bundle. Without them, breakpoints land on minified output.
Firefox version

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:

  1. Type about:debugging in the address bar.
  2. Click This Firefox in the left sidebar.
  3. 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.

Service worker lifecycle

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));
Blob worker annotation

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 BreakpointsWorker 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(','));
}
Breakpoints and worker timing

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:

  1. Blocklet and const declarations in the current block.
  2. Local — function-level var declarations and parameters.
  3. Closure — variables captured from enclosing functions.
  4. Module — exported/imported bindings (module workers only).
  5. Worker — the worker’s global object (self), which contains onmessage, 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.

Firefox DevTools Debugger layout for a dedicated Web Worker Three-panel layout: left sidebar shows Threads and Sources list with a worker entry; center shows the worker source with a breakpoint; right panel shows the Scopes pane with worker variables. Sources ▾ This page main.js ▾ Workers worker.ts parseCSV.ts ▾ Event Listeners ☑ message ☐ error worker.ts 1 self.onmessage = ({ data }) => { 2 if (data.type === 'PARSE') { 3 const rows = parseCSV(data.csv); 4 self.postMessage({ rows }); 5 } 6 }; Paused on breakpoint — line 3 Scopes ▾ Local data: {type: "PARSE", …} rows: undefined ▸ Closure ▾ Worker self: DedicatedWorker… onmessage: function postMessage: function Console context: worker.ts (thread)
The Firefox Debugger showing a dedicated worker paused at a breakpoint: Sources list on the left with the worker entry highlighted, source code with a breakpoint marker in the center, and the Scopes pane on the right showing live worker variables.

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:

  1. Set a breakpoint in the main-thread code where worker.postMessage(...) is called.
  2. Set a second breakpoint inside the worker’s onmessage handler.
  3. Resume after the first breakpoint — the main thread delivers the message to the worker’s queue and the worker’s onmessage fires on the worker thread.
  4. 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 });
}
Thread context switching

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:

  1. Open the Console panel.
  2. Click the context picker dropdown.
  3. 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.

Console context and paused workers

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:

  1. Open the Performance panel in DevTools.
  2. Click Start Recording.
  3. Trigger the workload (e.g., send a large payload to the worker).
  4. Click Stop Recording.
  5. 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 });
};
Realistic profiling numbers

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:

  1. 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.
  2. 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 sourceURL annotation or source map path is correct.
  3. Verify console context — Type self.constructor.name in the Console with the worker context selected. It must return "DedicatedWorkerGlobalScope", not "Window".
  4. 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.
  5. Measure baseline — Add performance.mark / performance.measure calls 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.

Frequently Asked Questions

How do I make a worker appear in the Firefox Debugger's thread list?
The worker must be running when you open DevTools. Open DevTools first, then trigger the code path that calls new Worker(...). If the worker has already terminated, it will not appear. For blob: workers, Firefox shows the URL as blob:... — set your breakpoint in the Sources list before the worker starts to catch initialization.
How do I evaluate an expression in a worker's context in Firefox?
In the DevTools Console, click the context-picker dropdown (it shows ‘Top’ by default) and select the worker thread by name or URL. Any expression you type then runs in that worker’s global scope — self, worker-local variables, and all. The worker does not need to be paused for this to work.
Can Firefox profile individual worker threads separately?
Yes. In the Firefox Profiler (available via the Performance panel or profiler.firefox.com), each thread appears as a separate lane in the timeline. Worker threads are labeled by their script URL. You can filter the flame chart to a single thread to isolate CPU time spent in worker code without main-thread noise.
What is the difference between about:debugging and the Debugger panel for workers?
about:debugging lists all active service workers across all origins and lets you inspect, force-update, or unregister them — it is a global registry, not a per-page tool. The DevTools Debugger panel shows dedicated workers for the current page only, lets you set breakpoints in worker scripts, and is where you pause and step through worker execution.

See also