Импорт воркеров через ?worker и ?sharedworker

Vite поддерживает импорт Web Worker и SharedWorker напрямую через механизм специальных суффиксов в пути импорта. Вместо ручного создания отдельных файлов, настройки сборщика и генерации URL используется декларативный синтаксис:

import MyWorker from './worker.js?worker'
import MySharedWorker from './shared.js?sharedworker'

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

Пример:

import MyWorker from './worker.js?worker'

const worker = new MyWorker()

worker.postMessage('Hello')

Vite автоматически:

  • создаёт отдельный bundle для воркера;
  • обрабатывает зависимости воркера;
  • генерирует корректные URL;
  • подключает HMR в режиме разработки;
  • оптимизирует код при production-сборке.

Что такое Web Worker

Web Worker — отдельный поток выполнения JavaScript, работающий параллельно основному UI-потоку браузера.

Воркеры используются для:

  • тяжёлых вычислений;
  • обработки больших массивов данных;
  • парсинга;
  • компрессии;
  • криптографии;
  • фоновых вычислений;
  • работы с бинарными структурами;
  • обработки изображений;
  • предотвращения подвисания интерфейса.

Главная особенность — отсутствие доступа к DOM.


Базовый импорт через ?worker

Структура проекта

src/
├── main.js
└── worker.js

Код воркера

self.onmess age = (event) => {
    const result = event.data * 2

    self.postMessage(result)
}

Основной файл

import MyWorker from './worker.js?worker'

const worker = new MyWorker()

worker.postMessage(10)

worker.onmess age = (event) => {
    console.log(event.data)
}

Результат:

20

Что возвращает импорт ?worker

Импорт:

import WorkerModule from './worker.js?worker'

эквивалентен примерно следующему коду:

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

Однако Vite:

  • самостоятельно генерирует URL;
  • создаёт отдельный chunk;
  • подключает трансформации;
  • обеспечивает совместимость dev/prod режима.

Модульные воркеры

Vite создаёт worker в формате ES Module.

Это означает возможность использовать:

import
export
top-level await
dynamic import()

Пример:

import { sum } from './math.js'

self.onmess age = (e) => {
    self.postMessage(sum(e.data.a, e.data.b))
}

Импорт зависимостей внутри воркера

Воркеры в Vite работают как полноценные модули.

Пример

import dayjs from 'dayjs'

self.onmess age = () => {
    self.postMessage(dayjs().format())
}

Vite включит библиотеку в worker-bundle автоматически.


Передача данных между потоками

Обмен выполняется через:

postMessage()

и обработчики:

onmessage

Отправка данных в worker

worker.postMessage({
    numbers: [1, 2, 3]
})

Получение внутри worker

self.onmess age = (event) => {
    console.log(event.data)
}

Ответ обратно

self.postMessage({
    status: 'done'
})

Использование self

Внутри воркера глобальным объектом является:

self

Он аналогичен window в основном потоке.

Пример:

self.addEventListener('message', (event) => {
    self.postMessage(event.data)
})

Завершение работы воркера

Остановка из главного потока

worker.terminate()

После этого worker уничтожается полностью.


Самоуничтожение внутри worker

self.close()

Передача бинарных данных

Воркеры особенно эффективны при работе с:

  • ArrayBuffer
  • TypedArray
  • Blob
  • File
  • ImageBitmap

Пример

const buffer = new ArrayBuffer(1024)

worker.postMessage(buffer, [buffer])

Второй аргумент — transferable objects.

Вместо копирования память передаётся между потоками.


Использование ?worker&inline

Vite поддерживает inline-режим:

import MyWorker from './worker.js?worker&inline'

В этом случае worker встраивается в основной bundle.

Обычно используется:

  • для небольших воркеров;
  • для уменьшения количества network-запросов;
  • в компактных приложениях.

Разница между обычным worker и inline worker

Обычный worker

import W from './worker.js?worker'

Особенности:

  • отдельный файл;
  • отдельный network request;
  • лучшее кеширование;
  • меньший размер главного bundle.

Inline worker

import W from './worker.js?worker&inline'

Особенности:

  • код встроен в bundle;
  • нет отдельного запроса;
  • увеличивается размер основного JS-файла.

Использование ?worker&url

Иногда нужен только URL worker-файла.

Для этого используется:

import workerUrl from './worker.js?worker&url'

Пример:

const worker = new Worker(workerUrl, {
    type: 'module'
})

SharedWorker

SharedWorker — специальный тип воркера, который может использоваться несколькими вкладками, окнами или iframe одновременно.

Импорт:

import SharedWorkerModule from './shared.js?sharedworker'

Создание:

const worker = new SharedWorkerModule()

Отличие SharedWorker от Worker

Worker

  • отдельный экземпляр;
  • привязан к одной странице;
  • изолирован.

SharedWorker

  • общий экземпляр;
  • доступен нескольким вкладкам;
  • поддерживает обмен между клиентами.

Пример SharedWorker

shared.js

const connections = []

self.onconn ect = (event) => {
    const port = event.ports[0]

    connections.push(port)

    port.onmess age = (e) => {
        for (const connection of connections) {
            connection.postMessage(e.data)
        }
    }

    port.start()
}

main.js

import MySharedWorker from './shared.js?sharedworker'

const worker = new MySharedWorker()

worker.port.postMessage('Hello')

worker.port.onmess age = (e) => {
    console.log(e.data)
}

Особенности SharedWorker

Вместо:

onmessage
postMessage

используется:

port.onmessage
port.postMessage

Причина — SharedWorker поддерживает несколько подключений одновременно.


Поддержка TypeScript

Vite поддерживает worker-модули в TypeScript без дополнительной настройки.

worker.ts

self.onmess age = (event: MessageEvent<number>) => {
    self.postMessage(event.data * 2)
}

main.ts

import MyWorker from './worker.ts?worker'

const worker = new MyWorker()

Типизация worker в TypeScript

Можно описать тип сообщения:

type Request = {
    value: number
}

type Response = {
    result: number
}

Worker

self.onmess age = (event: MessageEvent<Request>) => {
    self.postMessage({
        result: event.data.value * 2
    } satisfies Response)
}

Использование new URL() без ?worker

Vite также поддерживает стандартный вариант:

new Worker(
    new URL('./worker.js', import.meta.url),
    {
        type: 'module'
    }
)

Но ?worker обычно предпочтительнее из-за:

  • более краткого синтаксиса;
  • лучшей читаемости;
  • автоматического экспорта конструктора;
  • поддержки inline/url-режимов.

Dynamic import worker

Воркеры можно загружать динамически.

const { default: MyWorker } =
    await import('./worker.js?worker')

const worker = new MyWorker()

Полезно для:

  • lazy loading;
  • code splitting;
  • условного создания worker.

Обработка ошибок worker

Ошибки внутри worker

worker.oner ror = (event) => {
    console.error(event.message)
}

Необработанные исключения

throw new Error('Worker failed')

Ошибки загрузки

Возможны при:

  • неправильном пути;
  • проблемах CSP;
  • ошибках сборки;
  • несовместимых API.

Hot Module Replacement

Во время разработки Vite автоматически обновляет worker-модули.

Изменения в:

worker.js

приводят к пересборке worker bundle.


Ограничения worker

Воркеры не имеют доступа к:

  • DOM;
  • window;
  • document;
  • localStorage;
  • sessionStorage;
  • синхронным UI API.

API, доступные внутри worker

Поддерживаются:

  • fetch
  • WebSocket
  • IndexedDB
  • crypto
  • timers
  • URL
  • TextEncoder
  • TextDecoder

Использование WASM внутри worker

Vite позволяет импортировать WebAssembly в worker.

import init from './math.wasm'

Это особенно полезно для:

  • Rust;
  • C++;
  • SIMD;
  • тяжёлых вычислений.

Производительность и worker

Worker не ускоряет JavaScript автоматически.

Преимущество достигается за счёт:

  • разгрузки main thread;
  • параллельности;
  • отсутствия блокировки интерфейса.

Когда worker не нужен

Использование worker может быть избыточным для:

  • простых вычислений;
  • коротких операций;
  • маленьких массивов;
  • редких задач.

Создание worker тоже имеет стоимость:

  • запуск потока;
  • сериализация данных;
  • передача сообщений;
  • расход памяти.

Архитектурные подходы

Один worker на задачу

UI → Worker

Подходит для:

  • независимых операций;
  • простых приложений.

Worker pool

UI → Pool → Workers

Используется для:

  • многопоточной обработки;
  • очередей задач;
  • CPU-intensive вычислений.

SharedWorker как message hub

Tabs ↔ SharedWorker ↔ Server

Применяется для:

  • единого websocket;
  • синхронизации вкладок;
  • централизованного состояния.

Поведение в production-сборке

При vite build:

  • worker выносится в отдельный chunk;
  • зависимости оптимизируются;
  • создаются hash-имена файлов;
  • выполняется минификация.

Пример:

assets/worker-8f3a1d.js

Совместимость браузеров

Worker

Поддерживается практически всеми современными браузерами.


SharedWorker

Поддержка хуже, особенно:

  • мобильными браузерами;
  • Safari iOS.

При использовании SharedWorker желательно проверять совместимость.


Практический пример вычислений

fibonacci-worker.js

function fib(n) {
    if (n <= 1) {
        return n
    }

    return fib(n - 1) + fib(n - 2)
}

self.onmess age = (e) => {
    const result = fib(e.data)

    self.postMessage(result)
}

main.js

import FibWorker from './fibonacci-worker.js?worker'

const worker = new FibWorker()

worker.postMessage(40)

worker.onmess age = (e) => {
    console.log('Result:', e.data)
}

Без worker интерфейс браузера мог бы зависнуть во время вычислений.


Практический пример парсинга JSON

json-worker.js

self.onmess age = (e) => {
    const parsed = JSON.parse(e.data)

    self.postMessage(parsed)
}

main.js

import JsonWorker from './json-worker.js?worker'

const worker = new JsonWorker()

fetch('/large.json')
    .then((r) => r.text())
    .then((text) => {
        worker.postMessage(text)
    })

worker.onmess age = (e) => {
    console.log(e.data)
}

Практический пример image processing

image-worker.js

self.onmess age = (e) => {
    const pixels = e.data

    for (let i = 0; i < pixels.length; i += 4) {
        pixels[i] = 255 - pixels[i]
        pixels[i + 1] = 255 - pixels[i + 1]
        pixels[i + 2] = 255 - pixels[i + 2]
    }

    self.postMessage(pixels)
}

Такой подход часто используется в:

  • canvas-приложениях;
  • редакторах изображений;
  • видеообработке;
  • графических инструментах.