Многопоточность: Web Workers и передача состояния

В JavaScript выполнение кода происходит в однопоточном режиме: главный поток отвечает за обработку интерфейса, событий и выполнения скриптов. Любые тяжёлые вычисления блокируют рендеринг страницы, вызывая задержки в отклике интерфейса. Для решения этой проблемы используется многопоточность через Web Workers — отдельные потоки, выполняющие код параллельно основному потоку.

Web Worker создаётся через конструктор Worker с указанием файла скрипта:

const worker = new Worker('worker.js');

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

// Внутри воркера
self.onmess age = (event) => {
    const result = event.data * 2;
    self.postMessage(result);
};
// В главном потоке
worker.onmess age = (event) => {
    console.log('Результат из воркера:', event.data);
};

worker.postMessage(10);

Передача сообщений между потоками копирует данные, что важно учитывать при работе с большими структурами, например с состоянием физического движка.


Интеграция Cannon.js с Web Workers

Cannon.js — это библиотека для физического моделирования на JavaScript, которая часто используется для симуляции твёрдотельной динамики, столкновений и гравитации. Основная нагрузка в Cannon.js приходится на расчёт физических взаимодействий, что делает её идеальной для переноса вычислений в воркер.

Основная архитектура

  1. Главный поток отвечает за визуализацию через Three.js, Babylon.js или Canvas.
  2. Worker занимается расчётом физики: обновляет позиции тел, вычисляет столкновения и силы.
  3. Обмен состоянием между потоками осуществляется через массивы, объекты или SharedArrayBuffer для минимизации копирования данных.

Пример создания физического мира внутри воркера:

importScripts('cannon.min.js');

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);

const sphereBody = new CANNON.Body({
    mass: 5,
    shape: new CANNON.Sphere(1)
});
sphereBody.position.set(0, 10, 0);
world.addBody(sphereBody);

self.onmess age = (event) => {
    if (event.data.type === 'step') {
        world.step(1 / 60);
        self.postMessage({
            positions: world.bodies.map(body => body.position)
        });
    }
};

В главном потоке можно визуализировать результат, полученный из воркера:

worker.onmess age = (event) => {
    const positions = event.data.positions;
    positions.forEach((pos, idx) => {
        meshArray[idx].position.set(pos.x, pos.y, pos.z);
    });
};

function animate() {
    worker.postMessage({ type: 'step' });
    requestAnimationFrame(animate);
}
animate();

Передача состояния и оптимизация

Ключевая проблема при работе с воркерами — эффективная передача состояния между потоками. Простая передача объектов через postMessage создаёт глубокую копию, что может замедлять работу при больших массивах тел или сложных сценах.

Использование Transferable Objects

Cannon.js позволяет использовать массивы чисел (Float32Array, ArrayBuffer) для хранения позиции, ориентации и скоростей тел. Эти массивы можно передавать с помощью механизма transferable objects, который перемещает данные, а не копирует их:

const positions = new Float32Array(world.bodies.length * 3);

self.postMessage({ positions }, [positions.buffer]);

После передачи в главном потоке массив можно напрямую использовать для обновления графики без лишнего копирования.

SharedArrayBuffer

Для ещё более быстрой синхронизации можно применять SharedArrayBuffer, который позволяет двум потокам работать с одним и тем же массивом в памяти:

// Главный поток
const sharedBuffer = new SharedArrayBuffer(Float32Array.BYTES_PER_ELEMENT * bodiesCount * 3);
const sharedPositions = new Float32Array(sharedBuffer);

worker.postMessage({ sharedPositions, type: 'init' });

// В воркере
self.onmess age = (event) => {
    if(event.data.type === 'init') {
        positions = new Float32Array(event.data.sharedPositions);
    }
};

function updateWorld() {
    world.step(1/60);
    world.bodies.forEach((body, idx) => {
        positions[idx*3] = body.position.x;
        positions[idx*3 + 1] = body.position.y;
        positions[idx*3 + 2] = body.position.z;
    });
}

Преимущество SharedArrayBuffer — отсутствие копирования и минимальная задержка между потоками, что критично для высокопроизводительных симуляций.


Организация многопоточной симуляции Cannon.js

  1. Инициализация мира в воркере: создаются тела, силы и коллайдеры.
  2. Главный поток получает массивы состояния: позиции и ориентации тел.
  3. Воркер выполняет шаги физики: world.step(dt).
  4. Обновление графики в главном потоке: синхронизация позиций и вращений с визуальными объектами.
  5. Передача данных через transferable objects или SharedArrayBuffer для снижения накладных расходов.

Рекомендации по производительности:

  • Разделять визуализацию и физику на отдельные потоки.
  • Использовать Typed Arrays для состояния тел.
  • Минимизировать количество сообщений между потоками (например, обновлять позиции каждые 2–3 кадра вместо каждого кадра, если точность позволяет).
  • При большом количестве объектов применять уровни детализации и объединённые массивы данных.

Расширенные возможности

  • Много воркеров: при огромных сценах можно распределять тела по нескольким воркерам.
  • Поддержка кастомных коллайдеров: Cannon.js позволяет создавать сложные составные формы, которые тоже можно вычислять параллельно.
  • Интеграция с сетевыми играми: состояние физики, рассчитанное в воркере, можно отправлять на сервер или другим клиентам без блокировки основного потока.

Многопоточность через Web Workers значительно увеличивает производительность Cannon.js, особенно для больших сцен с сотнями тел и сложными столкновениями. Правильная организация передачи состояния, использование Typed Arrays и SharedArrayBuffer позволяют достичь плавного и отзывчивого взаимодействия графики и физики.