Strona główna / Artykuły

Budżety wydajnościowe dla JavaScript: egzekwowanie ograniczeń pakietów w CI

Dowiedz się, dlaczego JavaScript kosztuje znacznie więcej niż czas jego pobierania, jak określić realistyczny budżet wydajności oraz jak sprawić, by webpack odrzucał projekty przekraczające ten budżet.

Wydawca: REACTAPP.TOP7 min czytania1503 słów

Źródła: The Cost of JavaScript — Performance budgets and why your bundle size is driving users away

Strona, która działa natychmiastowo na laptopie programisty, może się zatrzymać na kilka sekund w telefonie rzeczywistego klienta. Różnica rzadko wynika wyłącznie ze szybkości sieci – pochodzi od obliczeń CPU, jakie są wymagane przy przetwarzaniu każdego kilobajtu kodu JavaScript w urządzeniu o ograniczonych możliwościach. Ten przewodnik wyjaśnia, skąd bierze się ten koszt, jak przekształcić go w wyraźny budżet wydajności oraz jak zaprogramować proces budowania aplikacji tak, by odrzucał zmiany naruszające ten budżet, zanim dotrą one do wersji produkcyjnej.

Różnica pomiędzy maszyną programisty a urządzeniem użytkownika

Większość zespołów zajmujących się frontendem buduje i testuje aplikacje na sprzęcie, który w ogóle nie jest reprezentatywny: nowoczesnych laptopach, połączeniach przewodowych lub światłowodowych oraz serwerze deweloperskim uruchamianym na localhost. W takich warunkach niemal każdy plik wydaje się szybki, dlatego rozmiar tych plików jest zwykle traktowany jako coś, co można poprawić później, a nie jako ograniczenie projektowe.

Prawdziwy ruch jest inny. Duży odsetek odwiedzających korzysta z telefonów Android średniej półki, niestabilnego 4G lub przepełnionego publicznego Wi-Fi. Weźmy pod uwagę panel sterowania wymagający dużej ilości danych, który natychmiast reaguje w lokalnym przeglądarce, natomiast podczas testów na starszych iPhone’ach interaktywne elementy sterujące potrzebują około trzech sekund, aby stać się użyteczne. W kodzie nie ma żadnych błędów; urządzenie po prostu potrzebuje znacznie więcej czasu na przetworzenie tego samego skryptu.

To opóźnienie ma konsekwencje biznesowe. Nadmiar nieoptymalizowanego JavaScriptu stopniowo opóźnia moment, w którym użytkownik może zacząć interakcję, a efekt jest najbardziej odczuwalny w kluczowych procesach, takich jak logowanie lub dodawanie produktu do koszyka. Gdy te kroki wydają się powolne, ludzie rezygnują z nich, dlatego nawet niewielki wzrost rozmiaru pliku może po cichu odstraszyć użytkowników.

Dlaczego JavaScript kosztuje więcej niż czas jego pobierania

Przez wiele lat głównym ograniczeniem była opóźniona praca sieci, a prace nad wydajnością koncentrowały się na zmniejszaniu masy plików obrazowych oraz ulepszeniach w transporcie danych, takich jak HTTP/2. Te kwestie nadal są istotne, ale jeśli chodzi o JavaScript, większym problemem obecnie jest to, co dzieje się po przybyciu bajtów.

Skrypt nie może być używany od razu po zakończeniu pobierania. Przeglądarka musi go przetworzyć, skompilować i wykonać, a wiele z tych operacji odbywa się na głównej wątku, tym samym wątku odpowiedzialnym za obsługę dotyków, kliknięć i renderowanie. Całkowicie możliwe jest, że plik o wielkości 400 KB zostanie szybko pobraany, a strona nadal będzie wydawać się nieresponsywna, ponieważ urządzenie jest zajęte przetwarzaniem kodu zamiast reagować na dane wejściowe.

W telefonie średniej półki ten cykl analizy, kompilacji i wykonywania może znacząco pogorszyć Interaction to Next Paint (INP) – wskaźnik mierzący szybkość reakcji strony na działania użytkownika. Wpływa to bezpośrednio na oceny Core Web Vitals, a słabe wyniki tych wskaźników zwykle idą w parze z niższym retencją użytkowników i gorszymi wynikami biznesowymi. Dlatego wszelkie poważne działania zmierzające do poprawy tych wskaźników muszą obejmować redukcję ilości JavaScriptu, który urządzenia mobilne muszą analizować i wykonywać, a nie tylko zmniejszanie rozmiaru plików do pobrania.

Czym jest budżet wydajności i jak go ustalić

Budżet wydajności JavaScript to wyraźna granica określająca, ile kodu skryptowego może zawierać strona, ścieżka lub fragment aplikacji. Technicznie jest to po prostu liczba, ale jego rzeczywista wartość leży w aspekcie organizacyjnym – stanowi umowę, którą podpisuje cały zespół. Zamiast odkrywać regresje po wydaniu i usiłować je naprawić, budżet pełni rolę zabezpieczenia, które zapobiega ich włączeniu do kodu od samego początku.

Praktyczna wskazówka: Budżet przekształca kwestię wydajności z czegoś, na co reagujemy, w ograniczenie, które uwzględniamy podczas projektowania, tak samo jak wymagania dotyczące dostępności lub bezpieczeństwa.

Ustalanie takich ograniczeń zaczyna się od wybrania konkretnych, mierzalnych granic. Powszechnie przyjętym celem jest utrzymanie rozmiaru skompresowanego kodu JavaScript na ścieżce krytycznej każdej strony poniżej około 170 KB – wartość ta została wybrana tak, aby przeciętny urządzenie mobilne mogło szybko osiągnąć stan interaktywny. Należy zweryfikować takie liczby pod kątem specyfiki własnej publiczności, ale zasada pozostaje ta sama: gdy granica zostanie ustalona, każda nowa zależność i funkcja musi od pierwszego dnia uzasadnić swój rozmiar.

Kilka praktycznych wskazówek ułatwia zarządzanie budżetem:

  • Budżet przypisany do poszczególnych ścieżek lub punktów wejścia, a nie tylko do całej aplikacji, dzięki czemu obszar administracyjny o dużym obciążeniu nie może pochłonąć środków przeznaczonych na stronę startową.
  • Mierzenie zawsze odbywa się w ten sam sposób (rozmiar po skompresowaniu w porównaniu z rozmiarem surowym), aby liczby były porównywalne pomiędzy różnymi wersjami aplikacji.
  • Należy ponownie przeanalizować te ograniczenia, gdy zmienia się skład publiczności lub urządzeń, zamiast cicho je podnosić za każdym razem, gdy coś pójdzie nie tak podczas budowania aplikacji.

Autoryzacja zabezpieczeń w CI/CD

Budżety, od których zależy to, czy ktoś pamięta o ich sprawdzeniu, nie przetrwają długo. Wprowadza się nową funkcję, recenzent skupia się na logice, nikt nie sprawdza raportu o plikach, w rezultacie pojawia się regresja. Jedynym rozwiązaniem skalowalnym jest bezpośrednie włączenie tej kontroli do procesu, dzięki czemu każda zmiana jest automatycznie mierzona.

Uwaga: Dzięki temu, że kontrola odbywa się w ramach CI, każda prośba o pull request jest weryfikowana pod kątem budżetu, bez konieczności pamiętania o jej sprawdzeniu.

Narzędzia do budowania, takie jak Webpack, mogą to za Ciebie zrobić: konfiguruj je tak, aby nadmierne rozmiary pliku wyjściowego traktowały jako błąd budowania, a nie jako ostrzeżenie. Informacja zwrotna pojawia się natychmiast, dołączona do zmiany, która ją spowodowała. Jeśli prośba o połączenie wprowadza bibliotekę, która generuje fragment przekraczający ustalony limit, proces budowania zmienia kolor na czerwony i autor musi to naprawić przed fuzją.

Konfiguracja poniżej wykorzystuje opcję performance z webpacka. Ustawienie hints na 'error' przekształca ostrzeżenia dotyczące rozmiaru plików w błędy, które powodują nieudane skompilowanie. maxAssetSize określa maksymalny rozmiar pojedynczego wygenerowanego pliku (tutaj 250 KiB), natomiast maxEntrypointSize określa maksymalny rozmiar łącznych zasobów załadowanych dla jednego punktu wejścia (tutaj 500 KiB); obie wartości są wyrażone w bajtach. Funkcja assetFilter decyduje, które pliki w ogóle mają być sprawdzane – w tym przypadku pomija mapy źródłowe i pliki JSON, aby uniknąć fałszywych alarmów.

module.exports = {
  // ... other webpack configuration ...

  performance: {
    hints: 'error',
// Emit an error whenever a bundle exceeds the size limits
    maxAssetSize: 250 * 1024,
// Maximum size in bytes for any generated asset (e.g., 250 KiB)
    maxEntrypointSize: 500 * 1024,
// Maximum size in bytes for an entrypoint (e.g., 500 KiB)
    assetFilter: function(assetFilename) {

// Optionally exclude certain assets (e.g., source maps) from performance hints
      return !/\.(map|json)$/.test(assetFilename);
    }
  }
};

Należy pamiętać, że webpack porównuje te limity z rozmiarami plików po ich wygenerowaniu, które zazwyczaj nie odpowiadają rozmiarom skompresowanym, które pobierają użytkownicy. Jeśli Twój budżet jest określony w kompresowanych kilobajtach, przelicz go na rozmiary surowe do tego sprawdzenia lub połącz to z oddzielnym krokiem mierzącym rozmiar pliku po kompresji gzip lub Brotli.

Strategie utrzymywania rozmiaru plików w ramach budżetu

Budżet oraz automatyczne sprawdzenie informują o przekroczeniu limitu; utrzymanie się poniżej tego limitu wymaga starannego projektowania. Najskuteczniejsze techniki są dobrze znane i najlepiej działają w połączeniu.

Dzielenie kodu według tras i funkcjonalności

Rozbieranie kodu polega na podziale jednego monolitycznego pliku na mniejsze fragmenty, które ładują się na żądanie. Częstym przykładem jest ciężki panel administracyjny w aplikacji korporacyjnej: dzięki ładowaniu go za pomocą dynamicznych instrukcji import() kod ten jest pobierany tylko wtedy, gdy administrator faktycznie przejdzie do tej ścieżki. Zwykli użytkownicy nigdy go nie pobierają, co może pozwolić usunąć setki kilobajtów z początkowego pliku do wysłania.

Niech mechanizm tree shaking usuwa niepotrzebny kod

Mechanizm tree shaking eliminuje elementy eksportowane, które nigdy nie są używane, dzięki czemu nieużywane części własnych modułów oraz bibliotek nie trafiają do pliku wyjściowego. Najlepiej działa z składnią modułów ES oraz zależnościami opublikowanymi w formie nadającej się do przetworzenia przez tree shaking, dlatego warto sprawdzić, w jaki sposób zbudowany jest dany pakiet, zanim się na niego polegnie.

Uwaga: Wykorzystywanie dużej biblioteki tylko dla jednej funkcji pomocniczej to jeden z najczęstszych powodów nadmiernego rozmiaru aplikacji. Poszukaj lżejszej alternatywy lub importuj tylko konkretną funkcję, której potrzebujesz.

Odkładaj to, czego nie wymaga pierwszy ekran

Opóźnione ładowanie komponentów, które nie są kluczowe dla początkowego wyglądu strony – takich jak okna dialogowe, elementy znajdujące się poniżej linii widoku lub rzadko używane panele – odkłada koszty ich analizy i wykonywania na moment, gdy są potrzebne. W połączeniu z dzieleniem kodu i usuwaniem niepotrzebnego kodu zapewnia to wolny wątek główny w pierwszych sekundach, gdy użytkownicy najczęściej wchodzą w interakcję.

  • Droga część JavaScript na urządzeniach mobilnych to jego analiza, kompilacja i wykonywanie na wątku głównym, co bezpośrednio wpływa na INP oraz inne wskaźniki Core Web Vitals.
  • Budżet wydajności to umowa zespołu wyrażona liczbą, najlepiej dla każdej trasy i mierzona w sposób spójny.
  • Wdrażanie budżetu w CI, na przykład za pomocą performance.hints: 'error' w webpack, pozwala wykryć regresje w prośbach pull request, które je wprowadzają.
  • Dzielenie kodu, usuwanie niepotrzebnego kodu oraz ładowanie opóźnione to sposoby na utrzymanie się w ramach budżetu po jego ustaleniu; testuj na rzeczywistych urządzeniach, aby upewnić się, że te wartości przekładają się na szybszą obsługę.