Даже самые зелёные новички в вебе знаю, что скрипты JS, нужно располагать в самом низу страницы перед закрывающим тегом script и всё знаю что это повышает скорость загрузки страницы. Но Вы когда-нибудь задумывалась, почему оно так?

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

Но подобный подход годен не везде, иногда жизненно необходимо, чтобы код JS, начал исполнение как можно раньше. Да и при подключении в самом низу, крупный JS-бандл, может здорово подкосить перфоманс сайта. Как быть в таких ситуациях? Смирится с тем что пользователи будут испытывать определённый дискомфорт, во время загрузки сайта? Конечно же нет, можно попробовать вынести наш код в веб-воркеры!

Веб-воркеры что это и с чем их едят?

Веб-воркеры реализованы в виде класса Worker, позволяющего выполнять JavaScript, в фоновом потоке браузера. Код работающий в данном потоке, абсолютно автономен и не зависим от основного. Важнейшей особенностью фонового потока является, то что он не имеет доступа к глобальным объектам:

  • Document;

  • Window.

То есть из фонового потока невозможно через селекторы CSS, обращаться к ним и как-то модифицировать DOM. Данное ограничение позволяет избежать, сложностей возникающих в других языках, при работе с потоками. А именно не позволяет параллельным потокам войти в состояние гонки, когда бы например два воркера, одновременно начали вносить правки в одни и те же HTML-элементы. Воркеры вообще максимально автономны. Не главный поток, не любой другой воркер, не могут вмешиваться в их работу. Потоки могут общаться друг с другом исключительно через отправку/прослушивание сообщений. Также воркеры имеют свой глобальный объект WorkerGlobalScope (аналог объекта window в браузере, или global в Node.js), дающий им доступ практически ко всем привычным глобальным функциям и объектам JavaScript.

Запуск скрипта в воркере

Сначала нужно создать экземпляр класса Worker(). Его конструктор, принимает путь к файлу JavaScript, код которого, требуется запустить в фоновом потоке. Воркеры подвержены ограничению CORS и могут запускать код, только из того же самого источника, в котором они работают сами:

const worker = new Worker('path/to/script.js')

Вторым аргументом конструктор объекта Worker(), принимает объект настроек. В данном объекте может содержать следующие свойства:

  1. type - определяет то каким образом воркер будет подключать сторонние скрипты. Допустимые значения:

    1. classic - скрипты будут подключаться через специальный механизм импорта воркера (importScripts()). Является значением по умолчанию;

    2. module - в воркере станут доступны модули ES6.

  2. credentials - указывает каким образом воркер будет работать с учётными данными пользователей (cookie, HTTP-аутентификация). Влияет на работу функций: fetch и XMLHttpRequest. На практике редко возникает необходимости менять значение данного параметра, но он может принимать значения:

    1. omit - наиболее строгая настройка, подразумевает что воркер никогда не будет отправлять учётные данные в своих запросах. Таким образом утечка конфиденциальных данных, исключается в принципе;

    2. same-origin - является значением по умолчанию и единственным допустимым значением, для воркеров с типом classic. В этом режиме воркер будет соблюдать ограничения CORS и отправлять учётные данные только в запросах на тот же источник, где он сам запущен;

    3. include - воркер будет отправлять учётные данные при любом запросе, вне зависимости, направлены они на один с ним источник или нет. Наименее безопасный вариант, поэтому его следует использовать с максимальной осторожность.

  3. name - его значение, перекочует в аналогичное свойство глобального объекта веб-воркера (WorkerGlobalScope), что полезно для деббагинга.

И на этом всё, после создания воркера код указанного скрипта запустится во второстепенном потоке браузера. В свою очередь воркеры внутри себя, сами могут запускать другие воркеры.

Обмен сообщениями с воркерами

Отправка сообщения в воркер

Как упоминалось выше, воркер не может на прямую взаимодействовать с главным потоком. Поток воркера может общаться с главным потоком (или другим воркером), через сообщения. Для этого в классе Worker(), имеется метод postMessage(), позволяющий отправить данные из основного потока во второстепенный. Он принимает, данные для потока воркера, в качестве аргумента и передаёт их в него алгоритмом структурированного клонирования:

myWorker.postMessage('Привет Вася!')

В свою очередь воркер может принять это сообщение, зарегистрировав обработчик в своём свойстве onmessage:

self.onmessage = (event) => {
    console.log(event.data) // Привет Вася!
}

Или при помощи метода addEventListener():

self.addEventListener('message', (event) => {
    console.log(event.data) // Привет Вася!
})

Отправка сообщения из воркера

Отправка сообщений из воркера, в главный поток осуществляется, зеркальным способом. Воркер должен отправить сообщение при помощи метода postMessage():

self.postMessage('Привет Пётр!')

А в главном потоке нужно повесит обработчик события чтобы это сообщение принять:

worker.onmessage = (event) => {
    console.log(event.data) // Привет Пётр!
}

Как и в главном потоке можно вместо свойства onmessage, повесить обработчик события, через метод addEventListener:

worker.addEventListener('message', (event) => {
    console.log(event.data) // Привет Пётр!
})

Ещё так как в воркере postMessage() является свойством глобального объекта воркера, то его можно вызывать как и любую другую глобальную функцию:

postMessage('Привет Пётр!')

Свойство onmessage, также является глобальным:

onmessage = (event) => {
    console.log(event.data)
}

Остановка работы воркера

Для завершения работы воркера, из главного потока, нужно вызвать метод terminate():

worker.terminate()

Также воркер может остановить сам себя вызвав функцию close():

close()
postMessage('Привет Пётр!')

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

WorkerGlobalScope - глобальный объект фонового потока

Помимо перечисленных выше 2-х свойств, для отправки сообщений в основной поток, глобальный объект WorkerGlobalScope, имеет все стандартный свойства и методы, как и у глобального объекта window (JSON, isNaN(), Date() и т.д.). Так же он имеет дополнительные свойства, которых либо нет у объекта window, либо их поведение отличается:

  1. self - ссылается на сам глобальный объект WorkerGlobalScope;

  2. location - содержит объект WorkerLocation, хранящий данные о адресе, файле и порте, где запущен воркер. В отличии от основного потока, свойства объекта Location, в веб-воркере допускают только чтение;

  3. navigator - содержит объект Navigator, который имеет множество свойств об устройстве клиента.

Все эти свойства полезны при деббагинге.

Импорт скриптов в веб-воркерах

Так как воркеры появились в JS, раньше модулей, в них, как в Node.js, был определён свой, уникальный способ импорта сторонних скриптов, при помощи функции importScripts(), которая принимает один или более аргумент с URL подключаемого скрипта:

importScripts('path/to/script.js', 'path/to/second/script.js')

Относительные URL, работают относительно скрипта самого веб-воркера. Данная функция синхронно загружает все указанные скрипты, в указанном порядке. Если во время этой процедуры, в одном из скриптов произойдёт ошибка, загрузка прекратиться, даже не дойдя до скриптов следующих за ним.

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

Синхронность функции importScripts(), проблем не вызовет, так как код в веб-воркерах не блокирует основной поток и синхронные функции не скажутся на производительности приложения.

Модули в веб-воркерах

Веб-воркеры поддерживают и обычные модули ES6. Для того чтобы они стали доступны, нужно конструктору объекта Worker(), во втором аргументе передать объект с настройками, в котором должно быть указано значение свойства type, равное module. Это сделает доступным, для использования подключение модулей при помощи import, но при этом функция importScripts(), станет недоступной.

Переделаем наш воркер под модули ES6:

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

worker.addEventListener('message', (event) => {
    console.log(event.data)
})

Сделаем простенький модуль:

export default function greeting(name) {
     return `Hello ${name}!`
}

Импортируем наш модуль и используем его в коде воркера:

import greeting from "./import_script.js";

postMessage(greeting('Василий'))

Время жизни кода воркера

Код веб-воркеров выполняется синхронно, а после завершения выполнения переходит в асинхронную стадию в которой только реагирует на сообщения, из основного потока или работу таймеров.

При этом, если в воркере зарегистрировано прослушивание события message, он никогда не завершит работу. Если такое прослушивание отсутствует, воркер автоматически завершит свою работу, сразу после того как будет выполнен весь его код.

Ошибки в веб-воркерах

Перехват ошибок из воркеров в главном потоке

Когда в веб-воркере возникает ошибка, она инициализируется в объекте Worker(). Перехватить через блок try/catch в главном потоке, не удастся. Для обработки ошибок у объекта воркера есть специальное свойство onerror:

worker. => {
    console.log(error)
}

Как Вы уже наверное догадались, обработчик ошибок в воркере, можно повесить и через метод addEventListener:

worker.addEventListener('error', (error) => {
    console.log(error)
})

Чтобы прекратить распространение ошибки на главный поток, нужно использовать метод preventDefault()

worker.addEventListener('error', (error) => {
    error.preventDefault()

    console.log(error)
})

Прихват ошибок внутри воркеров

Внутри самого веб-воркера перехватывать ошибки можно при помощи обычного блока try/catch:

import greeting from "./import_script.js";

try {
    postMassage(greeting('Василий'))
} catch(error) {
    console.log(error)
}

При чём останавливать распространение ошибки при помощи метода preventDefault(), как делалось в главном потоке, уже не нужно.

Перехватывать отклонённые промисы можно при помощи привязки обработчика ошибок к свойству onunhandledrejection. Переделаем импортируемый скрипт, так чтобы он возвращал промисы:

export default function greeting(name) {
     if (!name) {
          return Promise.reject(new Error('Кого приветствовать то?!'));
     }

     return Promise.resolve(`Привет ${name}!`);
}

Затем перехватим отклонённый промис в воркере:

import greeting from "./import_script.js";

console.log(greeting())

onunhandledrejection = (error) => {
    console.log(error)
}

Остановить распространение ошибки, может метод preventDefault():

import greeting from "./import_script.js";

console.log(greeting())

onunhandledrejection = (error) => {
	error.preventDefault()
    console.log(error)
}

И разумеется пример выше можно переделать на метод addEventListener():

import greeting from "./import_script.js";

console.log(greeting())

self.addEventListener('unhandledrejection', (error) => {
    error.preventDefault()
    console.log(error)
})

Вывод

Веб-воркеры - это крутивший инструмент в составе JS, дающий возможность сильно улучшить перфоманс приложения за счёт многопоточного программирования. Но при этом многопоточка JavaScript сильно отличается от, аналогичной фичи в других языках в сторону упрощения. Это накладывает ряд ограничений, но одновременно, защищает разработчика от выстрела в себе в колено, не давая потокам войти в состояние гонки и создать сложнодиагностируемые баги. И как по мне это отличный компромисс, для языка с достаточно низким порогом входа! Если в Вашем приложении JavaScript сильно забивает главный поток и это в свою очередь снижает баллы в Google PageSpeed:

То рефакторинг, с переносом части кода в воркеры может стать отличным выходом!

Комментарии (10)


  1. BuslikDrev
    04.06.2025 13:44

    Если скрипт забивает главный поток, то его проще отложить до первого взаимодействия со страницей. Если скрипт имеет дату начала загрузки, то откладываем чисто часть кода отвечающую за размещение ссылки на скрипт в структуре DOM. Если есть какой-то модуль с большой структурой DOM, то необходимо сделать его загрузку через AJAX. Если нет возможности сделать через AJAX, то хотя бы обвернуть в <noscript></noscript>, а после появления в поле зрения экрана убрать эти теги.


  1. nihil-pro
    04.06.2025 13:44

    Каким образом код размещенный в воркере оптимизирует загрузку страницы? Вот есть например какой-то код, который навешивает каких-то слушателей для сбора аналитики. Большой, влияет на загрузку и .т.д. Как вы его в воркер вынесете?


    1. winkyBrain
      04.06.2025 13:44

      воркеры позволяют творить даже вот такое


      1. nihil-pro
        04.06.2025 13:44

        Как раз для этого они и предназначены, выполнять тяжелые вычисления в отдельном потоке. А не для того, чтобы скрипты из футера туда переносить


    1. JastaFly Автор
      04.06.2025 13:44

      Как вы его в воркер вынесете?

      Есть методика. Если этот пост наберёт 50 лайков, напишу статью об этом)


  1. goldexer
    04.06.2025 13:44

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


    1. JastaFly Автор
      04.06.2025 13:44

      прогоняйте статью через профридеры перед публикацией 

      Честное пионерское прогонял на текст ру, перед рейлизом. Уже глаз замылился. Буду благодарен если что-то подсветите или посоветуете профридер на будущее)


      1. dedmagic
        04.06.2025 13:44

        Подсвечиваю: странная любовь к запятым, в первых трёх абзацах штук восемь лишних. Хотя нескольких не хватает, так что любовь ли это?

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


        1. JastaFly Автор
          04.06.2025 13:44

          так что любовь ли это?

          Неразделённая)


  1. shsv382
    04.06.2025 13:44

    Считаю, что не поделу автору напихали в панамку - статья действительно может оказаться полезной для новичков и тех, кто не работал с Worker'ами