Загрузка сети с помощью модулей Workers

Перенос тяжелой работы в фоновые потоки теперь стал проще благодаря модулям JavaScript в веб-воркерах.

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

На веб-платформе основным примитивом для многопоточности и параллелизма является Web Workers API . Worker — это легкая абстракция поверх потоков операционной системы , которая предоставляет API передачи сообщений для межпоточного взаимодействия. Это может быть чрезвычайно полезно при выполнении дорогостоящих вычислений или работе с большими наборами данных, позволяя основному потоку работать плавно, одновременно выполняя дорогостоящие операции в одном или нескольких фоновых потоках.

Вот типичный пример использования работника, где рабочий сценарий прослушивает сообщения из основного потока и отвечает, отправляя обратно собственные сообщения:

страница.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

рабочий.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

API Web Worker доступен в большинстве браузеров уже более десяти лет. Хотя это означает, что рабочие процессы имеют отличную поддержку браузеров и хорошо оптимизированы, это также означает, что они появились задолго до модулей JavaScript. Поскольку при проектировании воркеров не было модульной системы, API для загрузки кода в воркёр и составления скриптов остался похожим на подходы к синхронной загрузке скриптов, распространенные в 2009 году.

История: классические рабочие

Конструктор Worker принимает классический URL-адрес сценария , относящийся к URL-адресу документа. Он немедленно возвращает ссылку на новый экземпляр работника, который предоставляет интерфейс обмена сообщениями, а также метод terminate() , который немедленно останавливает и уничтожает работника.

const worker = new Worker('worker.js');

Функция importScripts() доступна в веб-воркерах для загрузки дополнительного кода, но она приостанавливает выполнение работника, чтобы получить и оценить каждый скрипт. Он также выполняет сценарии в глобальной области действия, как классический тег <script> , что означает, что переменные в одном сценарии могут быть перезаписаны переменными в другом.

рабочий.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

приветствие.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

По этой причине веб-работники исторически оказывали огромное влияние на архитектуру приложения. Разработчикам пришлось создавать умные инструменты и обходные пути, чтобы сделать возможным использование веб-работников, не отказываясь от современных методов разработки. Например, упаковщики, такие как webpack, встраивают небольшую реализацию загрузчика модулей в сгенерированный код, который использует importScripts() для загрузки кода, но оборачивает модули в функции, чтобы избежать конфликтов переменных и имитировать импорт и экспорт зависимостей.

Введите работников модуля

В Chrome 80 появился новый режим для веб-работников с эргономикой и преимуществами производительности модулей JavaScript , который называется «модульные работники». Конструктор Worker теперь принимает новую опцию {type:"module"} , которая изменяет загрузку и выполнение скрипта в соответствии с <script type="module"> .

const worker = new Worker('worker.js', {
  type: 'module'
});

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

Переход на модули JavaScript также позволяет использовать динамический импорт для отложенной загрузки кода без блокировки выполнения работника. Динамический импорт гораздо более явный, чем использование importScripts() для загрузки зависимостей, поскольку возвращаются экспортированные данные модуля, а не полагаются на глобальные переменные.

рабочий.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

приветствие.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

Чтобы обеспечить высокую производительность, старый метод importScripts() недоступен в модулях-воркерах. Переключение воркеров на использование модулей JavaScript означает, что весь код загружается в строгом режиме . Еще одним заметным изменением является то, что значение this в области верхнего уровня модуля JavaScript равно undefined , тогда как в классических рабочих процессах значением является глобальная область видимости рабочего процесса. К счастью, всегда существовал self глобальный объект, который ссылался на глобальную область видимости. Он доступен во всех типах работников, включая сервисных работников, а также в DOM.

Предварительная загрузка воркеров с помощью modulepreload

Одним из существенных улучшений производительности, которые дает модуль-воркер, является возможность предварительной загрузки воркеров и их зависимостей. С помощью модулей-воркеров сценарии загружаются и выполняются как стандартные модули JavaScript, что означает, что их можно предварительно загрузить и даже предварительно проанализировать с помощью modulepreload :

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

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

Раньше возможности предварительной загрузки сценариев веб-воркеров были ограничены и не обязательно надежны. Классические рабочие процессы имели собственный тип ресурса «рабочий» для предварительной загрузки, но ни один браузер не реализовал <link rel="preload" as="worker"> . В результате основным методом предварительной загрузки веб-воркеров было использование <link rel="prefetch"> , который полностью полагался на кэш HTTP. При использовании в сочетании с правильными заголовками кэширования это позволило избежать необходимости ожидания загрузки рабочего сценария при создании экземпляра рабочего процесса. Однако, в отличие от modulepreload этот метод не поддерживает зависимости предварительной загрузки или предварительный анализ.

А как насчет совместных работников?

Общие рабочие процессы были обновлены с поддержкой модулей JavaScript начиная с Chrome 83. Как и выделенные рабочие процессы, создание общего рабочего процесса с опцией {type:"module"} теперь загружает рабочий сценарий как модуль, а не классический сценарий:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

До поддержки модулей JavaScript конструктор SharedWorker() ожидал только URL-адрес и необязательный аргумент name . Это будет продолжать работать для классического использования совместно используемого работника; однако для создания общих рабочих модулей требуется использование аргумента new options . Доступные параметры такие же, как и для выделенного работника, включая параметр name , который заменяет предыдущий аргумент name .

А как насчет сервис-работника?

Спецификация сервис-воркера уже обновлена ​​для поддержки принятия модуля JavaScript в качестве точки входа с использованием той же опции {type:"module"} , что и для воркеров модулей, однако это изменение еще не реализовано в браузерах. Как только это произойдет, можно будет создать экземпляр сервисного работника с помощью модуля JavaScript, используя следующий код:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

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

Дополнительные ресурсы и дальнейшее чтение