Пошук уроків, статей та іншого контенту
Обмінюватиметеся даними між головним потоком і Web Worker, не блокуючи інтерфейс.
JavaScript у браузері зазвичай виконується в головному потоці. Цей самий потік:
обробляє події користувача;
оновлює DOM;
виконує JavaScript-код;
перераховує стилі та готує кадри для відображення.
Якщо виконати в ньому тривале обчислення, інтерфейс перестане реагувати:
const startedAt = performance.now();
while (performance.now() - startedAt < 5000) {
// Імітація важкого обчислення
}
console.log('Обчислення завершено');Протягом п’яти секунд браузер не зможе нормально обробляти кліки, прокручування та перемальовування сторінки.
Web Worker — це окремий потік JavaScript, у якому можна виконувати обчислення незалежно від головного потоку.
Важливо:
Worker не має прямого доступу до DOM;
Worker не може напряму змінювати елементи сторінки;
дані передаються через повідомлення;
звичайні об’єкти передаються з копіюванням;
деякі типи даних можна передавати без копіювання.
Найпоширеніший тип — Dedicated Worker. Він належить одному екземпляру сторінки.
Створення Worker:
const worker = new Worker('./worker.js');Обмін повідомленнями:
worker.postMessage({
type: 'calculate',
value: 42
});
worker.addEventListener('message', (event) => {
console.log('Результат від Worker:', event.data);
});У файлі Worker використовується глобальний об’єкт self:
self.addEventListener('message', (event) => {
const { type, value } = event.data;
if (type === 'calculate') {
self.postMessage(value * 2);
}
});event.data містить дані повідомлення, а postMessage() надсилає повідомлення іншій стороні.
Розглянемо застосунок, який шукає прості числа в заданому діапазоні. Worker:
отримує діапазон;
виконує обчислення частинами;
надсилає прогрес;
повертає результат;
підтримує скасування поточного завдання.
Структура файлів:
index.html
main.js
prime-worker.jsindex.html<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Прості числа у Web Worker</title>
</head>
<body>
<h1>Пошук простих чисел</h1>
<label>
Верхня межа:
<input id="limit" type="number" min="2" value="1000000">
</label>
<button id="start">Почати</button>
<button id="cancel" disabled>Скасувати</button>
<p id="status">Очікування</p>
<pre id="result"></pre>
<script src="./main.js"></script>
</body>
</html>prime-worker.jslet currentJob = null;
self.addEventListener('message', (event) => {
const message = event.data;
if (message.type === 'start') {
startJob(message);
return;
}
if (message.type === 'cancel') {
if (currentJob && currentJob.id === message.id) {
currentJob.cancelled = true;
}
}
});
function startJob({ id, limit }) {
// Скасовуємо попереднє завдання, якщо воно ще виконується.
if (currentJob) {
currentJob.cancelled = true;
}
currentJob = {
id,
limit,
current: 2,
primes: [],
cancelled: false
};
processChunk(currentJob);
}
function processChunk(job) {
if (job.cancelled) {
self.postMessage({
type: 'cancelled',
id: job.id
});
if (currentJob === job) {
currentJob = null;
}
return;
}
const chunkSize = 10_000;
const chunkEnd = Math.min(job.current + chunkSize, job.limit + 1);
for (; job.current < chunkEnd; job.current += 1) {
if (isPrime(job.current)) {
job.primes.push(job.current);
}
}
const progress = Math.round(
((job.current - 2) / (job.limit - 1)) * 100
);
self.postMessage({
type: 'progress',
id: job.id,
progress: Math.min(progress, 100)
});
if (job.current <= job.limit) {
// Передаємо керування циклу подій Worker між частинами обчислення.
setTimeout(() => processChunk(job), 0);
return;
}
self.postMessage({
type: 'done',
id: job.id,
primes: job.primes
});
if (currentJob === job) {
currentJob = null;
}
}
function isPrime(number) {
if (number < 2) {
return false;
}
if (number === 2) {
return true;
}
if (number % 2 === 0) {
return false;
}
for (let divisor = 3; divisor * divisor <= number; divisor += 2) {
if (number % divisor === 0) {
return false;
}
}
return true;
}main.jsconst limitInput = document.querySelector('#limit');
const startButton = document.querySelector('#start');
const cancelButton = document.querySelector('#cancel');
const statusElement = document.querySelector('#status');
const resultElement = document.querySelector('#result');
const worker = new Worker('./prime-worker.js');
let activeJobId = 0;
startButton.addEventListener('click', () => {
const limit = Number(limitInput.value);
if (!Number.isInteger(limit) || limit < 2) {
statusElement.textContent = 'Введіть ціле число не менше 2';
return;
}
activeJobId += 1;
worker.postMessage({
type: 'start',
id: activeJobId,
limit
});
startButton.disabled = true;
cancelButton.disabled = false;
resultElement.textContent = '';
statusElement.textContent = 'Прогрес: 0%';
});
cancelButton.addEventListener('click', () => {
worker.postMessage({
type: 'cancel',
id: activeJobId
});
statusElement.textContent = 'Скасування...';
cancelButton.disabled = true;
});
worker.addEventListener('message', (event) => {
const message = event.data;
// Ігноруємо відповіді від застарілих завдань.
if (message.id !== activeJobId) {
return;
}
if (message.type === 'progress') {
statusElement.textContent = `Прогрес: ${message.progress}%`;
return;
}
if (message.type === 'cancelled') {
statusElement.textContent = 'Обчислення скасовано';
startButton.disabled = false;
cancelButton.disabled = true;
return;
}
if (message.type === 'done') {
statusElement.textContent =
`Знайдено простих чисел: ${message.primes.length}`;
resultElement.textContent = message.primes.join(', ');
startButton.disabled = false;
cancelButton.disabled = true;
}
});
worker.addEventListener('error', (event) => {
statusElement.textContent = `Помилка Worker: ${event.message}`;
startButton.disabled = false;
cancelButton.disabled = true;
});
worker.addEventListener('messageerror', () => {
statusElement.textContent = 'Не вдалося десеріалізувати повідомлення';
});
window.addEventListener('beforeunload', () => {
// Завершуємо Worker перед знищенням сторінки.
worker.terminate();
});Для запуску відкрийте index.html через локальний HTTP-сервер. Наприклад, у каталозі з файлами:
python3 -m http.server 8000Після цього відкрийте сторінку на порту 8000.
Навіть у Worker важкі операції можуть блокувати сам Worker. Якщо Worker обробляє одне дуже довге синхронне завдання, він не зможе вчасно приймати нові повідомлення, зокрема повідомлення про скасування.
Тому обчислення розбивають на частини:
function processChunk() {
// Обробка обмеженої кількості даних
setTimeout(processChunk, 0);
}Це дає Worker можливість:
обробити повідомлення про скасування;
надсилати актуальний прогрес;
не блокувати власний цикл подій на невизначений час.
У прикладі ідентифікатор id потрібен для розрізнення завдань. Це важливо, якщо користувач швидко запускає кілька обчислень поспіль: результати старого завдання не повинні перезаписати стан нового.
За замовчуванням postMessage() використовує алгоритм structured clone. Він створює копію більшості стандартних структур даних:
рядків;
чисел;
масивів;
звичайних об’єктів;
Map;
Set;
Date;
RegExp;
ArrayBuffer, якщо його не передано як transferable.
Приклад:
worker.postMessage({
numbers: [1, 2, 3],
metadata: {
source: 'form',
version: 1
}
});Worker отримує логічно незалежну копію. Зміна отриманого об’єкта не змінює оригінал у головному потоці.
Не можна передати таким способом, наприклад:
DOM-елемент;
функцію;
об’єкт із деякими зовнішніми ресурсами;
WeakMap;
WeakSet.
Якщо дані не можна серіалізувати, браузер може згенерувати помилку під час надсилання або отримання повідомлення.
Копіювання великих масивів може бути дорогим. Для деяких об’єктів можна передати право власності замість копії. Такі об’єкти називають transferable objects.
Найпоширеніший приклад — ArrayBuffer:
const worker = new Worker('./worker.js');
const buffer = new ArrayBuffer(1024 * 1024);
const bytes = new Uint8Array(buffer);
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = index % 256;
}
worker.postMessage(
{
type: 'process-buffer',
buffer
},
[buffer]
);
// Після передавання buffer стає від'єднаним у цьому потоці.
console.log(buffer.byteLength); // 0Другий аргумент postMessage() — список об’єктів, право власності на які потрібно передати.
У Worker:
self.addEventListener('message', (event) => {
if (event.data.type !== 'process-buffer') {
return;
}
const bytes = new Uint8Array(event.data.buffer);
let sum = 0;
for (const byte of bytes) {
sum += byte;
}
self.postMessage({
type: 'buffer-result',
sum
});
});Після передавання ArrayBuffer головний потік більше не може використовувати його вміст. Це не копіювання, а переміщення права власності.
До transferable objects також належать, зокрема:
MessagePort;
ImageBitmap;
деякі типи потоків і буферів, підтримка яких залежить від API браузера.
ArrayBuffer і typed arraysUint8Array, Float32Array та інші typed arrays є представленнями над буфером. Передавати потрібно саме buffer:
const values = new Float32Array([1.5, 2.5, 3.5]);
worker.postMessage(
{
type: 'values',
buffer: values.buffer
},
[values.buffer]
);У Worker:
self.addEventListener('message', (event) => {
if (event.data.type !== 'values') {
return;
}
const values = new Float32Array(event.data.buffer);
const doubled = values.map((value) => value * 2);
self.postMessage(
{
type: 'result',
buffer: doubled.buffer
},
[doubled.buffer]
);
});Такий підхід особливо корисний для:
обробки аудіоданих;
роботи із зображеннями;
числових симуляцій;
машинного навчання в браузері;
великих бінарних файлів.
Worker можна завершити двома способами.
worker.terminate();Метод негайно завершує Worker. Поточне обчислення буде перервано, а невідправлені повідомлення можуть бути втрачені.
self.close();Це корисно для Worker, який виконує одноразове завдання:
self.addEventListener('message', (event) => {
const result = performCalculation(event.data);
self.postMessage(result);
self.close();
});У довгоживучому Worker зазвичай краще реалізовувати протокол скасування, як у прикладі з простими числами, а не безумовно викликати terminate().
Помилки Worker не потрапляють автоматично в try...catch головного потоку. Потрібно слухати події Worker:
worker.addEventListener('error', (event) => {
console.error('Помилка у Worker:', event.message);
});
worker.addEventListener('messageerror', (event) => {
console.error('Помилка отримання повідомлення:', event);
});error сигналізує про помилку під час виконання Worker. messageerror виникає, коли браузер не може десеріалізувати повідомлення.
У самому Worker локальні помилки можна обробляти через try...catch і передавати уніфікований результат:
self.addEventListener('message', (event) => {
try {
const result = performCalculation(event.data);
self.postMessage({
type: 'success',
result
});
} catch (error) {
self.postMessage({
type: 'failure',
message: error instanceof Error
? error.message
: 'Невідома помилка'
});
}
});Це дозволяє відокремити помилки бізнес-логіки від аварійного завершення Worker.
Worker за замовчуванням завантажує classic script. Для використання import можна створити module Worker:
const worker = new Worker('./worker.js', {
type: 'module'
});У worker.js можна використовувати імпорти:
import { calculate } from './calculation.js';
self.addEventListener('message', (event) => {
const result = calculate(event.data);
self.postMessage(result);
});Module Worker зручний для великих проєктів, де логіку потрібно розділяти на окремі модулі.
Важливо, щоб шляхи до Worker і його модулів були коректними, а застосунок запускався в середовищі, де браузер дозволяє завантаження таких ресурсів. Для розробки краще використовувати локальний HTTP-сервер, а не відкривати HTML-файл напряму через file://.
Worker має власне глобальне оточення, але не має доступу до DOM.
У Worker не можна безпосередньо виконати:
document.querySelector('#app');
window.alert('Привіт');Замість цього Worker повинен повернути результат:
self.postMessage({
type: 'update',
value: 42
});А головний потік змінює DOM:
worker.addEventListener('message', (event) => {
document.querySelector('#app').textContent = event.data.value;
});У Worker доступні багато API, які не залежать від DOM, зокрема:
fetch;
WebSocket;
Web Crypto;
таймери;
URL;
console;
IndexedDB.
Конкретна доступність API залежить від типу Worker і браузера.
Structured clone і transferable objects не створюють спільного змінного стану між потоками. Іноді це необхідно, наприклад для високопродуктивних числових алгоритмів.
Для спільної пам’яті використовують:
SharedArrayBuffer;
typed arrays;
Atomics.
Приклад атомарного збільшення значення:
const sharedBuffer = new SharedArrayBuffer(
Int32Array.BYTES_PER_ELEMENT
);
const sharedValues = new Int32Array(sharedBuffer);
worker.postMessage({
type: 'shared-memory',
buffer: sharedBuffer
});
Atomics.add(sharedValues, 0, 1);У Worker:
self.addEventListener('message', (event) => {
if (event.data.type !== 'shared-memory') {
return;
}
const values = new Int32Array(event.data.buffer);
Atomics.add(values, 0, 10);
self.postMessage({
type: 'shared-result',
value: Atomics.load(values, 0)
});
});Atomics потрібні для синхронізації доступу до спільної пам’яті. Просте читання та запис без синхронізації може призвести до некоректних результатів у конкурентному коді.
Використання SharedArrayBuffer має додаткові вимоги безпеки та конфігурації ізоляції документа. Тому для більшості прикладних задач краще спочатку використовувати звичайні повідомлення або transferable objects.
Worker має власні витрати:
запуск окремого середовища JavaScript;
серіалізація або передавання даних;
синхронізація результатів із головним потоком;
складніша обробка стану та помилок.
Не варто переносити у Worker кожну операцію. Він доречний для:
обробки великих масивів;
складних математичних обчислень;
парсингу великих файлів;
кодування та декодування даних;
обробки зображень;
криптографічних операцій;
симуляцій і пошуку по великих просторах станів.
Короткі операції зазвичай швидше виконати в головному потоці, ніж передавати їх у Worker.
Для складних Worker корисно формалізувати повідомлення. Наприклад:
{
type: 'start',
id: 17,
payload: {
limit: 1000000
}
}Відповіді можуть мати однакову форму:
{
type: 'progress',
id: 17,
progress: 42
}{
type: 'done',
id: 17,
result: [...]
}{
type: 'error',
id: 17,
message: 'Некоректний діапазон'
}Такий протокол допомагає:
пов’язувати відповідь із конкретним запитом;
підтримувати кілька типів операцій;
відкидати застарілі результати;
відображати прогрес;
реалізувати скасування;
будувати обгортку на основі Promise.
Worker не має доступу до document і елементів сторінки. Він повинен обчислити результат і передати його через postMessage().
Функції, елементи DOM та інші несеріалізовані значення не можна передати через structured clone. Передавайте прості дані, об’єкти, масиви або буфери.
Передавання великого масиву без transfer може створити копію та збільшити використання пам’яті. Для ArrayBuffer використовуйте список transferable objects.
Після такого виклику:
worker.postMessage({ buffer }, [buffer]);буфер у поточному потоці стає від’єднаним. Не можна продовжувати читати його як звичайний ArrayBuffer.
Кнопка «Скасувати» не зупинить синхронний цикл Worker автоматично. Обчислення потрібно розбити на частини та регулярно перевіряти прапорець скасування.
Якщо можна запустити кілька операцій, кожна з них повинна мати id. Інакше пізня відповідь від старого завдання може зіпсувати стан інтерфейсу.
Постійне створення та завершення Worker може бути повільнішим за виконання невеликого обчислення в головному потоці. Для серії операцій доцільно повторно використовувати один Worker.
Worker не замінює головний потік. Він виконує обчислення, а всі зміни DOM, доступність інтерфейсу та візуальні оновлення залишаються відповідальністю головного потоку.
Web Worker виконує JavaScript в окремому потоці та допомагає не блокувати інтерфейс.
Головний потік і Worker обмінюються даними через postMessage() і подію message.
Worker не має прямого доступу до DOM.
За замовчуванням дані передаються через structured clone.
Великі ArrayBuffer можна передавати без копіювання за допомогою transferable objects.
Для довгих операцій корисно надсилати прогрес і підтримувати скасування.
Ідентифікатори завдань захищають від обробки застарілих результатів.
Помилки потрібно обробляти через error, messageerror та власний протокол повідомлень.
SharedArrayBuffer і Atomics призначені для спеціалізованих сценаріїв зі спільною пам’яттю.
Worker доцільно використовувати лише для справді важких або тривалих обчислень.