Wolna aplikacja to droga aplikacja
Wydajność frontendu wpływa nie tylko na to, jak długo użytkownik czeka. Niepotrzebna praca oznacza również więcej ruchu sieciowego, więcej zapytań do backendu i czasem wyższy rachunek za infrastrukturę. Pierwszy problem jest zwykle widoczny. Drugi potrafi długo pozostać niezauważony.
Pomagam znaleźć źródło tej niepotrzebnej pracy i przekuć wnioski w konkretne zmiany w produkcie. Najwięcej doświadczenia mam z Vue i Nuxt, ale problemy, które analizuję, są znacznie szersze: kaskady requestów, zduplikowane pobieranie danych, cache'owanie, wykonanie JavaScriptu, renderowanie, rozmiar bundle'a oraz sposób, w jaki frontend komunikuje się z resztą systemu.
Jak szukam problemów z wydajnością
Nie zaczynam od listy wyników z Lighthouse. Najpierw patrzę na aplikację tak, jak widzi ją użytkownik, a potem śledzę dowody przez przeglądarkę, sieć i kod.
- Rzeczywiste doświadczenie użytkownika. Core Web Vitals, takie jak Largest Contentful Paint, Interaction to Next Paint i Cumulative Layout Shift, są przydatnymi sygnałami. Patrzę jednak przede wszystkim na strony i interakcje, które rzeczywiście mają znaczenie, zamiast optymalizować jeden wynik na stronie głównej.
- Requesty i kaskady. Szukam sytuacji, w których jedno wywołanie czeka na poprzednie, a także tych samych danych pobieranych przez kilka niezależnych części aplikacji.
- Bundle i code splitting. Sprawdzam, co trafia do pierwszego ładowania, co można odłożyć na później i które zależności dodają dużo ciężaru, nie dając w zamian wystarczającej wartości.
- Koszt JavaScriptu i renderowania. Powtarzające się obliczenia, niepotrzebna praca reaktywna, kosztowne pętle czy sposób renderowania, który zmusza przeglądarkę do wykonywania tej samej pracy częściej, niż to konieczne.
- Cache'owanie i ponowne wykorzystanie danych. Sprawdzam, czy zasoby i dane są sensownie cache'owane, czy klucze zapytań poprawnie opisują dane i czy aplikacja nie pyta backendu o informacje, które już ma.
- Aktualizacje w czasie rzeczywistym. Gdy aplikacja odbiera zdarzenia na żywo, np. przez SignalR, patrzę, jak wpływają one na istniejący stan i cache. Zmiana jednego fragmentu danych nie powinna powodować odświeżenia niezwiązanych z nią zapytań.
- Samo wymaganie. Zanim spróbuję przyspieszyć dany fragment, sprawdzam, czy jest on w ogóle potrzebny. Czy ekran naprawdę potrzebuje tych danych? Czy potrzebuje ich od razu? Najprostszy request do zoptymalizowania to często ten, którego aplikacja nie musi wysyłać.
Od znalezienia problemu do działającej poprawki
Dobry audyt powinien prowadzić do zmiany w produkcie, a nie tylko do raportu pełnego rekomendacji. Najbardziej wartościowe są dla mnie wnioski, które odpowiadają nie tylko na pytanie „co jest wolne?”, ale również „dlaczego?” i „co konkretnie trzeba zmienić?”.
Dobrym przykładem była duża aplikacja w Vue 2 przepisywana na Vue 3. Nowa wersja już przed wdrożeniem wykazywała problemy z wydajnością. Przyczyną były między innymi zagnieżdżone pętle, samonapędzające się watchery, używanie TanStack Query jako globalnej szyny zdarzeń oraz reaktywne zapytania, które tworzyły kaskady requestów.
Scentralizowałem klientów API, queries, mutations i query keys w jednej typowanej warstwie danych oraz zbudowałem mechanizm inwalidacji sterowany wiadomościami SignalR. Dzięki temu przepływ danych stał się jawny, a każde zdarzenie real-time mogło unieważniać tylko te zapytania, których rzeczywiście dotyczyło.
Efekt był prosty: pozostałe kaskady requestów zostały wyeliminowane. Więcej szczegółów znajduje się w case study o ratowaniu wydajności.
Gdy problem z wydajnością zaczyna się poza przeglądarką
Śledzenie requestu nie zawsze kończy się na frontendzie. Czasem prowadzi dalej — przez API, cache, konfigurację środowiska albo kod należący do innego zespołu.
Na jednej z platform, przy których pracowałem, developerzy korzystali z izolowanych klastrów feature'owych opartych na Dockerze i Kubernetesie. Każdy klaster miał własny pod przechowujący cache tłumaczeń. Platforma obsługiwała 28 aplikacji w 4 językach, a każda publikacja tłumaczeń powodowała pobieranie danych przez każdy z tych podów z płatnego API.
Przyczynę znalazłem w kodzie odpowiedzialnym za budowanie cache'u. Zawierał niepotrzebne pętle i po każdej publikacji pobierał cały zestaw tłumaczeń zamiast tylko tych, które faktycznie się zmieniły.
Poprawka należała do kodu innego zespołu, więc moim zadaniem była precyzyjna diagnoza i pokazanie, skąd bierze się niepotrzebna praca. Ostatecznie rozwiązanie polegało na pobieraniu tylko zmienionych tłumaczeń i utrzymywaniu wspólnego cache'u zamiast wielokrotnego pobierania tych samych danych przez każdy klaster.
Efekt: około 1000 requestów na publikację zostało ograniczonych do jednego wspólnego pobrania oraz requestów dotyczących faktycznie zmienionych danych, a liczba płatnych wywołań API spadła o 99,9%. Zaczęło się od analizy wydajności, a nie od zadania w backlogu. To częsty scenariusz: kosztowny problem najpierw pojawia się jako objaw, a dopiero później ktoś zaczyna szukać jego źródła.
Więcej szczegółów znajdziesz w case study o kosztach tłumaczeń w chmurze.
Jak sprawić, żeby poprawa nie zniknęła
Naprawienie jednej wolnej ścieżki to dopiero część pracy. Bez odpowiednich zabezpieczeń te same problemy mogą wrócić przy kolejnej funkcji.
- Automatyczne reguły. Linting i formatowanie z użyciem Biome, ESlint i Prettier, czy Oxlint i Oxfmt przenoszą możliwie dużo kontroli ze zwykłego code review do narzędzi, które działają automatycznie.
- Git hooki. Kontrole uruchamiane przed commitem i pushem wychwytują proste problemy, zanim trafią do pull requesta. Dzięki temu review może skupić się na zachowaniu i architekturze, a nie na formatowaniu.
- Typowana warstwa danych. Typowane query keys i jawny sposób dostępu do danych ułatwiają zrozumienie przepływu informacji i utrudniają przypadkowe obchodzenie przyjętych zasad.
- Kontrola zależności i architektury. Cykliczne zależności mogą utrudniać code splitting i niepotrzebnie zwiększać rozmiar bundle'a. Korzystam również z rds — otwartoźródłowego narzędzia w Rust, które napisałem do wykrywania cykli zależności w aplikacjach Node.js i TypeScript.
Wydajność w Nuxt
Nie każdy problem z wydajnością dotyczy requestów. W serwisie dla schroniska
dla zwierząt, który zbudowałem w Nuxt 4 i Payload CMS, pracowałem między
innymi nad responsywnym dostarczaniem obrazów z użyciem
@nuxt/image oraz critical CSS, wykorzystując Lighthouse do
mierzenia efektów zmian.
Nuxt jest jednym z obszarów mojego doświadczenia, ale nie definiuje całej usługi. Te same zasady mają zastosowanie wszędzie tam, gdzie przeglądarka pobiera, wykonuje, renderuje albo wielokrotnie przetwarza więcej pracy, niż aplikacja rzeczywiście potrzebuje.
Dla kogo jest ta usługa
Ta usługa jest szczególnie przydatna dla zespołów, które:
- mają aplikację frontendową, która wraz z rozwojem stała się wyraźnie wolniejsza,
- widzą rosnący ruch sieciowy, obciążenie backendu albo koszty infrastruktury, których trudno jednoznacznie wyjaśnić,
- przygotowują większy rewrite i chcą uniknąć przeniesienia tych samych problemów do nowej wersji,
- próbowały już oczywistych optymalizacji, ale potrzebują kogoś, kto prześledzi problem głębiej.
Najwięcej doświadczenia mam z Vue i Nuxt, ale sama analiza nie jest związana z jednym frameworkiem. Przeglądarka, requesty HTTP, cache'owanie, wykonanie JavaScriptu, renderowanie czy przepływ danych podlegają tym samym podstawowym zasadom niezależnie od użytej technologii.
Jeśli część Twojej platformy nadal działa na Vue 2, zobacz również migrację Vue 2 do Vue 3. Przy dużych platformach z wieloma aplikacjami szczególnie przydatne jest też moje doświadczenie z mikrofrontendami, zwłaszcza gdy zduplikowane requesty i współdzielone dane zaczynają być problemem w wielu aplikacjach jednocześnie.
Pracuję zdalnie, part-time, w modelu B2B. Napisz, który fragment Twojej aplikacji wydaje się najwolniejszy, a odpowiem w ciągu jednego dnia roboczego.