Проблема
Фізичні симуляції — це дорого: кожен кадр виконує крок фізики, оновлює дані буфера, малює геометрію та виконує фрагментні шейдери. На десктопі з дискретним GPU це тривіально. На середньому телефоні 2020 року з спільним тепловим бюджетом і 60-герцевим AMOLED це спричиняє помітні ривки, перегрів і швидке розряджання батареї.
Базове профілювання на Samsung Galaxy A52 показало, що симуляція рідини SPH просідала до 11 FPS при повній кількості частинок. Це було неприйнятно.
Крок 1: уніфіковані Pointer Events (більше не дотик проти миші)
Ранні симуляції використовували окремі слухачі
touchstart / mousedown. Це означало
дублювання коду, непослідовну поведінку та пропущені взаємодії,
коли браузер перетворював події дотику на події миші.
Рішенням стало перейти повністю на Pointer Events API, який об'єднує мишу, дотик і стилус в одну модель подій:
На сенсорних пристроях e.pointerType === 'touch'.
Мультитач реалізується через setPointerCapture та
відстеження кількох ідентифікаторів вказівника — той самий API, що
й для графічних планшетів.
Крок 2: адаптивні рівні якості
Ми визначаємо клас пристрою при запуску та встановлюємо пресет якості. Жодних дій користувача не потрібно.
Що насправді змінюють пресети
- Кількість частинок: квадратично зменшує обсяг фізичних обчислень (SPH має складність O(N²) без сітки).
- Карти тіней: повністю вимкнені на мобільних — економить цілий прохід рендерингу щокадру.
- Піксельне співвідношення: обмежене до 1.5 на середньому рівні та 1.0 на низькому — зменшення вдвічі скорочує обсяг фрагментної роботи в 4 рази.
- Роздільна здатність сітки хвиль: симуляції океану та мілководдя використовують сітку 64×64 на низькому рівні проти 256×256 на високому.
Крок 3: відкладене завантаження Three.js
Three.js r160 важить 624 КБ у мінімізованому вигляді. Завантажувати його при кожному відвідуванні сторінки — навіть тих, де користувач жодного разу не взаємодіє з симуляцією, — було марнотратно. Ми перейшли на динамічний імпорт, що запускається при першій взаємодії з вказівником:
Для симуляцій, які мають запускатися автоматично (як на головній сторінці), імпорт запускається після спрацювання IntersectionObserver — коли симуляція потрапляє в область видимості.
Крок 4: особливості viewport в iOS
Safari на iPhone має кілька особливостей поведінки, які ламають фізичні симуляції, якщо їх не обробити:
-
Динамічна панель інструментів: прокрутка змінює
висоту viewport, спричиняючи події зміни розміру canvas
посеред жесту. Рішення: використовувати висоту
window.visualViewport, а неwindow.innerHeight. -
Перевизначення відскоку прокрутки: стандартна
«гумова» прокрутка скасовує події
touchmove, необхідні для орбітального керування. Рішення:touch-action: noneна елементі canvas. - Обмеження розміру текстур WebGL у Safari: деякі старіші iPhone обмежують текстури до 4096×4096. Обмежуємо всі розміри цілей рендерингу.
-
Події тиску на пам'ять: iOS генерує
memorywarning(через міст-поліфіл), коли пам'яті мало. Ми реагуємо переходом на пресетlow.
Крок 5: тротлінг з урахуванням заряду батареї
Battery Status API дозволяє знижувати якість симуляції, коли телефон не заряджається, а рівень батареї низький — це запобігає витрачанню симуляцією останніх відсотків заряду:
Ми також реагуємо на подію visibilitychange: коли
вкладка прихована, requestAnimationFrame
автоматично зупиняється, але ми також призупиняємо цикл кроку
фізики, щоб не витрачати процесор у фоновому режимі даремно.
Результати
Чого ми не робили: фізичні рушії на WASM. Ми оцінили Rapier (Rust/WASM), але накладні витрати на серіалізацію масивів позицій через межу WASM щокадру перевищили виграш у обчисленнях для розмірів наших симуляцій (менше 2000 тіл). JavaScript-рушій Cannon-es, налаштований під мобільні пристрої, на практиці виявився швидшим.
Що далі
Наступний рубіж продуктивності — фізика у фоновому потоці (Web Workers + SharedArrayBuffer). Ми розповімо про архітектуру в нашому наступному пості з порадами.