WASM + WebGPU: ризики та переваги
WebAssembly дає браузеру майже нативне виконання CPU-коду; WebGPU дає сучасний low-overhead GPU API з повноцінними compute-шейдерами. Разом вони дозволяють запускати числове ядро фізичної симуляції у WASM, а рендеринг чи обчислення частинок — на GPU, без жодного нативного інсталятора. Проблема в тому, що "майже нативне" і "сучасне" мають гострі кути: копіювання пам'яті через межу JS, нерівномірне впровадження WebGPU по браузерах і білд-ланцюжок, що досі дозріває. Ця стаття розбирає архітектуру, реальні цифри продуктивності і коли додаткова складність себе виправдовує.
1. Навіщо поєднувати WASM і WebGPU
WebAssembly і WebGPU вирішують два різні вузькі місця. WASM замінює повільний, непередбачувано JIT-компільований JavaScript для числових обчислень (детекція зіткнень, розв'язувачі жорстких тіл, просторове хешування, розв'язання constraint'ів тканини) на компактний байткод, що працює у 1.3–3× швидше за ретельно оптимізований JS і з набагато стабільнішим часом кадру — без GC-пауз у гарячих циклах, написаних на Rust чи C++ з ручним керуванням пам'яттю. WebGPU замінює трюки WebGL з compute-обчисленнями лише через фрагментні шейдери (кодування даних у текстури, щоб імітувати загальні GPU-обчислення) на справжні compute-шейдери, storage-буфери та явну синхронізацію.
Жодна з цих технологій сама по собі не вирішує всю проблему для серйозної симуляції. Розв'язувач тканини потребує CPU-прохід broad-phase колізій (нерегулярний потік керування, важко розпаралелити на GPU) і GPU-прохід масового оновлення частинок (легко паралелиться, тисячі вершин за кадр). Розділення роботи саме по цій лінії розлому — CPU-дружня логіка у WASM, GPU-дружня математика у WGSL compute — і є патерн, який ця стаття називає "гібридним рушієм".
Сильні сторони WASM
Передбачувана продуктивність, без GC-джитеру, повторне використання наявного C++/Rust фізичного коду, SIMD128 для векторизованої математики.
Сильні сторони WebGPU
Справжні compute-шейдери, storage-буфери, явні пайплайни, краще багатопотокове записування команд ніж у WebGL.
Спільна слабкість
Кожен перехід через межу WASM↔JS↔GPU коштує копіювання чи точки синхронізації — архітектура має мінімізувати ці переходи.
2. Референсна архітектура: CPU-ядро + GPU-обчислення
Типовий гібридний цикл симуляції тримає живими три буфери: WASM linear memory (позиції/швидкості частинок як плаский Float32Array), GPU storage-буфер, що дзеркалить той самий layout, і невеликий JS клейовий шар, що копіює між ними лише раз за кадр — не на кожну частинку.
class HybridSimulation {
async init() {
// 1. Ініціалізуємо WASM-модуль (скомпільований Rust/C++ solver)
const { instance } = await WebAssembly.instantiateStreaming(
fetch('solver.wasm'), { env: this.importObject() }
);
this.wasm = instance.exports;
this.particlesPtr = this.wasm.alloc_particles(100000);
// 2. Отримуємо WebGPU-девайс + storage-буфер того ж розміру
const adapter = await navigator.gpu.requestAdapter();
this.device = await adapter.requestDevice();
this.gpuBuffer = this.device.createBuffer({
size: 100000 * 4 * 4, // vec4 на частинку
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
}
step(dt) {
// CPU: розв'язання constraint'ів, broad-phase колізії, контакти (нерегулярне)
this.wasm.solve_constraints(this.particlesPtr, dt);
// Одне копіювання за кадр: WASM linear memory → GPU staging
const view = new Float32Array(
this.wasm.memory.buffer, this.particlesPtr, 100000 * 4
);
this.device.queue.writeBuffer(this.gpuBuffer, 0, view);
// GPU: масово-паралельне інтегрування + рендеринг (регулярне, легко паралелиться)
this.dispatchComputePass();
}
}
Ключова дисципліна: WASM володіє нерегулярною, розгалуженою логікою; WGSL compute володіє регулярною, паралельною математикою. JS клейовий шар робить рівно одне завантаження буфера за кадр, а не на кожен об'єкт.
3. Межа пам'яті: linear memory vs GPU-буфери
Linear memory WASM — це один суцільний ArrayBuffer, читається з JS
напряму без жодного маршалінгу — саме тому межа WASM↔JS дешева
порівняно, скажімо, з викликом нативного addon'у. Межа JS↔GPU —
зовсім інша історія: queue.writeBuffer запускає
копіювання на рівні драйвера з CPU RAM у VRAM (або швидкий шлях
через спільну пам'ять на інтегрованих GPU), а читання даних назад
(mapAsync) асинхронне і може заблокувати кадр, якщо
користуватись ним необережно.
Модель вартості за кадр (порядок величини, дискретна GPU, PCIe 4.0):
Створення view WASM↔JS: ~0 (та сама пам'ять, без копіювання)
JS→GPU writeBuffer (1 МБ): ~0.05–0.2 мс
GPU→JS mapAsync readback (1 МБ): ~1–3 мс + ризик зупинки пайплайна
Правило: завантажуй кожен кадр, читай назад тільки коли неминуче (напр. GPU picking, debug)
Уникнення round-trip'ів
Найбільший вбивця продуктивності у гібридних рушіях — round-trip CPU→GPU→CPU всередині кадру (наприклад, запуск фізики на GPU, а потім читання результатів назад у WASM для реакції на зіткнення). Де можливо, тримайте весь ланцюжок залежностей кадру з одного боку: або GPU compute напряму годує GPU-рендеринг (без readback), або WASM обчислює все і лише фінальний vertex-буфер іде на GPU один раз.
4. Підтримка WebGPU у браузерах та fallback
WebGPU за замовчуванням запрацював у Chrome/Edge (2023), а Firefox і Safari додали власну підтримку за своїми графіками протягом 2024–2025 років; покриття тепер широке на десктопі, але все ще нерівномірне на деяких мобільних браузерах і старіших версіях Safari. Будь-яка продакшн-симуляція потребує fallback-шляху на WebGL2 — це означає або підтримку двох рендер-бекендів, або використання бібліотеки-абстракції, що компілює WGSL-подібний код в обидві цілі.
| Можливість | WebGL2 | WebGPU |
|---|---|---|
| Compute-шейдери | Ні (лише хаки з текстурами) | Так, повноцінно |
| Storage-буфери | Ні | Так |
| Багатопотокове записування команд | Ні | Так |
| Bindless-подібне прив'язування ресурсів | Лише через розширення | Bind groups (нативно) |
| Мова шейдерів | GLSL ES | WGSL |
navigator.gpu,
і якщо його немає — переключаємось на WebGL2-рендерер, що читає той
самий буфер, згенерований WASM, через bufferSubData.
Ядро симуляції у WASM не повинно знати, який бекенд його рендерить.
5. Бенчмарки: WASM проти JS проти нативного коду
Цифри сильно варіюються залежно від навантаження, але у опублікованих бенчмарках для числового, без-розгалужень коду (SIMD-дружнє інтегрування частинок, матрична математика) з'являється стійкий патерн: WASM опиняється приблизно на 50–80% швидкості нативного C++ і у 1.5–4× швидше за еквівалентний гарячий цикл JavaScript — здебільшого через уникнення деоптимізації типів і передбачувану розкладку пам'яті. Для коду з великою кількістю розгалужень (обхід дерев, побудова BVH) розрив з нативним кодом зростає, оскільки overhead неінлайнованих викликів функцій та перевірок меж пам'яті у WASM важить більше.
| Навантаження | JS (V8, прогріте) | WASM | Нативний код |
|---|---|---|---|
| Verlet-інтегрування 10к частинок | 1× | ~2.5× | ~3.5× |
| Побудова BVH (100к трикутників) | 1× | ~1.8× | ~3× |
| SIMD dot-product обчислення | 1× | ~4× | ~5× |
Висновок: WASM — це не "безкоштовна нативна швидкість", а велике, надійне покращення над JS для числового коду, причому SIMD128 звужує розрив з нативним кодом ще більше. Чи варта ця складність білду — залежить від того, чи числове ядро справді є вузьким місцем; спочатку профілюйте.
6. Типові пастки
Зростання WASM-пам'яті
memory.grow() може зробити недійсними всі наявні
Float32Array-view у ній. Пересоздавайте view після кожного
зростання, або одразу виділяйте великий фіксований розмір.
Асинхронність WebGPU всюди
Створення девайсу, мапінг буфера і створення пайплайну — усе на Promise. Очікування їх усередині рендер-циклу блокує кадри — компілюйте пайплайни лише при ініціалізації.
Непрозорість дебагу
Крах усередині скомпільованого Rust/C++ проявляється як незрозуміла WASM-пастка (trap). Тримайте source maps і debug-варіант білду; не дебажте наосліп у release WASM.
Розмір бандла
Повноцінний фізичний рушій, скомпільований у WASM, може важити
500КБ–2МБ. Використовуйте wasm-opt, зрізайте
debug-інформацію і завантажуйте модуль ліниво за взаємодією
"почати симуляцію".
7. Коли ця комбінація виправдана
Не кожній симуляції потрібні обидві технології одразу. Невелика демонстрація з кількома тисячами частинок чудово працює на чистому JS + WebGL. Гібридна архітектура WASM+WebGPU виправдовує свою складність, коли справджуються принаймні два з цих пунктів: CPU-логіка є справжнім вузьким місцем (виміряним профайлером, а не припущеним), навантаження має велику легко-паралельну GPU-складову, що виграє від справжніх compute-шейдерів, і проєкт може дозволити собі підтримувати WebGL2 fallback для ~10–20% відвідувачів на браузерах без WebGPU.
Для власних важчих демонстрацій MySimulator — великих N-body систем, тканини з десятками тисяч constraint'ів, SPH-рідин — це саме та форма проблеми: WASM обробляє розв'язувач і просторові структури, WebGPU compute обробляє масово-паралельне інтегрування та прохід рендерингу.