Простое объяснение, что такое Webworkeр-ы в JS и как с ними работать
Как ваш JS-код может выполнять несколько задач параллельно в фоне? Для этого существуют Webworker-ы, позволяющие реализовать такой функционал. В этой статье я расскажу, как с ними работать, познакомлю вам с Webworkers API, и покажу, как в JS выполнять задачи в фоне, в отрезе от кода текущей веб-страницы.
Для того, чтобы комфортно понимать суть этой статьи вы должны знать основы JavaScript (события, работа с DOM).
Когда JavaScript был впервые создан, никто особо не беспокоился о производительности этого языка программирования. Его основная задача была запуск небольших скриптов на веб-странице, имея при этом максимально простой синтаксис написания скриптов. Ранее JavaScript был упрощенным скриптовым языком для программистов-любителей. И, изначально, он не задумывался как язык, который решает какие-то сложные бизнес задачи.
Однако, годы шли, и спустя как 25 лет, JavaScript полностью покорил веб (захватив при этом нишу и фронтенда, и бекенда). Спустя какое-то время он стал быть похожим на профессиональный язык программирования, в нём стали появляться новые продвинутые инструменты и функции. Одной из которых и являются Webwork-ы.
Вебворкеры позволяют вам написать скрипт, который выполняет ресурсоёмкую работу в фоне без подвисаний страницы. Например, представьте, что вы хотите сделать какие-то большие, сложные расчёты по клику на определённую кнопку. Если вы запустите расчёт сразу же по клику пользователя, то расчёты незамедлительно начнутся (которые займут несколько, а то и десятки секунд). И в это время, пока скрипт будет работать, ваш поток будет занят, из-за чего вы никак не сможете взаимодействовать со страницей. В некоторых случаях вы даже можете получить уведомление о том, что страница "не отвечает".
В этой случае для сохранения удобства пользования сайтом, ничего не остаётся, кроме как выполнять задачу по этим расчётам в фоне, отораванно от основного сайта. И благодаря этому, пока задача будет выполняться в фоне, пользователь беспрепятственно сможет пользоваться вашим сайтом, не имея при этом "тормозов". Технически - задача выполняется в фоне, в отдельном потоке компьютера.
Полное техническое понимание того, как работают потоки немного более сложная задача, чем мы сегодня обсуждаем. Но основная идея заключается в том, что современная операционная система запускает сотни разных потоков за один раз, обрабатывает информацию в многих из них, и быстро переключается от одного потока к другому, обмениваясь данными. На самом деле, переключение происходит настолько быстро, что кажется, как буд-то ничего не происходит.
Веб-воркеры позволяют вам выполнять любые ресурсоёмкие задачи в фоне. Процесс их работы прост:
- Вы создаёте Веб-воркер.
- Вы описываете, какую задачу Веб-воркер должен выполнять (Например, перебор большого набора данных в поисках конкретных значений).
- Запускаете Веб-воркер.
- Когда скрипт закончит работу он сообщит вам, и вы получите результат его работы, описанный скриптом. (К примеру, показать результаты расчёта на странице).
Теперь давайте углубимся!
Создаём ресурсоёмкую задачу
Прежде чем вы увидите преимущества использования Веб-воркеров, сначала напишем код, у которого будут проблемы с производительностью, и который мы будем улучшать путём внедрения в Веб-воркеры. Нет смысла использовать Веб-воркеры для простых и быстрых задач, потому что они не тормозят вашу страницу. Основная их задача - выполнение ресурсоёмких задач.
К примеру, напишем скрипт, который будет искать простые числа и выводить их на странице.
Этот код перебирает все числа, заданные в диапазоне, и выбирает только простые числа. Вы указываете границы чисел для перебора в 2 инпутах. Если выбрать, к примеру, от 1 до 100к, эта задача выполнится за доли секунд, без заметных подтормаживаний. Но, если уже указать границы от 1 до 1кк, вы уже можете заметить явные фризы на странице. В результате чего, страница будет недоступной в течении нескольких секунд, или минут. При этом, вы не сможете кликнуть на какой-то элемент, или как-либо взаимодействовать со страницей.
Производительность этой страницы может быть улучшен за счёт внедрения Веб-воркеров. Но, перед тем, как перейти к ним, рассмотрим подробнее текущий JavaScript код. Прямо сейчас, когда вы нажмёте на кнопку "Поиск", обработчик вызовет функцию doSearch
, код которой показан ниже:
function doSearch() {
// Получаем числа ренджа От и До
var fromNumber = document.getElementById("from").value;
var toNumber = document.getElementById("to").value;
var statusDisplay = document.getElementById("status");
statusDisplay.innerHTML = "Начинаем новый поиск...";
// Выполняем поиск
var primes = findPrimes(fromNumber, toNumber);
// полученные результаты проходим циклом и соединяем их одну строку для публикации в блок
var primeList = "";
for (var i=0; i < primes.length; i++) {
primeList += primes[i];
if (i !== primes.length-1) primeList += ", ";
}
// отображаем найденные числа на странице
var primeContainer = document.getElementById("primeContainer");
primeContainer.innerHTML = primeList;
statusDisplay = document.getElementById("status");
if (primeList.length === 0) {
statusDisplay.innerHTML = "Не найдено ни одного числа.";
} else {
statusDisplay.innerHTML = "Результаты здесь!";
}
}
Этот код совершенно непримечательный. Здесь представлены основы JavaScript: инициализируется цикл на основе полученных значений инпутов со страницы, перебираются значения в поисках нужного числа, производится расчёт, а результат добавляется в <div>
для наглядного просмотра.
Этот код ищет простые числа благодаря другой функции findPrimes()
. Вам не нужно знать дословного определение простого числа, чтобы понять этот пример. Я использую этот пример только потому, что его просто проиллюстрировать и понять, но с вычислительной точки зрения этот код может занять достаточно серьезное время. Если вам интересно понять математическую сторону поиска простых чисел, просто отредактируйте CodePen и посмотрите на функцию findPrimes()
.
Выполнение работы в фоне
Функциональность веб-воркеров сосредоточена вокруг объекта, называемого как Worker
. Когда вы хотите запустить что-то в фоне, вам необходимо создать экземпляр объекта Worker
, которому передать код на выполнение.
Здесь показан пример того, как создать новый веб воркер, код которого расположен в файле под названием PrimeWorker.js
:
var worker = new Worker("PrimeWorker.js");
Код, который выполняет воркер, всегда должен находится в отдельной JavaScript файле. Это требование связано с тем, чтобы не дать новичкам-программистов использовать вызов глобальных переменных, или прямой работы с DOM-ом документа. Ни одна из этих операций невозможна. Почему? Потому что может случиться что-то непредсказуемое, если несколько потоков попытаются работать с одними и теми же данными одновременно. Это значит, что из скрипта воркера вы никак не сможете работать с DOM. Вы не сможете записать простые числа в <div>
элемент. Вместо этого, ваш воркер из кода должен отправлять обработанные данные назад в JavaScript код, из которого воркер был вызван. И уже этот скрипт, получив данные от воркера должен их отобразить.
Веб страницы и воркеры взаимодействуют между собой благодаря обмену сообщениями. Для отправки данных воркеру, вам нужно вызвать метод воркера postMessage()
:
worker.postMessage(myData);
В этот же момент, у воркера сработает событие onMessage
, которое получает эти переданные данные. После чего, воркер начинает работу.
Аналогично, когда воркер закончит обработку данных, ему необходимо сообщить скрипту, который его вызвал, и передать ему отработанные данные. Для этого, уже в коде воркера нужно вызвать его собственный метод postMessage()
, передавая ему нужные данные. После чего, эти данные уже так же, получаем, при срабатывании события onMessage
.
Если вы ранее работали с вебсокетами, то здесь принцип похожий. Клиент общается с сервером, отправляя друг другу сообщения, при наступлении определённых событий.
Есть еще один момент, который нужно рассмотреть перед тем как углубиться. Метод postMessage()
принимает только одно значение в виде аргумента. Этот факт является камнем преткновения для скрипта поиска простых чисел, потому что для работы ему необходимо два параметра (число от которого считать, и до какого). Решение состоит в том, чтобы упаковать эти два параметра в обин объект (что является распространённой практикой для JS-библиотек). Для примера, если бы мы хотели передать жестко заданные числа, мы бы вызвали:
worker.postMessage({
from: 1,
to: 20000
});
Теперь, вооружившись этими знаниями, можем переписать функцию doSearch()
, которая рассматривалась ранее. Теперь, вместо того, чтобы обрабатывать числа прямо на странице, из которой вызывается, создадим веб воркер, который будет выполнять всю грязную работу:
var worker;
function doSearch() {
// Дизейблим кнопку, чтобы пользователь не мог запустить больше чем один поиск в одно время
searchButton.disabled = true;
// Создаём воркер
worker = new Worker("PrimeWorker.js");
// Навешиваемся на событие onMessage, чтобы получать сообщения от воркера
worker.onmessage = receivedWorkerMessage;
// берём значения для ренджа, чтобы передать его воркеру
var fromNumber = document.getElementById("from").value;
var toNumber = document.getElementById("to").value;
worker.postMessage({
from: fromNumber,
to: toNumber
});
// Даём пользователю понять, что происходит в данный момент
statusDisplay.innerHTML = "Веб воркер в поисках числа в границах от ("+ fromNumber + " до " + toNumber + ") ...";
Теперь осталось написать код файла PrimeWorker.js
, который будет делать всю работу. Нужно описать получение входных данных через событие onMessage
, выполнить поиск, после чего вернуть назад ответ со списком простых чисел:
onmessage = function(event) {
// Объект, который веб страница отправила находит в свойстве event.data
var fromNumber = event.data.from;
var toNumber = event.data.to;
// Выполняем поиск по указанному ренджу
var primes = findPrimes(fromNumber, toNumber);
// Поиск закончен, возвращаем результат
postMessage(primes);
};
function findPrimes(fromNumber, toNumber) {
// Скучный алгоритм поиска простых чисел в этой функции
}
После того, как вебворкер вызовет postMessage()
, он создаст событие onMessage
, которое затриггерит функцию receivedWorkerMessage
на веб-странице:
function receivedWorkerMessage(event) {
// Получили список простых чисел
var primes = event.data;
// Добавляем этот список числе в HTML-разметку
// ...
// Разрешаем выполнить новый поиск
searchButton.disabled = false;
}
Структура кода была немного изменена, однако основная логика такая же. Результат, однако, драматически разный. Теперь, когда вы выполняете поиск в числах большого ренджа, страница по-прежнему остаётся отзывчивой, и не подвисающей. Вы свободно можете скроллить вниз, печатать текст в инпуты, указывать, выбирать числа с результатов предыдущего поиска. Попробуйте этот скрипт на Codepen:
Обработка ошибок воркера
Метод postMessage()
является ключом при взаимодействии с веб воркерами. Однако, бывают случаи, когда веб воркер может отработать с ошибкой, и это событие мы так же можем отловить, для получения больше подробностей. Для этого, при создании веб воркера, мы можем определить обработчик при возникновении события ошибки onerrror
:
worker.onerror = function(error) {
console.log(error);
statusDisplay.textContent = error.message;
};
Объект ошибки, передаваемый в обработчик onerror
содержит в себе несколько свойств: message
- текст ошибки, и lineno
, filename
- которые указывают на номер строки и название файла воркера, в котором произошла ошибка.
Отмена выполнения задачи в фоне Вебворкера
После того, как вы изучили пример и поняли, как создавать вебворкеры, можем немного углубиться, и улучшить код. Первое что сделаем - добавим поддержку отмены выполнения задачи, которая позволяет вашей странице остановить выполнение веб-воркера прямо посреди его рабочего процесса.
Для остановки работы воркера существует 2 способа. Первый - вебворкер может остановить выполнение самого себя (изнутри скрипта вебворкера), вызвав метод close()
. Более общий метод, который можно вызвать из страницы, на которой был создан вебворкер - terminate()
, вызвав который работа воркера будет прервана. Вот пример, как вы можете добавить кнопку остановки работы скрипта воркера:
function cancelSearch() {
worker.terminate();
statusDisplay.textContent = "";
searchButton.disabled = false;
}
Нажав на эту кнопку, работа воркера будет остановлена, а кнопка поиска будет переведена в статус enabled
. Только запомните, что остановив Веб Воркер таким способом, вы больше не сможете отправить ему новые сообщения, и не сможете выполнять какие-либо операции с текущим экземпляром воркера. Для того, чтобы запустить работу воркера, необходимо создать новый экземпляр объекта воркера.
Передача сложных сообщений
Последний приём, который мы изучим в этой статье - это получение информации о текущем прогрессе выполнения задачи. Это более сложный и продвинутый приём, потому что вам нужно предусмотреть дополнительную логику, которая будет "стучать" веб-странице при изменении прогресса работы. Иногда это очень полезно в более продвинутых реальных проектах, потому советую не пропускать этот раздел.
Как вы уже знаете, веб воркеры имеют только один способ коммуникации с веб-страницей, с помощью метода postMessage()
. Потому, для этого примера нужно как-то научиться создавать 2 разных типа сообщений, которые будут приходить от веб воркера: сообщение об изменении прогресса (пока задача в работе), и сообщение о результате работы, когда работа скрипта будет окончена (в текущем случае - со списком простых чисел). Теперь, нам нужно изменить обработчик события onMessage
чтобы страница могла читать 2 разных типа сообщений, и обрабатывать их соответственно.
Потому, для этого, добавим дополнительную информацию для каждого отправляемого сообщения из веб воркера. Например, можем добавить в объект сообщения дополнительное поле type
, по которому мы и будем определять тип полученного сообщения. Когда веб воркер отправляет информацию о прогрессе, будет передавать type: 'Progress'
, а когда он будет отправлять список числе, то type: 'PrimeList'
.
Для объединения этой информации, нужно воспользоваться техникой, применённой ранее в этой статье: нужно создать объект вручную, передав нужные данные о полях. Этот объект будет иметь 2 свойства, где type
- тип сообщения, а в data
будет содержаться информация, полезные данные: {type: '...', data: ...}
.
И вот так теперь будет выглядеть модифицированный код веб воркера:
onmessage = function(event) {
// Выполняем поиск простых чисел
var primes = findPrimes(event.data.from, event.data.to);
// Возвращаем результаты веб-серипту
postMessage({
type: 'PrimeList',
data: primes
});
};
В код функции findPrimes()
так же был добавлен вызов метода postMessage()
для отправки информации о прогрессе назад в веб страницу. И в этом случае, так же, используется объект с аналогичными 2 свойствами: type
и data
. Но теперь, свойство type
указывает на то, что это сообщение является информацией о прогрессе, и из data
можем получить точный процент выполнения прогресса:
function findPrimes(fromNumber, toNumber) {
// ...
// Рассчитываем процент прогресса
var progress = Math.round(i / list.length * 100);
// Тригеррим событие только если прогресс сизменился, хотябы на 1 %
if (progress != previousProgress) {
postMessage({
type: "Progress",
data: progress
});
previousProgress = progress;
}
//...
}
После того, как страница получит сообщение, нам, теперь, прежде всего, нужно проверить тип сообщения type
, чтобы определить, как обработать информацию из data
дальше. Если была получена информация о прогрессе, текст кнопки прогресса будет обновлён. Если будет получена информация о списке простых чисел, то они все будут отображены в соответствующем блоке.
function receivedWorkerMessage(event) {
var message = event.data;
if (message.type == "PrimeList") {
var primes = message.data;
// Отобразить список чисел в DOM-е HTML, как было ранее
// ...
} else if (message.type == "Progress") {
// Распечатать текущий прогресс
statusDisplay.textContent = message.data + "% выполнено …";
}
}
Этот вид передачи сообщений может показаться сложным. Однако дополнительная работа, проделанная нами того стоит. Ведь теперь ваш код является более подробным, продвинутым и функциональным.
Резюме
Сейчас пример приведенный в этой статье выполняет только одну задачу по поиску простых чисел. Обычно на одной странице происходит дирижирование из задач и функций. Потому, советую не останавливаться на рассмотренном, и углубляться в более продвинутые фишки, которые вы можете рассмотреть самостоятельно. Вот несколько направлений, в стороны которых можете посмотреть:
- Создание нескольких веб воркеров. Ваша страница не имеет ограничений на количество одновременно работающих воркеров. Например, представьте, что вы хотите позволить посетителю выполнять поиск простых номеров одновременно в нескольких интервалах. Вы можете создавать новый веб воркер для каждого подобного поиска, и отслеживать все из них, храня все экземпляры в массиве.
- Создание веб воркера изнутри веб воркера. Веб воркер может порождать свои собственные воркеры, отправлять им сообщения, и читать сообщения от них. Эта техника является очень полезной, когда логика выполнения задачи построена на рекурсии, типа подсчёт числа Фибоначчи.
- Выполнение периодических задач с веб воркером. Веб воркеры могут использовать функции
setTimeout()
иsetInterval()
, точно так же, как и обычная веб-страница. Например, вы можете создать веб воркер который проверяет вебсайт на наличие новых данных каждую минуту.
Веб воркеры - это важная тема, которая позволяет вам писать более оптимизированные и производительные приложения. Их возможность появилась относительно недавно, однако, они уже помогают и решают большинство серьёзных задач.
В этой статье я рассказал, что такое Webworker JS, как с ним работать, добавлять сообщения, взаимодействовать между двумя скриптами, останавливать скрипты вебворкера. Все понятия web worker-а на JS были рассмотрены на простом и практичном примере. Теперь вы знаете, как создать собственный Web Worker, и какие задачи он помогает решить.