/ JavaScript

Простое объяснение, что такое Webworkeр-ы в JS и как с ними работать

Как ваш JS-код может выполнять несколько задач параллельно в фоне? Для этого существуют Webworker-ы, позволяющие реализовать такой функционал. В этой статье я расскажу, как с ними работать, познакомлю вам с Webworkers API, и покажу, как в JS выполнять задачи в фоне, в отрезе от кода текущей веб-страницы.

Для того, чтобы комфортно понимать суть этой статьи вы должны знать основы JavaScript (события, работа с DOM).

Когда JavaScript был впервые создан, никто особо не беспокоился о производительности этого языка программирования. Его основная задача была запуск небольших скриптов на веб-странице, имея при этом максимально простой синтаксис написания скриптов. Ранее JavaScript был упрощенным скриптовым языком для программистов-любителей. И, изначально, он не задумывался как язык, который решает какие-то сложные бизнес задачи.

Однако, годы шли, и спустя как 25 лет, JavaScript полностью покорил веб (захватив при этом нишу и фронтенда, и бекенда). Спустя какое-то время он стал быть похожим на профессиональный язык программирования, в нём стали появляться новые продвинутые инструменты и функции. Одной из которых и являются Webwork-ы.

Вебворкеры позволяют вам написать скрипт, который выполняет ресурсоёмкую работу в фоне без подвисаний страницы. Например, представьте, что вы хотите сделать какие-то большие, сложные расчёты по клику на определённую кнопку. Если вы запустите расчёт сразу же по клику пользователя, то расчёты незамедлительно начнутся (которые займут несколько, а то и десятки секунд). И в это время, пока скрипт будет работать, ваш поток будет занят, из-за чего вы никак не сможете взаимодействовать со страницей. В некоторых случаях вы даже можете получить уведомление о том, что страница "не отвечает".
1_6FqBtyiDrh1P7KWe4OkaGQ

В этой случае для сохранения удобства пользования сайтом, ничего не остаётся, кроме как выполнять задачу по этим расчётам в фоне, отораванно от основного сайта. И благодаря этому, пока задача будет выполняться в фоне, пользователь беспрепятственно сможет пользоваться вашим сайтом, не имея при этом "тормозов". Технически - задача выполняется в фоне, в отдельном потоке компьютера.

Полное техническое понимание того, как работают потоки немного более сложная задача, чем мы сегодня обсуждаем. Но основная идея заключается в том, что современная операционная система запускает сотни разных потоков за один раз, обрабатывает информацию в многих из них, и быстро переключается от одного потока к другому, обмениваясь данными. На самом деле, переключение происходит настолько быстро, что кажется, как буд-то ничего не происходит.

Веб-воркеры позволяют вам выполнять любые ресурсоёмкие задачи в фоне. Процесс их работы прост:

  • Вы создаёте Веб-воркер.
  • Вы описываете, какую задачу Веб-воркер должен выполнять (Например, перебор большого набора данных в поисках конкретных значений).
  • Запускаете Веб-воркер.
  • Когда скрипт закончит работу он сообщит вам, и вы получите результат его работы, описанный скриптом. (К примеру, показать результаты расчёта на странице).

Теперь давайте углубимся!

Создаём ресурсоёмкую задачу

Прежде чем вы увидите преимущества использования Веб-воркеров, сначала напишем код, у которого будут проблемы с производительностью, и который мы будем улучшать путём внедрения в Веб-воркеры. Нет смысла использовать Веб-воркеры для простых и быстрых задач, потому что они не тормозят вашу страницу. Основная их задача - выполнение ресурсоёмких задач.

К примеру, напишем скрипт, который будет искать простые числа и выводить их на странице.


Этот код перебирает все числа, заданные в диапазоне, и выбирает только простые числа. Вы указываете границы чисел для перебора в 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.

Если вы ранее работали с вебсокетами, то здесь принцип похожий. Клиент общается с сервером, отправляя друг другу сообщения, при наступлении определённых событий.

schema

Есть еще один момент, который нужно рассмотреть перед тем как углубиться. Метод 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, и какие задачи он помогает решить.