Scena three.js z niskim wynikiem w PageSpeed: dlaczego to nie JavaScript
Scena 3D chodzi u Ciebie płynnie, sześćdziesiąt klatek, zero zacięć. Wrzucasz stronę do PageSpeed Insights i dostajesz 48. Na czerwono świeci Total Blocking Time, a w rozbiciu wątku głównego wisi jedno długie zadanie podpięte pod gsap.min.js. Pierwsza myśl to „za dużo JavaScriptu" albo „za dużo cząstek". Obie są prawie zawsze błędne, a szukanie po nich potrafi zjeść cały dzień.
Ten wpis to diagnoza, nie recepta. Pokazuję, dlaczego ciężka scena three.js dostaje niski wynik w PageSpeed i gdzie naprawdę siedzi koszt. Jak to zabrzmi znajomo, na końcu jest link do gotowej metody.
PageSpeed nie ma karty graficznej
To jest klucz, od którego trzeba zacząć, bo zmienia całą resztę. Google nie mierzy Twojej strony na karcie graficznej. Lighthouse i PageSpeed uruchamiają bezgłowego Chrome, w którym WebGL idzie przez programowe rysowanie na procesorze (SwiftShader). Twoja scena, która na Twoim GPU liczy się w ułamku milisekundy, u Google liczy się na CPU.
Sprawdzisz to jednym odczytem nazwy renderera. Jeśli WEBGL_debug_renderer_info zwraca swiftshader, llvmpipe, softpipe albo software, to jest właśnie ten tryb. Tego samego doświadczają zresztą realni użytkownicy na maszynach wirtualnych i słabym sprzęcie. Dopóki mierzysz na własnej karcie, nie widzisz tego, co widzi Google.
Dlaczego to nie JavaScript
W rozbiciu pracy wątku głównego patrzysz na kategorie: „Script Evaluation", „Style & Layout", „Other". Przy ciężkiej scenie WebGL kategoria „Other" potrafi być większa niż „Script Evaluation". To nie jest worek bez znaczenia. Other to najczęściej czekanie na kartę graficzną i skanowanie pikseli, czyli praca, której profil skryptów w ogóle nie pokazuje.
Do tego dochodzi mylące nazewnictwo. Długie zadanie z etykietą gsap.min.js albo requestAnimationFrame to zwykle nie GSAP. W jednej klatce wszystkie wywołania requestAnimationFrame lądują w jednym zadaniu, które dostaje etykietę tego, kto je zaczął. Zegar GSAP rejestruje się pierwszy, więc bierze na siebie winę za cudzą pracę. Optymalizowanie GSAP w tym miejscu to strzelanie w zły cel.
Pierwsza klatka to najdroższa klatka
Scena z reguły rysuje na starcie jedną klatkę i czeka na ruch człowieka. Pomiar Google nie rusza myszą, więc widzi dokładnie tę jedną klatkę. A ta jedna klatka kompiluje wszystkie programy cieniowania naraz.
Na jednym z moich produkcyjnych wdrożeń scena miała czternaście programów: drobinki, łuki, kilka warstw poświaty, złożenie, wyjście. Każdy z nich kompilował się w pierwszej klatce, a biblioteka po każdej kompilacji pytała kartę o wynik. Każde takie pytanie czeka, aż karta przerobi całą kolejkę poleceń. Efekt: jedno zadanie 656 ms na maszynie Google i wynik 73 na komputerze. Total Blocking Time siedział w okolicach 700 ms. Nic z tego nie było JavaScriptem w sensie, w jakim myśli o nim większość poradników.
Cięcie cząstek nie pomaga, a psuje telefon
Najczęstszy odruch to zmniejszenie liczby cząstek „pod wydajność". To nieporozumienie na dwóch poziomach.
Po pierwsze, cząstki są tanie. Punkt to jeden wierzchołek. Drogie jest wypełnianie ekranu, czyli poświata, przezroczystość i nakładanie się warstw, a nie sama liczba punktów. Ucinając cząstki, tniesz tani zasób i zostawiasz drogi nietknięty.
Po drugie, obraz zbudowany z cząstek ma rozdzielczość równą ich liczbie. Napis z dziesięciu tysięcy drobinek wygląda inaczej niż ten sam napis z siedemdziesięciu tysięcy. Kiedy na telefonie dajesz dziesięć razy mniej cząstek „żeby było lżej", napis robi się rozmyty albo dziurawy, a i tak nie ruszasz prawdziwego kosztu. Na jednym wdrożeniu podniosłem liczbę cząstek na telefonie z siedmiu tysięcy do siedemdziesięciu dwóch tysięcy i napis wreszcie był ostry, a wynik się nie pogorszył, bo problem nigdy nie leżał w liczbie punktów.
Jak to zmierzyć, żeby nie gonić szumu
Zanim cokolwiek naprawisz, potrzebujesz miary, która nie kłamie. Tu są trzy pułapki, w które wpada prawie każdy.
| Co robisz | Dlaczego kłamie | Co robić zamiast tego |
|---|---|---|
| Profil w DevTools na swojej karcie | Drugie wczytanie ma programy z pamięci podręcznej dysku, kompilacja znika | Świeży profil bez pamięci, rysowanie programowe (--disable-gpu) |
| Jeden pomiar PageSpeed | Szum ±7 do 10 punktów, ten sam plik daje 80 i 95 | Mediana z trzech, próg istotności 7 punktów |
| Patrzenie na kategorie zamiast na zadania | „Other" nic Ci nie powie o przyczynie | Czytaj listę długich zadań z czasem startu |
Reguła jest prosta. Mierz w warunkach Google, nie na swojej karcie. Czytaj konkretne zadania, nie kategorie. Zmianę mniejszą niż siedem punktów traktuj jak przypadek, nie jak sukces. Dopiero z taką miarą ma sens dotykać sceny.
Co z tym zrobić
Naprawa istnieje i jest powtarzalna. W skrócie: pierwsza klatka nie może kompilować programów, więc kompilujesz je z wyprzedzeniem i rozgrzewasz w tle, we właściwych warunkach renderowania. Pętle po pikselach i budowa sceny nie mogą siedzieć w jednym zadaniu przy load. Telefon dostaje tę samą liczbę cząstek, lżejsze są tylko ujęcia bez napisu. Na tym samym wdrożeniu, na którym zaczynałem od 73, skończyłem na 97 na komputerze, z Total Blocking Time zbitym z 700 do 140 ms i pierwszą klatką z 656 ms do kilku milisekund, bez zmiany wyglądu sceny.
Całą metodę z gotowym kodem, protokołem pomiaru i skryptem do PageSpeed spakowałem jako gotowy skill dla programistów i agentów kodujących. Jeśli wolisz oddać robotę mnie, napisz i podeślij URL sceny - powiem wprost, gdzie siedzi koszt i ile realnie zdejmę. Ta sama wiedza o three.js stoi za moimi konfiguratorami produktowymi 3D, więc jeśli budujesz konfigurator i martwisz się o wynik PageSpeed, to jest dokładnie ten sam problem.
Często zadawane pytania
Czy niski wynik PageSpeed przy scenie WebGL szkodzi pozycji w Google?
Google rankinguje przede wszystkim po danych od realnych użytkowników (Core Web Vitals: LCP, CLS, INP), a nie po wyniku laboratoryjnym z PageSpeed. Sam <canvas> nie jest kandydatem na LCP, więc scena WebGL nie psuje LCP. Psuje za to Total Blocking Time, który jest miarą laboratoryjną, i realny INP, jeśli scena blokuje wątek podczas interakcji. Warto to naprawić dla użytkownika i dla wyniku, ale bez paniki, że jeden czerwony wskaźnik w PageSpeed od razu zrzuca stronę w wynikach.
Dlaczego długie zadanie jest podpisane jako GSAP, skoro nie używam GSAP intensywnie?
Bo etykieta długiego zadania wskazuje skrypt, który je zaczął, a nie ten, który wykonał pracę. Wszystkie wywołania requestAnimationFrame z jednej klatki idą w jednym zadaniu i dostają nazwę pierwszego z nich. Zegar GSAP rejestruje się wcześnie, więc bierze winę na siebie. Prawdziwym ciężarem jest zwykle kompilacja programów cieniowania w pierwszej klatce sceny. Nie optymalizuj GSAP na tej podstawie, bo naprawiasz nie ten element.
Czy wystarczy przenieść scenę do Web Workera albo OffscreenCanvas?
Zwykle nie i zwykle nie tędy droga jako pierwszy ruch. Web Worker i OffscreenCanvas to dzień pracy, a problem najczęściej rozwiązuje właściwa kolejność zadań i kompilacja programów z wyprzedzeniem, czyli zmiana na kilka godzin. Do Workera warto sięgać, kiedy naprawdę masz ciężką pracę CPU w pętli renderowania, a nie po to, żeby ukryć kompilację shaderów przed pomiarem.
Ile realnie da się ugrać na wyniku?
To zależy od sceny, kodu i hostingu, więc nie obiecuję liczby w ciemno. Na wdrożeniu, które opisuję, było to z 73 do 97 na komputerze i z 700 do 140 ms Total Blocking Time. Klucz to zdjęcie kompilacji z pierwszej klatki i uporządkowanie zadań przy starcie. Jeśli Twój niski wynik bierze się z zupełnie innego miejsca, na przykład z ciężkich obrazów albo z ładowania fontów, to osobna sprawa, którą też widać w pomiarze.
Czy da się to naprawić bez zmiany wyglądu sceny?
Tak i to jest cały sens. Metoda pilnuje, żeby obraz na komputerze został nietknięty co do procenta, a na telefonie był tak samo ostry, tylko mniejszy. Chodzi o kolejność i sposób rysowania, o to, kiedy kompilują się programy i co siedzi w pierwszej klatce, a nie o cięcie grafiki. Wynik podnosisz, a scena wygląda tak samo.
Napisz - wycenę odeślę do 48 h. Bez agencji, bez pośredników.
✉ napisz: dominik.gronski@grodev.pl