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

Перенос ресурсоемких задач в фоновые потоки теперь стал проще благодаря модулям JavaScript в Web Worker.

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

На веб-платформе основным примитивом для потоковой обработки и параллелизма является API Web Workers . Workers — это облегчённая абстракция поверх потоков операционной системы , предоставляющая 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-адреса документа. Он немедленно возвращает ссылку на новый экземпляр Worker, предоставляющий интерфейс обмена сообщениями, а также метод terminate() , который немедленно останавливает и уничтожает Worker.

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() для загрузки кода, но оборачивает модули в функции, чтобы избежать конфликтов переменных и имитировать импорт и экспорт зависимостей.

Введите рабочих модуля

Новый режим для веб-воркеров, сочетающий эргономику и производительность модулей JavaScript, появился в Chrome 80. Он называется «модульные воркеры». Конструктор 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() недоступен в модулях Worker. Переключение Worker на использование модулей JavaScript означает, что весь код загружается в строгом режиме . Ещё одно заметное изменение заключается в том, что значение this в области действия верхнего уровня модуля JavaScript равно undefined , тогда как в классических Worker значением является глобальная область действия Worker. К счастью, всегда существовал глобальный self , предоставляющий ссылку на глобальную область действия. Он доступен во всех типах Worker, включая Service Worker, а также в 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>

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

Ранее возможности предварительной загрузки скриптов веб-воркеров были ограничены и не всегда были надёжными. Классические воркеры имели собственный тип ресурса «worker» для предварительной загрузки, но ни один браузер не реализовал <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 . Это будет работать и для классического использования общих рабочих процессов; однако для создания модульных общих рабочих процессов требуется новый аргумент options . Доступные параметры те же, что и для выделенного рабочего процесса, включая параметр name , который заменяет предыдущий аргумент name .

А как насчет работников сферы услуг?

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

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

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

Дополнительные ресурсы и дополнительная литература