Реалізація Web Workers для фонових обчислень на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Web Workers для фонових обчислень на сайті
Середня
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Впровадження 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-ів.