Skip to content

vue-worker-kit

vue-worker-kit

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

Установка

bash
npm install vue-worker-kit

Никаких peer-зависимостей, кроме самого vue.

Быстрый старт

ts
// heavy-sort.worker.ts
import { defineWorkerHandler } from 'vue-worker-kit/worker'

export default defineWorkerHandler(async (data: number[], ctx) => {
  for (let i = 0; i < data.length; i++) {
    if (ctx.signal.aborted) throw ctx.signal.reason
    if (i % 10_000 === 0) ctx.reportProgress(i / data.length)
  }
  return data.sort((a, b) => a - b)
})
ts
// component setup()
import { useWorker } from 'vue-worker-kit'

const { run, isRunning, progress, error, cancel } = useWorker<typeof import('./heavy-sort.worker')>(
  () => new Worker(new URL('./heavy-sort.worker.ts', import.meta.url), { type: 'module' }),
)

const sorted = await run(hugeArray, { transfer: [hugeArray.buffer] })
// sorted: number[] — выведено из heavy-sort.worker.ts, аннотация generic не нужна

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

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