What is a Web Worker?
A web worker is an API that allows code to run outside of the main thread. This means that lengthy or complex calculations performed on workers do not block the user interface (UI).
A web worker is created in the main thread. When creating the worker, a URL of the script is passed to it. Once loaded, a separate thread is created in which the worker's script will execute.
The script will have its own context, different from the window
context of the main thread. In the main thread, the global context is bound to the window
variable, while in the worker, it is bound to the self
variable. The execution context of the web worker WorkerGlobalScope is different from that of the main thread. It does not have access to the document
object or DOM API
.
Execution Thread Features
The thread in which the worker's code runs is isolated from the main thread. In Chromium, each of these threads corresponds to its own instance of the JavaScript engine. Because of this, creating a new worker is considered a "heavy" operation. It is typically assumed that there will be a few workers, and they will live for a long time.
Threads can communicate with each other through message sending. Use the post
function to send messages.
How to Create and Run?
It's simple: name the constructor Worker
and pass the URL of the JavaScript file to it.
// window context app.jsconst worker = new Worker('worker.js')
// window context app.js const worker = new Worker('worker.js')
The worker uses the message mechanism to communicate with the main thread. The post
method is used to send messages.
// Main thread: app.jsconst worker = new Worker('worker.js')// Sending a message from the main thread to the workerworker.postMessage({ message: '415, this is base, respond' })
// Main thread: app.js const worker = new Worker('worker.js') // Sending a message from the main thread to the worker worker.postMessage({ message: '415, this is base, respond' })
In the global context of the worker, there is an onmessage
handler. It can be used to receive messages. The worker can also send messages back to the main thread using the post
function. The function can be called anywhere within the worker.
// Worker: worker.jsonmessage = function (e) { // Listening for messages from the main thread if (e.message === '415, this is base, respond') { {/* Sending a message from the worker to the main thread */} postMessage('Base, this is 415, how do you hear me?') }}
// Worker: worker.js onmessage = function (e) { // Listening for messages from the main thread if (e.message === '415, this is base, respond') { {/* Sending a message from the worker to the main thread */} postMessage('Base, this is 415, how do you hear me?') } }
To receive messages in the main thread, use the onmessage
handler method of the Worker
object.
// window context app.jsconst worker = new Worker('worker.js')worker.postMessage({ message: '415, this is base, respond' })worker.onmessage = function (e) { // Listening for messages from the worker console.log(e) // Base, this is 415, how do you hear me?}
// window context app.js const worker = new Worker('worker.js') worker.postMessage({ message: '415, this is base, respond' }) worker.onmessage = function (e) { // Listening for messages from the worker console.log(e) // Base, this is 415, how do you hear me? }
The careful reader will note that an object with the message
property went to the worker, while a string returned from the worker. The post
function can take values of any type, including objects. The only limitation is that the data being sent must support the structured cloning algorithm.
What is Available Inside?
It was previously mentioned that many APIs from the window
object of the main thread are not available in the execution context of the worker. So what is available? Let's list some API functions that are commonly used: fetch
, set
, set
, request
, and queue
. For the curious — a complete list of supported APIs.
Types of Workers
In the example above, we explored the first type of worker — Dedicated Worker
. It will only be available in the thread that created it. This can be the main thread or the thread of another worker. But what if we want to use a worker across different browser tabs? For that, we use another type of worker — Shared Worker
.
Shared Workers allow the creation of a thread that is shared among several tabs, iframes, or windows under the same origin. This means that a Shared Worker can be simultaneously used by multiple parts of a web application for data exchange, state synchronization, or background task execution without the need for reloading or duplication in each tab or window. It is worth noting that the state of a Shared Worker will remain alive as long as it is remembered by someone.
Let's see how to create and run a Shared Worker.
The logic is similar to that of Dedicated Worker
, but there are a few exceptions. First, to create a Shared
, you need to use the Shared
constructor. Second, onmessage
and post
are available in the worker's port
property:
// First tab: app1.jsconst sharedWorker = new SharedWorker('worker.js')sharedWorker.port.onmessage = (event) => { console.log('data from worker', event)}const sendDataToWorker = () => { sharedWorker.port.postMessage(1)}
// First tab: app1.js const sharedWorker = new SharedWorker('worker.js') sharedWorker.port.onmessage = (event) => { console.log('data from worker', event) } const sendDataToWorker = () => { sharedWorker.port.postMessage(1) }
The same is done in another tab:
// Second tab: app2.jsconst sharedWorker = new SharedWorker('worker.js')sharedWorker.port.onmessage = (event) => { console.log('data from worker', event)}const sendDataToWorker = () => { sharedWorker.port.postMessage(2)}
// Second tab: app2.js const sharedWorker = new SharedWorker('worker.js') sharedWorker.port.onmessage = (event) => { console.log('data from worker', event) } const sendDataToWorker = () => { sharedWorker.port.postMessage(2) }
The SharedWorker code looks like this:
// Worker: worker.jslet sum = 0onconnect = (connect) => { const port = connect.ports[0] // There is always one element in ports port.onmessage = (event) => { sum += event } port.postMessage(sum)}
// Worker: worker.js let sum = 0 onconnect = (connect) => { const port = connect.ports[0] // There is always one element in ports port.onmessage = (event) => { sum += event } port.postMessage(sum) }
The onconnect
event handler receives the event (we call it connect
). Inside the handler, the ports
property is used — an array that will always contain one element. By using the onmessage
property of the port object, you can listen for messages from other threads. Sending a message also happens through the port
.
Nesting Web Workers
Workers can be nested, and one worker can manage another worker. Working with nested workers is no different from working with workers in the main thread.
Imports in Web Workers
Starting in June 2023, nearly all browsers support ES module imports in the worker context. Therefore, you can use the construct import xxxxx from ‘lib’
. This information will be useful for you when setting up the application build.
Sending Data to a Web Worker
Data passed to post
is by default copied, which can be slow, especially when transferring large or complex objects.
Sending Data Without Copying
To optimize performance and minimize the copying cost of data, you can use the technique of data transfer through Transferable objects. Transferable objects are not copied but are moved between contexts. After sending, the Transferable object disappears from where it was sent. Examples of transferable objects include Array
and Message
.
Example of Using Transferable Objects
Sending data to a web worker:
// Creating ArrayBufferconst buffer = new ArrayBuffer(1024) // 1024 bytes// Sending ArrayBuffer to the workerworker.postMessage(buffer, [buffer])// Now buffer is not accessible in the main thread
// Creating ArrayBuffer const buffer = new ArrayBuffer(1024) // 1024 bytes // Sending ArrayBuffer to the worker worker.postMessage(buffer, [buffer]) // Now buffer is not accessible in the main thread
Receiving data in the worker:
onmessage = function(e) { const buffer = e.data // Receiving ArrayBuffer // You can start working with the data};
onmessage = function(e) { const buffer = e.data // Receiving ArrayBuffer // You can start working with the data };
In this example, the object of type Array
is sent to the worker via post
, and the array with this object is also passed as the second argument as a transferable object.
Please note:
- After transferring a transferable object, the source loses access to the object. This means that the object cannot be used in the source after it has been sent.
- Not all types can be transferred as transferable objects.
Array
andBuffer Message
can certainly be moved.Port
Transferable objects work fast, as they avoid deep copying. This is especially noticeable when working with large or complex objects in applications that require high performance, such as games, graphic editors, and real-time audio and video processors.
Conclusion
Workers are a powerful tool for developing more responsive and efficient web applications. They help to offload the main thread from heavy computations, improving the user experience. However, their limitations and features need to be taken into account for effective use in your projects.