Using Web Workers - Web APIs | MDN
Skip to search
Using Web Workers
Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. In addition, they can make network requests using the
fetch()
or
XMLHttpRequest
APIs. Once created, a worker can send messages to the JavaScript code that created it by posting messages to an event handler specified by that code (and vice versa).
This article provides a detailed introduction to using web workers.
Web Workers API
A worker is an object created using a constructor (e.g.,
Worker()
) that runs a named JavaScript file — this file contains the code that will run in the worker thread; workers run in another global context that is different from the current
window
. Thus, using the
window
shortcut to get the current global scope (instead of
self
) within a
Worker
will return an error.
The worker context is represented by a
DedicatedWorkerGlobalScope
object in the case of dedicated workers (standard workers that are utilized by a single script; shared workers use
SharedWorkerGlobalScope
). A dedicated worker is only accessible from the script that first spawned it, whereas shared workers can be accessed from multiple scripts.
Note:
See
The Web Workers API landing page
for reference documentation on workers and additional guides.
You can run whatever code you like inside the worker thread, with some exceptions. For example, you can't directly manipulate the DOM from inside a worker, or use some default methods and properties of the
window
object. But you can use a large number of items available under
window
, including
WebSockets
, and data storage mechanisms like
IndexedDB
. See
Functions and classes available to workers
for more details.
Data is sent between workers and the main thread via a system of messages — both sides send their messages using the
postMessage()
method, and respond to messages via the
onmessage
event handler (the message is contained within the
message
event's data attribute). The data is copied rather than shared.
Workers may in turn spawn new workers, as long as those workers are hosted within the same
origin
as the parent page.
In addition, workers can make network requests using the
fetch()
or
XMLHttpRequest
APIs (although note that the
responseXML
attribute of
XMLHttpRequest
will always be
null
).
Dedicated workers
As mentioned above, a dedicated worker is only accessible by the script that called it. In this section we'll discuss the JavaScript found in our
Basic dedicated worker example
run dedicated worker
): This allows you to enter two numbers to be multiplied together. The numbers are sent to a dedicated worker, multiplied together, and the result is returned to the page and displayed.
This example is rather trivial, but we decided to keep it simple while introducing you to basic worker concepts. More advanced details are covered later on in the article.
Worker feature detection
For slightly more controlled error handling and backwards compatibility, it is a good idea to wrap your worker accessing code in the following (
main.js
):
js
if (window.Worker) {
// …
Spawning a dedicated worker
Creating a new worker is simple. All you need to do is call the
Worker()
constructor, specifying the URI of a script to execute in the worker thread (
main.js
):
js
const myWorker = new Worker("worker.js");
Note:
Bundlers, including
webpack
Vite
, and
Parcel
, recommend passing URLs that are resolved relative to
import.meta.url
to the
Worker()
constructor. For example:
js
const myWorker = new Worker(new URL("worker.js", import.meta.url));
This way, the path is relative to the current script instead of the current HTML page, which allows the bundler to safely do optimizations like renaming (because otherwise the
worker.js
URL may point to a file not controlled by the bundler, so it cannot make any assumptions).
Sending messages to and from a dedicated worker
The magic of workers happens via the
postMessage()
method and the
onmessage
event handler. When you want to send a message to the worker, you post messages to it like this (
main.js
):
js
[first, second].forEach((input) => {
input.onchange = () => {
myWorker.postMessage([first.value, second.value]);
console.log("Message posted to worker");
};
});
So here we have two
elements represented by the variables
first
and
second
; when the value of either is changed,
myWorker.postMessage([first.value,second.value])
is used to send the value inside both to the worker, as an array. You can send pretty much anything you like in the message.
In the worker, we can respond when the message is received by writing an event handler block like this (
worker.js
):
js
onmessage = (e) => {
console.log("Message received from main script");
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
console.log("Posting message back to main script");
postMessage(workerResult);
};
The
onmessage
handler allows us to run some code whenever a message is received, with the message itself being available in the
message
event's
data
attribute. Here we multiply together the two numbers then use
postMessage()
again, to post the result back to the main thread.
Back in the main thread, we use
onmessage
again, to respond to the message sent back from the worker:
js
myWorker.onmessage = (e) => {
result.textContent = e.data;
console.log("Message received from worker");
};
Here we grab the message event data and set it as the
textContent
of the result paragraph, so the user can see the result of the calculation.
Note:
Notice that
onmessage
and
postMessage()
need to be hung off the
Worker
object when used in the main script thread, but not when used in the worker. This is because, inside the worker, the worker is effectively the global scope.
Note:
When a message is passed between the main thread and worker, it is copied or "transferred" (moved), not shared. Read
Transferring data to and from workers: further details
for a much more thorough explanation.
Terminating a worker
If you need to immediately terminate a running worker from the main thread, you can do so by calling the worker's
terminate
method:
js
myWorker.terminate();
The worker thread is killed immediately.
Handling errors
When a runtime error occurs in the worker, its
onerror
event handler is called. It receives an event named
error
which implements the
ErrorEvent
interface.
The event doesn't bubble and is cancelable; to prevent the default action from taking place, the worker can call the error event's
preventDefault()
method.
The error event has the following three fields that are of interest:
message
A human-readable error message.
filename
The name of the script file in which the error occurred.
lineno
The line number of the script file on which the error occurred.
Spawning subworkers
Workers may spawn more workers if they wish. So-called sub-workers must be hosted within the same origin as the parent page. Also, the URIs for subworkers are resolved relative to the parent worker's location rather than that of the owning page. This makes it easier for workers to keep track of where their dependencies are.
Importing scripts and libraries
Worker threads have access to a global function,
importScripts()
, which lets them import scripts. It accepts zero or more URIs as parameters to resources to import; all the following examples are valid:
js
importScripts(); /* imports nothing */
importScripts("foo.js"); /* imports just "foo.js" */
importScripts("foo.js", "bar.js"); /* imports two scripts */
importScripts(
"//example.com/hello.js",
); /* You can import scripts from other origins */
The browser loads each listed script and executes it. Any global objects from each script may then be used by the worker. If the script can't be loaded,
NETWORK_ERROR
is thrown, and subsequent code will not be executed. Previously executed code (including code deferred using
setTimeout()
) will still be functional though. Function declarations
after
the
importScripts()
method are also kept, since these are always evaluated before the rest of the code.
Note:
Scripts may be downloaded in any order, but will be executed in the order in which you pass the filenames into
importScripts()
. This is done synchronously;
importScripts()
does not return until all the scripts have been loaded and executed.
Shared workers
A shared worker is accessible by multiple scripts — even if they are being accessed by different windows, iframes or even workers. In this section we'll discuss the JavaScript found in our
Basic shared worker example
run shared worker
): This is very similar to the basic dedicated worker example, except that it has two functions available handled by different script files:
multiplying two numbers
, or
squaring a number
. Both scripts use the same worker to do the actual calculation required.
Here we'll concentrate on the differences between dedicated and shared workers. Note that in this example we have two HTML pages, each with JavaScript applied that uses the same single worker file.
Note:
If SharedWorker can be accessed from several browsing contexts, all those browsing contexts must share the exact same origin (same protocol, host, and port).
Note:
In Firefox, shared workers cannot be shared between documents loaded in private and non-private windows (
Firefox bug 1177621
).
Spawning a shared worker
Spawning a new shared worker is pretty much the same as with a dedicated worker, but with a different constructor name (see
index.html
and
index2.html
) — each one has to spin up the worker using code like the following:
js
const myWorker = new SharedWorker("worker.js");
One big difference is that with a shared worker you have to communicate via a
port
object — an explicit port is opened that the scripts can use to communicate with the worker (this is done implicitly in the case of dedicated workers).
The port connection needs to be started either implicitly by use of the
onmessage
event handler or explicitly with the
start()
method before any messages can be posted. Calling
start()
is only needed if the
message
event is wired up via the
addEventListener()
method.
Note:
When using the
start()
method to open the port connection, it needs to be called from both the parent thread and the worker thread if two-way communication is needed.
Sending messages to and from a shared worker
Now messages can be sent to the worker as before, but the
postMessage()
method has to be invoked through the port object (again, you'll see similar constructs in both
multiply.js
and
square.js
):
js
squareNumber.onchange = () => {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log("Message posted to worker");
};
Now, on to the worker. There is a bit more complexity here as well (
worker.js
):
js
onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (e) => {
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
port.postMessage(workerResult);
};
};
First, we use an
onconnect
handler to fire code when a connection to the port happens (i.e., when the
onmessage
event handler in the parent thread is set up, or when the
start()
method is explicitly called in the parent thread).
We use the
ports
attribute of this event object to grab the port and store it in a variable.
Next, we add an
onmessage
handler on the port to do the calculation and return the result to the main thread. Setting up this
onmessage
handler in the worker thread also implicitly opens the port connection back to the parent thread, so the call to
port.start()
is not actually needed, as noted above.
Finally, back in the main script, we deal with the message (again, you'll see similar constructs in both
multiply.js
and
square.js
):
js
myWorker.port.onmessage = (e) => {
result2.textContent = e.data;
console.log("Message received from worker");
};
When a message comes back through the port from the worker, we insert the calculation result inside the appropriate result paragraph.
About thread safety
The
Worker
interface spawns real OS-level threads, and mindful programmers may be concerned that concurrency can cause "interesting" effects in your code if you aren't careful.
However, since web workers have carefully controlled communication points with other threads, it's actually very hard to cause concurrency problems. There's no access to non-thread-safe components or the DOM. And you have to pass specific data in and out of a thread through serialized objects. So you have to work really hard to cause problems in your code.
Content security policy
Workers are considered to have their own execution context, distinct from the document that created them. For this reason they are, in general, not governed by the
content security policy
of the document (or parent worker) that created them. So for example, suppose a document is served with the following header:
http
Content-Security-Policy: script-src 'self'
Among other things, this will prevent any scripts it includes from using
eval()
. However, if the script constructs a worker, code running in the worker's context
will
be allowed to use
eval()
To specify a content security policy for the worker, set a
Content-Security-Policy
response header for the request which delivered the worker script itself.
The exception to this is if the worker script's origin is a globally unique identifier (for example, if its URL has a scheme of data or blob). In this case, the worker does inherit the CSP of the document or worker that created it.
Transferring data to and from workers: further details
Data passed between the main page and workers is
copied
, not shared (except for certain objects that can be explicitly
shared
). Objects are serialized as they're handed to the worker, and subsequently, de-serialized on the other end. The page and worker
do not share the same instance
, so the end result is that
a duplicate
is created on each end. Most browsers implement this feature as
structured cloning
As you probably know by now, data is exchanged between the two threads via messages using
postMessage()
, and the
message
event's
data
attribute contains data passed back from the worker.
example.html
: (the main page):
js
const myWorker = new Worker("my_task.js");
myWorker.onmessage = (event) => {
console.log(`Worker said : "${event.data}"`);
};
myWorker.postMessage({ lastUpdate: new Date() });
my_task.js
(the worker):
js
self.onmessage = (event) => {
postMessage(`Last updated: ${event.data.lastUpdate.toDateString()}`);
};
The
structured cloning
algorithm can accept JSON and a few things that JSON can't — like circular references.
Passing data examples
Example 1: Advanced passing JSON Data and creating a switching system
If you have to pass some complex data and have to call many different functions both on the main page and in the Worker, you can create a system which groups everything together.
First, we create a
QueryableWorker
class that takes the URL of the worker, a default listener, and an error handler, and this class is going to keep track of a list of listeners and help us communicate with the worker:
js
function QueryableWorker(url, defaultListener, onError) {
const worker = new Worker(url);
const listeners = {};
this.defaultListener = defaultListener ?? (() => {});
if (onError) {
worker.onerror = onError;
this.postMessage = (message) => {
worker.postMessage(message);
};
this.terminate = () => {
worker.terminate();
};
Then we add the methods of adding/removing listeners:
js
this.addListeners = (name, listener) => {
listeners[name] = listener;
};
this.removeListeners = (name) => {
delete listeners[name];
};
Here we let the worker handle two simple operations for illustration: getting the difference of two numbers and making an alert after three seconds. In order to achieve that we first implement a
sendQuery
method which queries if the worker actually has the corresponding methods to do what we want.
js
// This functions takes at least one argument, the method name we want to query.
// Then we can pass in the arguments that the method needs.
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
if (!queryMethod) {
throw new TypeError(
"QueryableWorker.sendQuery takes at least one argument",
);
worker.postMessage({
queryMethod,
queryMethodArguments,
});
};
We finish QueryableWorker with the
onmessage
method. If the worker has the corresponding methods we queried, it should return the name of the corresponding listener and the arguments it needs, we just need to find it in
listeners
.:
js
worker.onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethodListener") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
listeners[event.data.queryMethodListener].apply(
this,
event.data.queryMethodArguments,
);
} else {
this.defaultListener(event.data);
};
Now onto the worker. First we need to have the methods to handle the two simple operations:
js
const queryableFunctions = {
getDifference(a, b) {
reply("printStuff", a - b);
},
waitSomeTime() {
setTimeout(() => {
reply("doAlert", 3, "seconds");
}, 3000);
},
};
function reply(queryMethodListener, ...queryMethodArguments) {
if (!queryMethodListener) {
throw new TypeError("reply - takes at least one argument");
postMessage({
queryMethodListener,
queryMethodArguments,
});
// This method is called when main page calls QueryWorker's postMessage
// method directly
function defaultReply(message) {
// do something
And the
onmessage
method is now trivial:
js
onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethod") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
queryableFunctions[event.data.queryMethod].apply(
self,
event.data.queryMethodArguments,
);
} else {
defaultReply(event.data);
};
Here are the full implementation:
example.html
(the main page):
html
It needs to execute the following script, either inline or as an external file:
js
// QueryableWorker instances methods:
// * sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc.): calls a Worker's queryable function
// * postMessage(string or JSON Data): see Worker.prototype.postMessage()
// * terminate(): terminates the Worker
// * addListener(name, function): adds a listener
// * removeListener(name): removes a listener
// QueryableWorker instances properties:
// * defaultListener: the default listener executed only when the Worker calls the postMessage() function directly
function QueryableWorker(url, defaultListener, onError) {
const worker = new Worker(url);
const listeners = {};
this.defaultListener = defaultListener ?? (() => {});
if (onError) {
worker.onerror = onError;
this.postMessage = (message) => {
worker.postMessage(message);
};
this.terminate = () => {
worker.terminate();
};
this.addListener = (name, listener) => {
listeners[name] = listener;
};
this.removeListener = (name) => {
delete listeners[name];
};
// This functions takes at least one argument, the method name we want to query.
// Then we can pass in the arguments that the method needs.
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
if (!queryMethod) {
throw new TypeError(
"QueryableWorker.sendQuery takes at least one argument",
);
worker.postMessage({
queryMethod,
queryMethodArguments,
});
};
worker.onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethodListener") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
listeners[event.data.queryMethodListener].apply(
this,
event.data.queryMethodArguments,
);
} else {
this.defaultListener(event.data);
};
// your custom "queryable" worker
const myTask = new QueryableWorker("my_task.js");
// your custom "listeners"
myTask.addListener("printStuff", (result) => {
document
.getElementById("firstLink")
.parentNode.appendChild(
document.createTextNode(`The difference is ${result}!`),
);
});
myTask.addListener("doAlert", (time, unit) => {
alert(`Worker waited for ${time} ${unit} :-)`);
});
document.getElementById("first-action").addEventListener("click", () => {
myTask.sendQuery("getDifference", 5, 3);
});
document.getElementById("second-action").addEventListener("click", () => {
myTask.sendQuery("waitSomeTime");
});
document.getElementById("terminate").addEventListener("click", () => {
myTask.terminate();
});
my_task.js
(the worker):
js
const queryableFunctions = {
// example #1: get the difference between two numbers:
getDifference(minuend, subtrahend) {
reply("printStuff", minuend - subtrahend);
},
// example #2: wait three seconds
waitSomeTime() {
setTimeout(() => {
reply("doAlert", 3, "seconds");
}, 3000);
},
};
// system functions
function defaultReply(message) {
// your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly
// do something
function reply(queryMethodListener, ...queryMethodArguments) {
if (!queryMethodListener) {
throw new TypeError("reply - not enough arguments");
postMessage({
queryMethodListener,
queryMethodArguments,
});
onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, "queryMethod") &&
Object.hasOwn(event.data, "queryMethodArguments")
) {
queryableFunctions[event.data.queryMethod].apply(
self,
event.data.queryMethodArguments,
);
} else {
defaultReply(event.data);
};
It is possible to switch the content of each mainpage -> worker and worker -> mainpage message. And the property names "queryMethod", "queryMethodListeners", "queryMethodArguments" can be anything as long as they are consistent in
QueryableWorker
and the
worker
Passing data by transferring ownership (transferable objects)
Modern browsers contain an additional way to pass certain types of objects to or from a worker with high performance.
Transferable objects
are transferred from one context to another with a zero-copy operation, which results in a vast performance improvement when sending large data sets.
For example, when transferring an
ArrayBuffer
from your main app to a worker script, the original
ArrayBuffer
is cleared and no longer usable. Its content is (quite literally) transferred to the worker context.
js
// Create a 32MB "file" and fill it with consecutive values from 0 to 255 – 32MB = 1024 * 1024 * 32
const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
Sharing data
The
SharedArrayBuffer
object allows two threads, such as the worker and the main thread, to simultaneously operate on the same memory span and exchange data without going through the messaging mechanism. Using shared memory does come with significant determinism, security, and performance concerns, some of which are outlined in the
JavaScript execution model
article.
Embedded workers
There is not an "official" way to embed the code of a worker within a web page, like