← Браузер/Web Workers: многопоточность в браузере#185 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

Web Workers: многопоточность в браузере

JavaScript однопоточен — весь твой код выполняется в одном потоке. Если что-то тяжёлое занимает этот поток надолго, интерфейс «замерзает»: перестаёт реагировать на клики, не обновляется анимация. Web Workers решают эту проблему, позволяя запускать JS-код в отдельном потоке.

Главный поток vs рабочий поток

Главный поток (main thread) — один поток, где работает весь UI: DOM, события, CSS-анимации, React/Vue. Если ты запустишь здесь тяжёлые вычисления, страница зависнет.

Worker thread — отдельный поток без доступа к DOM. Идеален для долгих вычислений: обработки данных, шифрования, парсинга большого JSON, обработки изображений.

Создание и запуск Worker

// main.js — главный поток
const worker = new Worker('worker.js')

// Отправляем данные в worker
worker.postMessage({ type: 'compute', data: [1, 2, 3] })

// Получаем результат
worker.onmessage = (event) => {
  console.log('Результат:', event.data)
}

// worker.js — код в отдельном потоке
self.onmessage = (event) => {
  const result = heavyComputation(event.data)
  self.postMessage(result)
}

postMessage API

Главный поток и worker общаются только через сообщения. Данные копируются при передаче — изменение данных в worker не влияет на оригинал. Это предотвращает проблемы с параллельным доступом.

Можно передавать: примитивы, объекты, массивы, ArrayBuffer. Нельзя: функции, DOM-элементы, замыкания.

Transferable Objects

Для больших данных (например, изображений) копирование медленно. Вместо этого можно передать владение объектом через второй аргумент postMessage — данные перемещаются, а не копируются, и оригинал становится пустым:

const buffer = new ArrayBuffer(1024 * 1024)  // 1MB
worker.postMessage(buffer, [buffer])  // передаём владение
// buffer теперь пуст в главном потоке

SharedArrayBuffer

SharedArrayBuffer — буфер, который разделяется между потоками без копирования. Оба потока читают и пишут в один и тот же кусок памяти. Требует специальных заголовков безопасности (COOP/COEP) из-за Spectre-уязвимости. Для синхронизации используется Atomics API.

Типы Workers

  • Dedicated Worker — работает только с одной страницей
  • Shared Worker — может общаться с несколькими вкладками одного домена
  • Service Worker — специальный тип, перехватывает сетевые запросы (отдельная тема)
  • Ограничения Workers

    Workers не имеют доступа к:

  • document, window, DOM
  • localStorage (но есть доступ к IndexedDB)
  • Большинству браузерных APIs
  • Зато доступны: fetch, WebSocket, IndexedDB, crypto, setTimeout, console.

    Случаи применения

  • Парсинг и трансформация больших JSON
  • Шифрование/дешифрование данных
  • Обработка изображений (фильтры, ресайз)
  • Сложные математические вычисления (ML, физические симуляции)
  • Реалтайм-обработка аудио/видео данных
  • Примеры

    Симуляция Worker через EventEmitter: вычисление числа Фибоначчи в «отдельном потоке»

    // Симулируем Worker API через EventEmitter в Node.js
    // В браузере это работало бы через настоящие Web Workers
    
    class EventEmitter {
      constructor() { this._listeners = {} }
      on(event, fn) {
        (this._listeners[event] = this._listeners[event] || []).push(fn)
        return this
      }
      emit(event, data) {
        (this._listeners[event] || []).forEach(fn => fn(data))
      }
    }
    
    // --- Код «worker-потока» ---
    function workerThread(port) {
      port.on('message', (event) => {
        const { id, type, data } = event
    
        if (type === 'fibonacci') {
          // Тяжёлое вычисление — было бы не в главном потоке
          function fib(n) {
            if (n <= 1) return n
            return fib(n - 1) + fib(n - 2)
          }
          const result = fib(data)
          port.emit('message', { id, result })
        }
    
        if (type === 'primes') {
          // Найти все простые числа до n
          const sieve = new Array(data + 1).fill(true)
          sieve[0] = sieve[1] = false
          for (let i = 2; i * i <= data; i++) {
            if (sieve[i]) {
              for (let j = i * i; j <= data; j += i) sieve[j] = false
            }
          }
          const result = sieve.reduce((acc, v, i) => v ? [...acc, i] : acc, [])
          port.emit('message', { id, result })
        }
      })
    }
    
    // --- Код «главного потока» ---
    class SimulatedWorker {
      constructor() {
        this._mainPort = new EventEmitter()
        this._workerPort = new EventEmitter()
        this._pendingTasks = new Map()
        this._taskIdCounter = 0
    
        // Связываем порты — сообщение одному идёт к другому
        this._mainPort.on('message', (data) => this._workerPort.emit('message', data))
        this._workerPort.on('message', (data) => this._mainPort.emit('message', data))
    
        // Запускаем «worker»
        workerThread(this._workerPort)
    
        // Обрабатываем ответы worker
        this._mainPort.on('message', ({ id, result }) => {
          const resolve = this._pendingTasks.get(id)
          if (resolve) {
            resolve(result)
            this._pendingTasks.delete(id)
          }
        })
      }
    
      postMessage(type, data) {
        return new Promise((resolve) => {
          const id = ++this._taskIdCounter
          this._pendingTasks.set(id, resolve)
          this._mainPort.emit('message', { id, type, data })
        })
      }
    }
    
    // Использование
    const worker = new SimulatedWorker()
    
    console.log('Главный поток: запускаем тяжёлые вычисления...')
    
    const fib35 = await worker.postMessage('fibonacci', 35)
    console.log(`Fibonacci(35) = ${fib35}`)  // 9227465
    
    const primes = await worker.postMessage('primes', 50)
    console.log(`Простые до 50: ${primes.join(', ')}`)
    // 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47
    
    console.log('Главный поток: UI не зависал во время вычислений')

    Web Workers: многопоточность в браузере

    JavaScript однопоточен — весь твой код выполняется в одном потоке. Если что-то тяжёлое занимает этот поток надолго, интерфейс «замерзает»: перестаёт реагировать на клики, не обновляется анимация. Web Workers решают эту проблему, позволяя запускать JS-код в отдельном потоке.

    Главный поток vs рабочий поток

    Главный поток (main thread) — один поток, где работает весь UI: DOM, события, CSS-анимации, React/Vue. Если ты запустишь здесь тяжёлые вычисления, страница зависнет.

    Worker thread — отдельный поток без доступа к DOM. Идеален для долгих вычислений: обработки данных, шифрования, парсинга большого JSON, обработки изображений.

    Создание и запуск Worker

    // main.js — главный поток
    const worker = new Worker('worker.js')
    
    // Отправляем данные в worker
    worker.postMessage({ type: 'compute', data: [1, 2, 3] })
    
    // Получаем результат
    worker.onmessage = (event) => {
      console.log('Результат:', event.data)
    }
    
    // worker.js — код в отдельном потоке
    self.onmessage = (event) => {
      const result = heavyComputation(event.data)
      self.postMessage(result)
    }

    postMessage API

    Главный поток и worker общаются только через сообщения. Данные копируются при передаче — изменение данных в worker не влияет на оригинал. Это предотвращает проблемы с параллельным доступом.

    Можно передавать: примитивы, объекты, массивы, ArrayBuffer. Нельзя: функции, DOM-элементы, замыкания.

    Transferable Objects

    Для больших данных (например, изображений) копирование медленно. Вместо этого можно передать владение объектом через второй аргумент postMessage — данные перемещаются, а не копируются, и оригинал становится пустым:

    const buffer = new ArrayBuffer(1024 * 1024)  // 1MB
    worker.postMessage(buffer, [buffer])  // передаём владение
    // buffer теперь пуст в главном потоке

    SharedArrayBuffer

    SharedArrayBuffer — буфер, который разделяется между потоками без копирования. Оба потока читают и пишут в один и тот же кусок памяти. Требует специальных заголовков безопасности (COOP/COEP) из-за Spectre-уязвимости. Для синхронизации используется Atomics API.

    Типы Workers

  • Dedicated Worker — работает только с одной страницей
  • Shared Worker — может общаться с несколькими вкладками одного домена
  • Service Worker — специальный тип, перехватывает сетевые запросы (отдельная тема)
  • Ограничения Workers

    Workers не имеют доступа к:

  • document, window, DOM
  • localStorage (но есть доступ к IndexedDB)
  • Большинству браузерных APIs
  • Зато доступны: fetch, WebSocket, IndexedDB, crypto, setTimeout, console.

    Случаи применения

  • Парсинг и трансформация больших JSON
  • Шифрование/дешифрование данных
  • Обработка изображений (фильтры, ресайз)
  • Сложные математические вычисления (ML, физические симуляции)
  • Реалтайм-обработка аудио/видео данных
  • Примеры

    Симуляция Worker через EventEmitter: вычисление числа Фибоначчи в «отдельном потоке»

    // Симулируем Worker API через EventEmitter в Node.js
    // В браузере это работало бы через настоящие Web Workers
    
    class EventEmitter {
      constructor() { this._listeners = {} }
      on(event, fn) {
        (this._listeners[event] = this._listeners[event] || []).push(fn)
        return this
      }
      emit(event, data) {
        (this._listeners[event] || []).forEach(fn => fn(data))
      }
    }
    
    // --- Код «worker-потока» ---
    function workerThread(port) {
      port.on('message', (event) => {
        const { id, type, data } = event
    
        if (type === 'fibonacci') {
          // Тяжёлое вычисление — было бы не в главном потоке
          function fib(n) {
            if (n <= 1) return n
            return fib(n - 1) + fib(n - 2)
          }
          const result = fib(data)
          port.emit('message', { id, result })
        }
    
        if (type === 'primes') {
          // Найти все простые числа до n
          const sieve = new Array(data + 1).fill(true)
          sieve[0] = sieve[1] = false
          for (let i = 2; i * i <= data; i++) {
            if (sieve[i]) {
              for (let j = i * i; j <= data; j += i) sieve[j] = false
            }
          }
          const result = sieve.reduce((acc, v, i) => v ? [...acc, i] : acc, [])
          port.emit('message', { id, result })
        }
      })
    }
    
    // --- Код «главного потока» ---
    class SimulatedWorker {
      constructor() {
        this._mainPort = new EventEmitter()
        this._workerPort = new EventEmitter()
        this._pendingTasks = new Map()
        this._taskIdCounter = 0
    
        // Связываем порты — сообщение одному идёт к другому
        this._mainPort.on('message', (data) => this._workerPort.emit('message', data))
        this._workerPort.on('message', (data) => this._mainPort.emit('message', data))
    
        // Запускаем «worker»
        workerThread(this._workerPort)
    
        // Обрабатываем ответы worker
        this._mainPort.on('message', ({ id, result }) => {
          const resolve = this._pendingTasks.get(id)
          if (resolve) {
            resolve(result)
            this._pendingTasks.delete(id)
          }
        })
      }
    
      postMessage(type, data) {
        return new Promise((resolve) => {
          const id = ++this._taskIdCounter
          this._pendingTasks.set(id, resolve)
          this._mainPort.emit('message', { id, type, data })
        })
      }
    }
    
    // Использование
    const worker = new SimulatedWorker()
    
    console.log('Главный поток: запускаем тяжёлые вычисления...')
    
    const fib35 = await worker.postMessage('fibonacci', 35)
    console.log(`Fibonacci(35) = ${fib35}`)  // 9227465
    
    const primes = await worker.postMessage('primes', 50)
    console.log(`Простые до 50: ${primes.join(', ')}`)
    // 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47
    
    console.log('Главный поток: UI не зависал во время вычислений')

    Задание

    Реализуй createWorkerPool(workerCount) — пул из нескольких «воркеров», которые параллельно обрабатывают задачи. Метод submit(taskType, data) возвращает Promise с результатом. Метод getStats() возвращает объект {active, queued, completed}. Воркеры симулируются через setTimeout. Каждый воркер может выполнять только одну задачу одновременно.

    Подсказка

    workers.find(w => !w.busy) найдёт свободного воркера. Для reverse задачи используй data.split("").reverse().join(""). Случайная задержка: Math.floor(Math.random() * 100) + 50. Не забудь увеличить счётчики stats при добавлении в очередь и уменьшить при завершении.

    Загружаем среду выполнения...
    Загружаем AI-помощника...