Parallel computing and performance improvement
In today’s world, users are becoming increasingly demanding of website performance and a good user experience comes to the fore. Even the slightest hang or lack of smoothness can result in losing users.
There are cases where this problem can be solved using Web Workers, which I will tell you about later!
Contents
What are Web Workers?
Web Workers – Provides a simple means to run scripts in a background thread. A Worker thread can perform tasks without interfering with the user interface.
Has access to Navigator, XMLHttpRequest, Array, Date, Math, and String, setTimeout(), setInterval().
It has the following limitations, lack of access to the DOM, instead of window — the global object self, no access to cookies/localStorage/sessionStorage, some browser APIs are also unavailable, such as access to the camera/microphone. They also have resource limitations from the browser itself.Web Workers also have their own event loop, but it functions a little differently than the main thread.
In Web Workers, there is a single thread of execution that is used to handle all tasks, including events, messages, and code execution. WorkerGlobalScope. It works asynchronously and executes code in response to messages and events.
If it’s a little simpler, then Web Workers is a script that we can run in parallel with the main thread and perform some operations that do not block the main thread and do not interfere with the user’s interaction with our page.
I will talk about two types of Worker:
Dedicated Worker – Worker that creates separate and an isolated execution context running in parallel with the main thread in the application. Mainly used to perform calculations, data processing and time-intensive tasks that do not block the main thread.
Shared Worker – Worker that creates general execution context available to multiple windows, tabs, or program frames. It is primarily used to execute code in the background and provides shared access to data and state between different parts of an application, making it particularly useful in scenarios where multiple users or components must share data and interact with each other.
How to work with Dedicated Workers?
All interaction takes place using a function postMessage()
and listener onmessage
, Then we will consider them in detail. The general workflow looks like this:
-
We initialize the Dedicated Worker using the constructor
-
We do
postMessage
from Main Thread -
The listener in the Worker Thread is triggered
-
The Worker executes the logic you wrote
-
Worker with help
postMessage
sends the event back to the Main Thread -
Let’s activate the listener in the Main Thread
We can create as many streams as we want (while each of them will have a different context), the main thing is that there are enough PC resources and we do not rely on browser limitations.
Dedicated Worker initialization
To initialize the Worker instance, we throw in the constructor the path to the file of our Worker file
// new Worker('Путь до worker файла, относительно текущего файла')
const worker = new Worker('worker.js');
We have received the Worker, then we will consider the main functions of data exchange between threads
postMessage(message: any, transfer: Transferable[]): void
— a method for sending a message from one thread to another.
-
message – any value or object that can be processed by the structural cloning algorithm, in short, this algorithm is more advanced than the JSON serializer, for example it can clone – Blob, File, ImageData, Buffers, can restore circular references, but cannot in cloning properties and prototypes and works with Error, Function, DOM Elements.
-
transfer – an array of objects (objects can only be ArrayBuffer | MessagePort | ImageBitmap), which will be moved to the worker context and will no longer be available in the original thread, this can help when copying a large amount of data so as not to lose performance and memory.
// Тут мы передаем buffer в контекст worker, в этом скрипте он больше не будет доступен
const buffer = new ArrayBuffer(42);
const data = { text: 'Hello, World!', buffer };
worker.postMessage(data, [buffer]);
onmessage: ((this: Worker, event: MessageEvent) => any) | null
– message listener.
interface MessageEvent<T = any> extends Event {
// Переданные данные
readonly data: T;
// Последний идентификатор события (event ID) в случае событий, связанных с сервером
readonly lastEventId: string;
// Origin сообщения, используется, при работе с событиями связанными cross-document messaging, и позволяет определить источник отправителя сообщения.
readonly origin: string;
// Порты, по сути, открытые нами страницы, используются для обмена данными и сообщениями между веб-воркерами и основными потоками.
readonly ports: ReadonlyArray<MessagePort>;
// Предоставляет информацию об отправителе сообщения, такую как, например, какое окно отправило событие
readonly source: MessageEventSource | null;
}
Example of use
I will show an example of using Dedicated Worker using an example of working with an image (the example is as abstract as possible, without specific implementations, but demonstrates some possibilities).
Let’s say you’re writing something like google docs and want to compress an image if it’s larger than a certain size and at the same time not block the main stream.
In the main flow, we handle the event of the user selecting an image file, send it to the Worker, and when the processed image arrives from the Worker, we add this image to the page.
index.js
const imageProcessingWorker = new Worker('worker.js');
const imageSelect = document.getElementById('image-select');
imageSelect.addEventListener('change', function(event) {
const selectedImage = event.target.files[0];
imageProcessingWorker.postMessage(selectedImage);
});
imageProcessingWorker.onmessage = function(event) {
const processedImage = event.data;
const imageContainer = document.getElementById('image-container');
imageContainer.appendChild(processedImage);
};
worker.js
self.onmessage = function(event) {
const image = event.data;
// Функция, которая производит какие-то преобразования с картинкой, например сжатие
const processedImage = processImage(image)
self.postMessage(processedImage);
};
Killing a Dedicated Worker
-
When you no longer need a Dedicated Worker, you can kill it with help
worker.terminate()
. -
The Dedicated Worker will self-destruct when you close the tab with it.
Use Cases
-
Video/audio/picture processing – resizing, applying filters and encoding/decoding media data, etc.
-
Download, process and save large files.
-
3D graphics and various animation animations.
-
Mapping big data like lists/different sorts etc.
You can search for Workers on sites for your own interest, for example, using devtools: sources → threads → smth with workers.js
Shared Worker works in a similar way to Dedicated Worker, but here all interaction goes through port: MessagePort,
and accordingly, because of this, we have a listener onconnect
in the Worker file. The general workflow looks like this:
-
We initialize the Shared Worker using the constructor in our files (in this example, there are 2 of them)
-
We get the port of our Shared Worker
-
We do
port.postMessage()
with Main Threads -
We establish connect with Main Threads with Worker Thread, using
onconnect
-
We receive the port from the event that came to us on the connection, I am currently considering the case when I have 1 port.
const port = event.ports[0];
if you have more, choose the appropriate one (ports are created as follows – tick) -
Worker with help
port.postMessage
sends the event back to the Main Thread’s to all ports on which it hangsport.onmessage
-
Triggers listeners in Main Thread’s
We can create as many streams as we want (and if we create them from the same Worker file, they will have the same context), the main thing is that there are enough PC resources and we do not rely on browser limitations.
Initialization of Shared Worker
// new Worker('Путь до worker файла, относительно текущего файла')
const worker = new SharedWorker('worker.js');
// тут у нас в worker есть объект port он используется для управления Shared Worker
SharedWorker has the same function signatures for postMessage
and listener onmessage,
and onconnect
has the same sigature as onmessage
postMessage(message: any, transfer: Transferable[]): void
onmessage: ((this: Worker, event: MessageEvent) => any) | null
onconnect: ((this: Worker, event: MessageEvent) => any) | null
Example of use
I will show an example of using Shared Worker, let’s make a form where you can kill a message and it will appear on the page and with the help of our Worker we will display it on two pages at once (index1.html, index2.html). Open both pages to appreciate.
We initialize our Shared Worker’s in index1.html, index2.html, where
index1.html
-
We initialize the Shared Worker and take its port
-
When you click the Send button, we send a Shared Worker message using
port.postMessage()
-
We create a handler
onmessage
in it we add a new line to our message container
index2.html
-
We initialize the Shared Worker and take its port
-
We create a handler
onmessage
in it, add a new line to our container with messages (we do not make our form here, there will only be a list of messages)
worker.js
-
We create an array where we will put all our ports.
ports
(we need these ports to send a message at once to all tabs/iframes where our Worker is used and display a new message there) -
We create a handler
onmessage
and send messages to all ourports
-
Voila, we get the same on both pages
messages
index1.html
<!DOCTYPE html>
<html>
<head>
<title>Shared Worker 1</title>
</head>
<body>
<div class="message-container"></div>
<input type="text" class="message-input" />
<button class="send-message-button">Send</button>
<script>
const messageContainer = document.querySelector('.message-container');
const messageInput = document.querySelector('.message-input');
const sendMessageButton = document.querySelector('.send-message-button');
const worker = new SharedWorker('worker.js');
const port = worker.port;
sendMessageButton.addEventListener('click', () => {
const message = messageInput.value;
port.postMessage(message);
messageInput.value="";
});
port.onmessage = (e) => {
messageContainer.innerHTML += e.data + '<br>';
};
</script>
</body>
</html>
index2.html
<!DOCTYPE html>
<html>
<head>
<title>Shared Worker 2</title>
</head>
<body>
<div class="message-container"></div>
<script>
const messageContainer = document.querySelector('.message-container');
const worker = new SharedWorker('worker.js');
const port = worker.port;
port.onmessage = (e) => {
messageContainer.innerHTML += e.data + '<br>';
};
</script>
</body>
</html>
worker.js
const ports = [];
self.onconnect = (event) => {
// Достаем порт с которого подключились и сохраняем его, чтобы потом отправить ему сообщение
const port = event.ports[0];
ports.push(port);
port.onmessage = (e) => {
const message = e.data;
for (const client of ports) {
client.postMessage(`Message: ${message}`);
}
};
};
Killing Shared Worker
-
By using
worker.close()
-
When all tabs on which this Shared Worker was used have been closed
Use Cases
-
Everything related to data exchange between tabs and program windows.
-
Management of common resources.
-
Everything is the same as in Dedicated Workers
Together
Shared Workers and Dedicated Workers are powerful tools for improving application performance. Shared Workers are suitable for sharing data and managing shared resources between different parts of an application, while Dedicated Workers help provide code isolation and speed up computations. The choice between them depends on the specific tasks of your project. Try it!
If the article seemed interesting to you, then I have it Telegram Channelwhere I write about new technologies at the front, share good books and interesting articles by other authors.