Впровадження Web Workers для фонових обчислень на веб-сайті
JavaScript — однопоточний. Важка операція у main thread замораживає інтерфейс: скролл дергається, анімації переривають, кнопки не реагують. Web Workers розв'язують це, перенісши обчислення в окремий потік операційної системи.
Worker не має доступу до DOM, window, document. Спілкування з main thread — тільки через повідомлення (postMessage/onmessage). Це обмеження — одночасно і захист від race conditions.
Базова структура
Два файли: основний потік і Worker:
// worker.ts
self.onmessage = (event: MessageEvent) => {
const { type, payload } = event.data
switch (type) {
case 'PROCESS': {
const result = heavyComputation(payload)
self.postMessage({ type: 'RESULT', payload: result })
break
}
}
}
function heavyComputation(data: number[]): number {
// Обчислення, яке зайняло би 500ms у main thread
return data.reduce((sum, n) => sum + Math.sqrt(n), 0)
}
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
})
worker.postMessage({ type: 'PROCESS', payload: largeArray })
worker.onmessage = (event: MessageEvent) => {
const { type, payload } = event.data
if (type === 'RESULT') {
console.log('Результат:', payload)
}
}
worker.onerror = (error) => {
console.error('Worker error:', error.message)
}
// Завершити Worker
worker.terminate()
Типізована обертка
Робота з сирим postMessage незручна. Типізована обертка розв'язує це:
// worker-bridge.ts
type WorkerMessage<T extends Record<string, unknown>> = {
[K in keyof T]: { type: K; payload: T[K] }
}[keyof T]
interface WorkerRequest {
SORT: { array: number[]; direction: 'asc' | 'desc' }
FILTER: { data: Record<string, unknown>[]; query: string }
PARSE_CSV: { content: string }
}
interface WorkerResponse {
SORT_DONE: number[]
FILTER_DONE: Record<string, unknown>[]
PARSE_CSV_DONE: Record<string, string>[]
ERROR: { message: string }
}
class TypedWorker {
private worker: Worker
private pending = new Map<string, { resolve: Function; reject: Function }>()
private seq = 0
constructor(workerUrl: URL) {
this.worker = new Worker(workerUrl, { type: 'module' })
this.worker.onmessage = ({ data }) => {
const { id, type, payload } = data
const handler = this.pending.get(id)
if (!handler) return
this.pending.delete(id)
if (type === 'ERROR') {
handler.reject(new Error(payload.message))
} else {
handler.resolve(payload)
}
}
}
send<K extends keyof WorkerRequest>(
type: K,
payload: WorkerRequest[K]
): Promise<WorkerResponse[`${K}_DONE` & keyof WorkerResponse]> {
return new Promise((resolve, reject) => {
const id = String(++this.seq)
this.pending.set(id, { resolve, reject })
this.worker.postMessage({ id, type, payload })
})
}
terminate(): void {
this.worker.terminate()
}
}
Передача великих даних — Transferable Objects
postMessage копіює дані за замовчуванням. Для великого ArrayBuffer це дорого. Transferable Objects передаються за посиланням (owner transfer), без копіювання:
// Створимо буфер
const buffer = new ArrayBuffer(1024 * 1024 * 10) // 10 MB
const view = new Float32Array(buffer)
// ... заповнити даними
// Передати без копіювання — після цього buffer у main thread недоступний
worker.postMessage({ type: 'PROCESS', payload: buffer }, [buffer])
// У Worker
self.onmessage = (event: MessageEvent) => {
const buffer = event.data.payload as ArrayBuffer
const view = new Float32Array(buffer)
// обробляємо...
// Повернути назад
self.postMessage({ type: 'DONE', payload: buffer }, [buffer])
}
Transferable: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream, WritableStream.
OffscreenCanvas — рендеринг у Worker
// main.ts
const canvas = document.getElementById('chart') as HTMLCanvasElement
const offscreen = canvas.transferControlToOffscreen()
worker.postMessage({ type: 'INIT_CANVAS', canvas: offscreen }, [offscreen])
worker.postMessage({ type: 'RENDER', data: chartData })
// chart-worker.ts
let ctx: OffscreenCanvasRenderingContext2D
self.onmessage = (event: MessageEvent) => {
const { type, canvas, data } = event.data
if (type === 'INIT_CANVAS') {
ctx = canvas.getContext('2d')!
return
}
if (type === 'RENDER') {
renderChart(ctx, data)
}
}
Пул Worker-ів
Для паралельної обробки кількох завдань:
class WorkerPool {
private workers: Worker[] = []
private queue: Array<{ resolve: Function; reject: Function; message: unknown }> = []
private idle: Worker[] = []
constructor(workerUrl: URL, poolSize = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerUrl, { type: 'module' })
worker.onmessage = (event) => this.onWorkerMessage(worker, event)
worker.onerror = (error) => this.onWorkerError(worker, error)
this.workers.push(worker)
this.idle.push(worker)
}
}
execute(message: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const task = { resolve, reject, message }
const worker = this.idle.pop()
if (worker) {
this.dispatch(worker, task)
} else {
this.queue.push(task)
}
})
}
private dispatch(worker: Worker, task: { resolve: Function; reject: Function; message: unknown }): void {
(worker as any).__resolve = task.resolve;
(worker as any).__reject = task.reject;
worker.postMessage(task.message)
}
private onWorkerMessage(worker: Worker, event: MessageEvent): void {
(worker as any).__resolve?.(event.data)
this.scheduleNext(worker)
}
private onWorkerError(worker: Worker, error: ErrorEvent): void {
(worker as any).__reject?.(new Error(error.message))
this.scheduleNext(worker)
}
private scheduleNext(worker: Worker): void {
const next = this.queue.shift()
if (next) {
this.dispatch(worker, next)
} else {
this.idle.push(worker)
}
}
terminate(): void {
this.workers.forEach((w) => w.terminate())
}
}
React Hook
function useWorker<TInput, TOutput>(workerUrl: URL) {
const workerRef = useRef<Worker>()
const [result, setResult] = useState<TOutput | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
workerRef.current = new Worker(workerUrl, { type: 'module' })
workerRef.current.onmessage = ({ data }) => {
setResult(data)
setLoading(false)
}
workerRef.current.onerror = (e) => {
setError(e.message)
setLoading(false)
}
return () => workerRef.current?.terminate()
}, [workerUrl.href])
const run = useCallback((payload: TInput) => {
setLoading(true)
setError(null)
workerRef.current?.postMessage(payload)
}, [])
return { run, result, error, loading }
}
Типові завдання для Workers
- Парсинг та трансформація великих CSV/JSON (>1MB)
- Шифрування/дешифрування
- Рендеринг Canvas-графіки та діаграми
- Обробка зображень (ресайз, фільтри, конвертація)
- Алгоритми пошуку та сортування на великих масивах
- Компресія даних (pako, zlib)
- Обчислення хешів (SHA-256, MD5)
- Raytracing, фізичні симуляції
Терміни: 1–2 дні залежно від складності завдань та необхідності пула Worker-ів.







