Skip to content

Worker Kit ​

Типобезопасные composables для Web Worker'ов во Vue 3 — useWorker(), пул воркеров и реактивный useWorkerComputed(), с типами входа/выхода, выводимыми прямо из файла воркера. Никаких runtime-зависимостей, кроме Vue.

Возможности ​

  • useWorker() — типобезопасный composable, оборачивающий один Web Worker; типы входа/выхода выводятся из typeof import('./x.worker'), без ручных дженериков ни с одной из сторон
  • createWorkerPool() / useWorkerPool() — лениво растущий пул (размер по умолчанию: navigator.hardwareConcurrency) для множества мелких независимых задач; pool.run()/pool.map() с ограниченной параллельностью
  • useWorkerComputed() — computed(), пересчитывающийся внутри воркера при каждом изменении реактивного источника; устаревшие/вытесненные запуски автоматически отбрасываются через номер поколения
  • useSharedWorker() — переиспользует один SharedWorker во всех вкладках/окнах одного источника вместо отдельного воркера на каждую вкладку
  • defineWorkerHandler() — вспомогательная функция на стороне воркера, автоматически настраивающая протокол сообщений run/cancel; вы пишете только саму функцию-обработчик
  • Transferables — передача с нулевым копированием в воркер (RunOptions.transfer) и обратно (ctx.transfer()) для больших буферов вроде изображений или кадров OffscreenCanvas
  • Стриминг / частичные результаты — ctx.reportChunk() отправляет промежуточные результаты, пока длительная задача ещё выполняется, собираются реактивно в chunks
  • Отмена — поддержка AbortSignal, кооперативная через ctx.signal; run() отклоняется немедленно независимо от того, что делает воркер
  • Повтор с задержкой (backoff) — опции retries/retryDelay автоматически повторяют неотменённые сбои с постоянной или зависящей от попытки задержкой
  • Мемоизация / кеш результатов — опциональный LRU-кеш по ключу JSON.stringify(input), полностью пропускает обращение к воркеру для повторяющегося входа
  • Прогрев (warmup) — заранее создайте воркер (или весь пул), чтобы устранить задержку холодного старта ~50-200 мс перед первой реальной задачей
  • Управление жизненным циклом воркера — завершение по таймауту простоя, авто-завершение на основе scope через onScopeDispose (без утечек при SPA-навигации), ленивое создание воркеров пула
  • Панель Devtools — <WorkerActivityPanel> из vue-worker-kit/devtools показывает количество занятых/свободных воркеров, длину очереди, среднее время задачи и последние ошибки — без зависимости от @vue/devtools-api
  • Структурированная обработка ошибок — брошенная в воркере ошибка становится WorkerError с исходным in-worker стеком, сохранённым в .workerStack; DataCloneError для значений, которые не проходят structured-clone; WorkerUnavailableError вместо сырого ReferenceError при SSR
  • Сквозной вывод типов — тип входа/выхода функции воркера выводится прямо из файла воркера через typeof import(...), а не дублируется вручную с обеих сторон
  • Ноль runtime-зависимостей, кроме Vue

Проблема ​

Существующие Vue-обёртки над Web Worker'ами (vue-worker, vue-web-workers и подобные) — это плагины эпохи Vue 2: без типов, без Composition API, без пула, без transferables, одноразовые воркеры, собранные сериализацией функции в строку. Comlink даёт надёжный RPC-протокол, но типизировать его приходится вручную (Comlink.wrap<MyAPI>()), без реактивности Vue и без интеграции с жизненным циклом компонента.

Главная отличительная идея этого пакета: сквозная типизация без дублирования дженериков. Тип входа/выхода функции воркера выводится прямо из файла воркера через typeof import(...), а не прописывается вручную с обеих сторон.

async/await против реального потока ​

Стоит проговорить явно, потому что легко решить, что async/await уже решает эту задачу: async/await не выносит работу с JS-потока. JavaScript (вне воркеров) всегда выполняется в один поток, сколько бы async/await вы туда ни добавляли.

Есть два принципиально разных случая, которые называют «асинхронностью»:

  • Ожидание ввода-вывода — fetch, setTimeout, любой промис, опирающийся на API браузера/ОС. Само ожидание происходит вне JS (в сетевом стеке, в таймере ОС), поэтому основной поток действительно свободен во время await. Воркер здесь не нужен никогда.
  • Вычисление, ограниченное CPU — ваш собственный цикл, сортировка, парсинг. Обёртывание в async function ничего не меняет: цикл по-прежнему выполняется синхронно, в том же потоке, который также пытается отрисовывать ваш UI и обрабатывать клики. Единственный способ сохранить отзывчивость UI без воркера — вручную порезать цикл на куски и уступать управление (await new Promise(r => setTimeout(r))) между ними — это ровно то, для чего внутри воркера служит паттерн ctx.reportProgress/ctx.signal у defineWorkerHandler, но на основном потоке это ничего не даёт: это всё тот же поток, просто более мелкие куски той же работы чередуются с рендерингом.

Worker — это по-настоящему отдельный поток ОС. В этом и заключается структурное отличие от async/await:

  • Основной поток на 100% свободен на всё время вычисления — не нужно вручную резать на куски и уступать управление только ради того, чтобы UI не завис (резать всё равно придётся, если нужны отчёт о прогрессе или отмена, но это опционально, а не обязательно для отзывчивости).
  • Это не делает работу автоматически быстрее по затраченному времени — у postMessage/структурного клонирования и запуска воркера есть реальная цена, и для короткого вычисления обычный запуск на основном потоке вполне может завершиться раньше. Смысл воркера не в сырой скорости, а в том, что работа больше не конкурирует с UI за один и тот же поток. createWorkerPool() — единственное место, где вы действительно получаете реальный прирост скорости от параллелизма: несколько воркеров по-настоящему считают на разных ядрах CPU одновременно.

Как работает вывод типов ​

typeof import('./heavy-sort.worker') — это выражение только для типов: TypeScript стирает его на этапе компиляции. Оно не импортирует код файла воркера в основной бандл — воркер всегда загружается только через new URL(..., import.meta.url), как отдельный чанк. defineWorkerHandler() возвращает фантомно типизированный маркер (поля __input/__output, которых никогда не существует в рантайме); useWorker/createWorkerPool считывают In/Out с этого маркера через условный тип. В итоге сигнатура run() — ровно (input: In, options?: RunOptions) => Promise<Out>, без ручного дженерика для формы данных ни с одной из сторон.