Problem
Symulacje fizyczne są kosztowne: każda klatka wykonuje krok fizyki, aktualizuje dane buforów, rysuje geometrię i wykonuje shadery fragmentów. Na desktopie z dedykowanym GPU to trywialne. Na telefonie średniej klasy z 2020 roku, dzielącym budżet termiczny i wyświetlaczem AMOLED 60 Hz, powoduje to widoczne zacinanie, przegrzewanie i drenaż baterii.
Nasze bazowe profilowanie na Samsungu Galaxy A52 pokazało, że płyn SPH spadał do 11 FPS przy pełnej liczbie cząstek. To było nie do przyjęcia.
Krok 1: ujednolicone zdarzenia wskaźnika (koniec touch vs mouse)
Wczesne symulacje używały osobnych nasłuchiwaczy
touchstart / mousedown. Oznaczało to
zduplikowany kod, niespójne zachowanie i pominięte interakcje,
gdy przeglądarka konwertowała zdarzenia dotykowe na zdarzenia
myszy.
Rozwiązaniem było przełączenie wszystkiego na Pointer Events API, które ujednolica mysz, dotyk i rysik w jednym modelu zdarzeń:
Na urządzeniach dotykowych e.pointerType === 'touch'.
Multi-touch jest dostępny przez setPointerCapture i
śledzenie wielu identyfikatorów wskaźników — to samo API co dla
tabletów piórkowych.
Krok 2: adaptacyjne poziomy jakości
Wykrywamy klasę urządzenia przy starcie i ustawiamy preset jakości. Bez żadnej akcji użytkownika.
Co faktycznie zmieniają presety
- Liczba cząstek: redukuje pracę fizyki kwadratowo (SPH jest O(N²) bez siatki).
- Mapy cieni: całkowicie wyłączone na mobile — oszczędza pełny przebieg renderowania w każdej klatce.
- Współczynnik pikseli: ograniczony do 1,5 na mid i 1,0 na low — zmniejszenie o połowę redukuje pracę fragmentów 4-krotnie.
- Rozdzielczość siatki fal: symulacje oceanu i płytkiej wody używają siatki 64×64 na low vs 256×256 na high.
Krok 3: leniwe ładowanie Three.js
Three.js r160 waży 624 KB po minifikacji. Ładowanie go przy każdej wizycie na stronie — w tym na stronach, gdzie użytkownik nigdy nie wchodzi w interakcję z symulacją — było marnotrawstwem. Przełączyliśmy się na dynamiczny import wyzwalany pierwszą interakcją wskaźnika:
Dla symulacji, które muszą uruchamiać się automatycznie (jak strona główna), import jest wyzwalany po tym, jak IntersectionObserver się uruchomi — gdy symulacja wchodzi w obszar widoczny.
Krok 4: dziwactwa viewportu iOS
Safari na iPhonie ma kilka zachowań, które psują symulacje fizyczne, jeśli nie są obsłużone:
-
Dynamiczny pasek narzędzi: przewijanie zmienia
wysokość viewportu, powodując zdarzenia zmiany rozmiaru canvasu
w środku gestu. Rozwiązanie: użyj wysokości
window.visualViewport, a niewindow.innerHeight. -
Nadpisanie odbicia przewijania: domyślne
gumowe przewijanie anuluje zdarzenia
touchmovepotrzebne do kontrolek orbitalnych. Rozwiązanie:touch-action: nonena elemencie canvas. - Limit rozmiaru tekstur WebGL w Safari: niektóre starsze iPhone'y ograniczają tekstury do 4096×4096. Ogranicz wszystkie rozmiary celów renderowania.
-
Zdarzenia presji pamięci: iOS wywołuje
memorywarning(przez mostek polyfill), gdy RAM jest niski. Odpowiadamy, przełączając się na presetlow.
Krok 5: throttling świadomy baterii
Battery Status API pozwala zmniejszyć jakość symulacji, gdy telefon nie jest ładowany, a poziom baterii jest niski — zapobiegając drenowaniu przez symulację ostatnich kilku procent naładowania:
Reagujemy też na zdarzenie visibilitychange: gdy
karta jest ukryta, requestAnimationFrame
automatycznie się zatrzymuje, ale wstrzymujemy też pętlę kroku
fizyki, by nie marnować CPU w tle.
Wyniki
Czego nie zrobiliśmy: silników fizyki WASM. Oceniliśmy Rapier (Rust/WASM), ale narzut serializacji tablic pozycji przez granicę WASM w każdej klatce przewyższał oszczędności obliczeniowe dla rozmiarów naszych symulacji (<2000 ciał). JavaScriptowy Cannon-es, dostrojony pod mobile, okazał się w praktyce szybszy.
Co dalej
Kolejnym frontem wydajności jest fizyka w wątku w tle (Web Workery + SharedArrayBuffer). Omówimy architekturę w nadchodzącym wpisie ze wskazówkami.