Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active July 15, 2026 22:40
Show Gist options
  • Select an option

  • Save PlugFox/ff2084a22b49b42aa92d0e45334b2cc3 to your computer and use it in GitHub Desktop.

Select an option

Save PlugFox/ff2084a22b49b42aa92d0e45334b2cc3 to your computer and use it in GitHub Desktop.
Dart vs TypeScript для WebGPU-игры на three.js

Dart vs TypeScript для WebGPU-игры на three.js

Сравнение TypeScript (Bun), Dart→JS (dart2js) и Dart→WASM (dart2wasm) как host-языков, гоняющих настоящий three.js WebGPURenderer. Все реализации крутят один и тот же движок; three.js — идентичное tree-shaken подмножество, так что разница в размере = код приложения + рантайм языка, а разница в CPU-времени = host-язык (GPU-путь общий).

Dart-сторона использует extension type + dart:js_interop поверх globalThis.THREE (ручные биндинги). TS импортирует three/webgpu напрямую.

Стенд и методология

  • Удалённый Chrome 150 / Windows, бэкенд real WebGPU, GPU NVIDIA Ada Lovelace (RTX 40). Дисплей 120 Гц (rAF-кап ~122 fps).
  • 4 режима-эксперимента × 3 реализации × свип по N. Метрики пишутся из host-кода и POST-ятся на сервер (results/report.jsonl). Детерминированный ворклоад (mulberry32 bit-exact, fixed dt), идентичный во всех реализациях (SPEC.md).
  • Шум среды: виртуалка даёт джиттер планировщика, avg/P95 загрязнены спайками. Опора на min (самый невытесненный кадр = чистая скорость кода) и P50. Интересны отношения между реализациями, не абсолютные числа.

TL;DR — вердикт

Ось Победитель Комментарий
Размер загрузки TS/Bun +2–5 КБ склейки vs +31–41 КБ рантайма Dart (фикс. налог, амортизируется)
Тесный числовой цикл (boids) dart2wasm ~10 % быстрее на масштабе
Тяжёлая ветвистая логика (fat) ничья преимущество wasm исчезает
Болтливый интероп (per-object) dart2js ≈ TS dart2wasm ~1.5–2× медленнее...
…но при батчинге (single buffer) ничья wasm-налог 1.8→0.3 мс, даже быстрее (Эксп.3b)
GPU/render-bound (gpu) ничья язык нерелевантен

Главный вывод: производительность зависит от ворклоада и от паттерна интеропа сильнее, чем от языка. Один и тот же dart2wasm бывает на ~10 % быстрее (bulk- upload) или на ~70 % медленнее (per-object writes) — решает архитектура границы JS.


Эксперимент 1 — boids: CPU-bound, тесный числовой цикл

Флокинг через spatial grid, одна InstancedMesh, bulk-upload матриц. simMs (min | P50), меньше лучше:

N ts dart2js dart2wasm fps ts/d2js/wasm
4 000 2.8 | 3.7 2.3 | 3.7 2.4 | 3.3 117 / 116 / 118
8 000 4.1 | 5.8 4.1 | 5.8 3.7 | 5.2 107 / 106 / 109
16 000 7.0 | 10.9 7.3 | 10.5 6.5 | 9.5 78 / 80 / 86
32 000 16.0 | 21.4 15.9 | 21.7 14.3 | 19.6 43 / 43 / 45

dart2wasm стабильно быстрее на ~10 % (WasmGC+AOT на числовом коде без JIT- прогрева). dart2js ≈ TS — ничья.

Эксперимент 2 — fat: раздутая игровая логика

Тот же рендер, но per-agent: видовой флокинг (3 вида, весовая матрица) + wander (hash-noise) + 20 препятствий + 10 хищников с бегством + 12 источников еды + FSM + энергетика. simMs (min | P50):

N ts dart2js dart2wasm
8 000 4.9 | 6.8 4.9 | 6.9 4.8 | 6.9
16 000 9.7 | 13.7 9.7 | 13.9 9.6 | 13.4
32 000 20.6 | 26.0 21.0 | 25.8 20.6 | 25.2

Все три равны. Ключевой нюанс: как только в горячем цикле появляется много ветвлений и трансцендентной математики (sin/cos/sqrt), преимущество wasm исчезает — тригонометрия и предсказание ветвлений у всех трёх упираются в одно и то же. Преимущество wasm специфично для плотных array-числодробилок (как boids). Fat при этом заметно тяжелее boids (~25–30 % на N=32k) — это честно «раздутая» логика, а не пустышка.

Эксперимент 3 — interop: болтливый per-object интероп

N отдельных Mesh, каждый кадр mesh.position.set() + mesh.rotation.set() — тысячи пересечений границы host↔JS. syncMs (P50) — стоимость именно интероп-записей:

N ts dart2js dart2wasm
2 000 0.2 0.2 0.3
4 000 0.5 0.4 0.8
8 000 1.2 1.0 1.7

dart2wasm платит больше всех за «болтливый» интероп (~1.5–2× от TS): каждый вызов position.set маршалит через границу WASM↔JS. dart2js — самый быстрый (компилит доступ к свойствам в прямой JS, V8 инлайнит). Это зеркало Эксп.1: там wasm выигрывал на bulk-upload, здесь проигрывает на per-object — паттерн интеропа важнее языка. (FPS при больших N тут упирается в число draw-call'ов three.js и одинаков у всех.)

Эксперимент 3b — batch: «правильный» интероп vs болтливый

Та же motion, что в interop (N орбитирующих объектов), но доставка на GPU через один матричный буфер InstancedMesh (одно пересечение границы за кадр) вместо N пар position.set/rotation.set. Сравнение в одной сессии:

N=8000 interop (per-object) batch (single buffer)
syncP50, мс — ts / d2js / wasm 1.4 / 1.0 / 1.8 0.0 / 0.0 / 0.3
cpuFrameP50, мс — ts / d2js / wasm 28.5 / 28.1 / 30.5 1.7 / 1.7 / 1.6
fps — ts / d2js / wasm 34.9 / 35.5 / 32.6 120.7 / 120.7 / 118.9

Налог wasm на интероп схлопывается: 1.8 → 0.3 мс (одно пересечение вместо тысяч), и в batch wasm даже самый быстрый по кадру. Батчинг — огромный выигрыш для всех трёх (draw-call'ов 1 вместо N: cpuFrame ~28→1.7 мс, fps 35→120), но wasm он помогает непропорционально сильнее, т.к. убирает пер-объектный маршалинг. Вывод: правильно спроектированная граница (bulk-передача типизированного буфера) делает dart2wasm полностью конкурентным — «интероп-обрыв» был артефактом болтливого паттерна, а не самого языка. На практике: тысячи объектов → InstancedMesh + матричный буфер, а не тысячи Object3D с сеттерами.

Эксперимент 4 — gpu: render/GPU-bound

Высокополигональные статичные инстансы (Icosahedron detail=6, ~82k tris каждый), 4× суперсэмплинг, ~0 CPU/кадр. FPS | cpuFrame P50:

N ts dart2js dart2wasm
8 000 120.6 | 0.2 120.9 | 0.2 120.6 | 0.2
16 000 66.7 | 0.2 65.0 | 0.2 63.5 | 0.2
32 000 23.9 | 0.2 23.3 | 0.1 23.0 | 0.1

Идентично у всех трёх. При N=16k–32k сцена реально GPU-bound (fps падает при CPU ~0.2 мс) — и язык на это не влияет вообще. Побочное наблюдение: этот RTX настолько быстр, что до 8k инстансов даже 82M tris + 4× SS упираются в vsync, а не в GPU — типичная three.js-сцена на топовом dGPU почти всегда CPU/vsync-bound.


Размер и амортизация рантайма

Brotli (q11), байты. «app-only» = вклад языка (three исключён):

app-only (lean, только boids) app-only (fat, 4 режима) Δ за 3 режима
ts 2 323 4 744 +2 421
dart2js 31 424 34 364 +2 940
dart2wasm 37 307 40 803 +3 496

Полная загрузка (fat-сборка), brotli: three (общий, 173.8 КБ) +

app итого
ts (three в бандле) 178 КБ
dart2js 34.4 КБ 208 КБ
dart2wasm 40.8 КБ 215 КБ

Про амортизацию. Рантайм Dart — фиксированный налог: разрыв (Dart − TS) по app-only держится ~29 КБ (dart2js) / ~35 КБ (wasm) независимо от размера кода. Поэтому относительный оверхед тает по мере роста приложения:

  • lean: dart2js/TS = 31.4 / 2.3 = 13.6×
  • fat: dart2js/TS = 34.4 / 4.7 = 7.3×
  • гипотетически ~100 КБ реального app-кода: ~131 / ~102 = ~1.3×

Добавление одинаковых фич стоит по коду сопоставимо во всех трёх (TS +2.4, dart2js +2.9, dart2wasm +3.5 КБ), так что для тонкой обёртки над three.js рантайм Dart — доминирующая цена, а для крупной игры он размывается почти в ноль. three.js (~174 КБ) в любом случае доминирует в бандле (~80–98 % итога).


Итоговый вердикт — что выгоднее

  • TypeScript + Bun — если это тонкая игра-обёртка над three.js, важен вес загрузки и DX: минимальный бандл, типы three бесплатно, ноль ручных биндингов, предсказуемая V8-скорость везде.
  • dart2wasm — если ядро игры это тяжёлая числовая симуляция в тесных циклах (частицы, физика, N-body) и/или нужен шаринг кода с Dart/Flutter. Даёт ~10 % на таком compute. Плати ~40 КБ рантайма и проектируй границу под bulk-передачу (матрицы в InstancedMesh-буфер, один crossing/кадр). При правильном батчинге wasm полностью конкурентен и даже самый быстрый (Эксп.3b: налог 1.8→0.3 мс); «интероп-обрыв» возникает только на болтливом per-object паттерне (тысячи mesh.position.set в кадр) — его и надо избегать, а не язык.
  • dart2js — самый безопасный Dart-выбор: скорость на уровне TS везде, лучший на болтливом интеропе, рантайм меньше wasm, максимальная совместимость. Берут ради переиспользования Dart-кода без «интероп-обрывов» wasm.

Если одним предложением: язык почти не решает — решают ворклоад и паттерн интеропа. three.js везде одинаков; на GPU-bound и на тяжёлой ветвистой логике все три равны; wasm выигрывает узко (плотный числовой код + bulk-upload) и проигрывает узко (болтливый per-object интероп); dart2js держит паритет с TS во всём, а TS выигрывает по весу за счёт отсутствия рантайма языка.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment