Dlaczego mikrooptymalizacje nie rozwiązują problemów z rzeczywistą wydajnością JavaScripta
Wyjaśnia, dlaczego ściganie mierzalnych wskaźników takich jak rozmiar paczki i ponowne renderowanie często pomija prawdziwe przyczyny powolnej wydajności aplikacji JavaScript.
Na papierze prośba o integrację wyglądała solidnie.
Rozmiar pliku spadł o 18 procent. Usunięto kilka nieużywanych zależności. Kilka komponentów zostało objętych mechanizmem memoizacji. Kilka operacji na tablicach zastąpiono rzekomo lżejszymi pętlami. Wynik testu Lighthouse również się poprawił. Wszystkie wskaźniki w opisie prośby o integrację wskazywały we właściwym kierunku.
Zespół zatwierdził ją bez wahania.
Dwa tygodnie później użytkownicy nadal uważali, że aplikacja działa powoli.
Nie tak, jak w przypadku „kilku dodatkowych milisekund pokazanych w testach”.
To nie był problem wynikający z liczby na panelu kontrolnym.
To była powolność, która faktycznie wpływała na użytkowników.
Naciskali przycisk i nie byli pewni, czy został zarejestrowany.
Otwierali ekran i czekali, aż pojawi się cokolwiek przydatnego.
Zmieniali filtr i obserwowali, jak interfejs na chwilę zastyga, zanim wracał do normalnej pracy.
Zespół włożył naprawdę wiele wysiłku w poprawę wydajności.
Tylko że skupili się na niewłaściwych celach.
Taki wzorzec występuje nieustannie we współczesnych projektach JavaScript.
Mimo lepszych narzędzi do analizy wydajności, szybszych czasów działania, inteligentniejszych narzędzi do pakowania kodu, bardziej zaawansowanych frameworków oraz coraz potężniejszych przeglądarek, programiści wciąż popełniają ten sam błąd:
Optymalizują to, co najłatwiej zmierzyć, zamiast to, co faktycznie odczuwają użytkownicy.
JavaScript oferuje bowiem niekończącą się listę elementów, które można optymalizować.
Komponent jest renderowany cztery tysiące razy.
Zatem to naprawiasz.
Pakiet waży 300 KB.
Zatem go zmniejszasz.
Funkcja wykonuje się w czasie 12 milisekund.
Zatem ją przepisujesz.
Zależność zajmuje 40 KB.
Zatem ją usuwasz.
Mikropomiary pokazują, że podejście A jest o 7 procent lepsze od podejścia B.
Zatem wybierasz A.
Każda z tych opcji może stanowić prawidłową praktykę.
Jednak żadna z nich automatycznie nie zapewnia lepszej obsługi dla osoby korzystającej z twojej aplikacji.
Czasami najszybszy kod, jaki napiszesz, rozwiązuje problem, który w ogóle nikogo nie nurtował.
Tymczasem powolne zapytanie do bazy danych, zbędny wywołanie sieciowe, nieudana sekwencja ładowania, przeciążony pakiet API lub słabo zaprojektowana interakcja w cichu kosztują użytkowników rzeczywistego czasu.
To niezgodność jest prawdziwym problemem, który warto zbadać.
Wydajność to nie to samo co szybkość
Jedną z najprzydatniejszych lekcji płynących z pracy nad systemami produkcyjnymi jest to, że wydajność nie da się sprowadzić do jednego wskaźnika.
Aplikacja może być wysoce efektywna pod względem obliczeniowym nawet na poziomie mikrosekundy, a mimo to sprawiać kłopoty przy użyciu.
Wyobraź sobie stronę, która przechodzi przez następującą sekwencję:
- Ładuj szkielet aplikacji.
- Pobierz kilka fragmentów JavaScriptu.
- Rozpocznij działanie frameworka.
- Pobierz dane konfiguracyjne.
- Pobierz informacje o aktualnym użytkowniku.
- Pobierz dane dotyczące uprawnień.
- Pobierz treść panelu sterowania.
- Wyświetl panel sterowania.
- Pobierz powiadomienia.
- Wyświetl powiadomienia.
Każdy z tych kroków sam w sobie może być doskonale szybki.
Prawdziwym problemem jest cała ta sekwencja działań.
Użytkownikom nie zależy na tym, czy każdy element był oddzielnie dobrze optymalizowany.
Zależy im na tym, że przez chwilę patrzyli na pusty ekran, zanim cokolwiek się pojawiło.
Dlatego każde prawdziwe badanie wydajności powinno zacząć się od jednego pytania:
Czego właściwie czeka osoba po drugiej stronie?
A nie:
Jak mogę skrócić czas działania tej funkcji?
Są to zasadniczo różne kierunki badań.
Rozwijający może spędzić trzy godziny na optymalizacji funkcji, by skrócić jej czas wykonywania z 8 ms do 3 ms.
Tymczasem strona czeka bezczynnie przez 700 ms na żądanie, które w ogóle nie było konieczne.
To nie jest prawdziwa optymalizacja.
To jak sprzątanie mebli, podczas gdy w ścianie za nimi pęka rura.
Obsesja na punkcie 5 milisekund
Rozwijający w JavaScript mają słabość do mikrooptymalizacji.
Część tej ciekawości jest rzeczywiście wartościowa.
Ludzie kłócą się o użycie pętli for w porównaniu z metodą map().
Dyskutują o strategiach alokacji pamięci.
Badają ukryte klasy, zachowanie zbierania śmieci, zamykania funkcji, koszty dekonstrukcji, nakładki na wywoływanie funkcji oraz optymalizacje JIT.
Za tym wszystkim kryje się prawdziwa, cenna wiedza.
Problem nie polega na nierozumieniu tych mechanizmów.
Problemem jest sięganie po nie bez żadnych dowodów na to, że są tu istotne.
Załóżmy, że funkcja jest wykonywana 100 razy podczas jednej interakcji użytkownika.
Obecnie każde wywołanie trwa 2 ms.
Potrzebujesz pół dnia, aby skrócić to do 1 ms.
Łączna oszczędność: 100 milisekund.
To może mieć jakąś wartość.
Ale załóżmy, że ta sama interakcja wywołuje również niepotrzebne wezwanie API, które trwa 600 ms do zakończenia.
Usunięcie tego wezwania zajęłoby pięć minut i przyniosłoby sześć razy większą oszczędność czasu niż całe twoje popołudnie poświęcone optymalizacji.
Mówiąc prościej, wydaje się to oczywiste.
A jednak rozmowy podczas przeglądania kodu skupiają się na pierwszym rodzaju problemów, ponieważ są one wyraźnie widoczne w różnicach.
Niepotrzebne wezwanie sieciowe natomiast może być ukryte trzy warstwy abstrakcji dalej.
To powoduje przewidywalną i niebezpieczną stronniczość:
Zespoły ostatecznie optymalizują kod, który jest im widoczny, a nie system, którego doświadczają faktycznie ich użytkownicy.
Przeglądarka to nie twoja funkcja
Kolejnym częstym błędem jest założenie, że czas wykonywania JavaScriptu opisuje całą historię wydajności.
To dalekie od prawdy.
Aplikacja oparta na przeglądarce to cały system, a nie pojedyncze wezwanie funkcji.
System ten obejmuje:
- Rozwiązywanie DNS
- Ustanawianie połączenia
- Wymianę danych TLS
- Przetwarzanie po stronie serwera
- Zapytania do bazy danych
- Serializację odpowiedzi API
- Czas transferu w sieci
- Analizę HTML
- Analizę JavaScriptu
- Wykonywanie JavaScriptu
- Rysowanie
- Obliczanie układu
- Rysowanie elementów na ekranie
- Kompozycja obrazu
Każdy z tych etapów może wpłynąć na to, jak szybko coś wydaje się działać.
Załóżmy, że udało ci się skrócić czas renderowania komponentu React z 15 ms do 8 ms.
To naprawdę dobra praca.
Jednak jeśli serwer potrzebuje 900 ms, aby przygotować dane potrzebne temu komponentowi, twoja poprawa prawie nie jest zauważalna.
Albo może serwer odpowiada szybko, ale wysyła 2 MB danych w formacie JSON, podczas gdy potrzeba tylko 20 KB.
Wtedy marnujesz zasoby procesora i przepustowość łącza, przenosząc dane, które w ogóle nie powinny istnieć w takiej formie.
A może strona pobiera ogromną bibliotekę po stronie klienta, zanim zdąży wyświetlić cokolwiek istotnego.
Żadne z tych problemów nie dotyczy samego komponentu.
Są to problemy architektoniczne.
Dlatego poważna praca nad wydajnością często przypomina raczej dochodzenie niż „usprawnienie JavaScriptu”.
Śledzisz przyczynę opóźnienia aż do jego źródła, bez względu na to, dokąd to prowadzi.
Najdroższa operacja to często ta, której powinieneś uniknąć
Przy planowaniu prac związanych z wydajnością istnieje przydatny porządek priorytetów.
Zwiększenie szybkości wykonania operacji to dobry rezultat.
Jednak całkowite pominięcie tej operacji jest zazwyczaj lepszym rozwiązaniem.
Zobacz ten fragment kodu:
const results = expensiveTransform(items);
Profilowanie pokazuje, że ta transformacja pochłania 40 ms.
Dlaczego w ogóle ma miejsce ta transformacja?
Być może uruchamiasz to ponownie przy każdym renderowaniu, bez względu na wszystko.
Być może serwer mogłby przekazać ci już przetworzoną wersję.
Być może interfejs rzeczywiście nie wymaga wszystkich 10 000 wierszy.
Być może pobierasz dane, których użytkownik nigdy tak naprawdę nie otworzy.
Prawdziwe rozwiązanie może w ogóle nie znajdować się wewnątrz expensiveTransform().
Może wystarczyć po prostu usunąć tę funkcję.
Takie podejście dobrze sprawdza się poza tym konkretnym przykładem.
Pomijaj żądania, których nie musisz wysyłać.
Pomijaj renderowanie interfejsu, który pozostaje ukryty.
Pomijaj obliczanie wartości, których nikt nie będzie używał.
Pomijaj ścieżki kodu, których użytkownicy nigdy nie uruchomią.
Pomijaj przetwarzanie danych, które można było filtrować wcześniej.
Pomijaj powtarzanie pracy, której wynik tak naprawdę się nie zmienił.
Naj szybszą możliwą operacją pozostaje ta, która w ogóle nie jest wykonywana.
Rozmiar pliku nie jest całym obrazem
Rozmiar pliku zasługuje na uwagę. To prawda.
Jest to jednak tak powszechnie monitorowana metryka, że zespoły czasami zaczynają traktować ją jako zamiennik samej wydajności.
Zespół skraca swój plik JavaScript o 50 KB i uważa to za sukces.
Tymczasem aplikacja nadal wysyła sześć żądań po kolei, zanim użytkownik może cokolwiek z nią zrobić.
Oczywiście plik się zmniejszył.
To nie oznacza automatycznie, że jakość użytkowania się poprawiła.
Nic z tego nie sugeruje, że rozmiar pliku jest bez znaczenia.
Oznacza to, że musisz ustalić kiedy dany plik JavaScript faktycznie zostanie użyty.
Skrypt o rozmiarze 100 KB, który blokuje początkową interaktywność, może być znacznie ważniejszy niż skrypt o rozmiarze 300 KB załadowany później, dla funkcji, której użytkownik może korzystać raz w miesiącu.
Czas ma znaczenie.
Moment wykonywania kodu też ma znaczenie.
Ma znaczenie urządzenie, na którym działa.
Ma znaczenie sieć, przez którą przesyła dane.
Ma znaczenie to, czy dane są zapisywane w pamięci tymczasowej.
I, co równie ważne, ma znaczenie to, co użytkownik faktycznie próbuje zrobić.
Jeśli jakaś funkcja jest rzadko używana, wcześniejsze załadowanie wszystkiego, czego potrzebuje podczas uruchamiania, może być niewłaściwym rozwiązaniem.
Rozdzielanie kodu pomaga w tym przypadku.
Pomaga też ładowanie opóźnione.
Jednak oba te podejścia mogą stać się pustym rytuałem, jeśli zostaną zastosowane bez rzeczywistego zrozumienia tego, jak strona jest ładowana w praktyce.
Pytanie, które warto zadać, to nie:
Jak możemy jeszcze bardziej zmniejszyć rozmiar tego pliku?
A raczej:
Czego w tym konkretnym momencie potrzebuje ten użytkownik i jak szybko możemy sprawić, by ta konkretna funkcja była dostępna do użycia?
Takie podejście doprowadzi was o wiele dalej.
Komponent, na który patrzysz, może nie być winny
Każdy, kto pracował z Reactem, rozpoznaje ten cykl.
Komponent renderuje się częściej, niż powinien.
Ktoś ucieka się do useMemo.
Jeden dodatkowy render znika.
Następnie identyczne traktowanie otrzymuje kolejny komponent.
Niedługo później kod jest przepełniony wywołaniami memoizacji:
const filtered = useMemo(
() => expensiveFilter(items, query),
[items, query]
);
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
const value = useMemo(
() => ({ user, permissions }),
[user, permissions]
);
Czasami to rzeczywiście właściwa decyzja.
Innym razem jedynie utrudnia zrozumienie kodu, nie naprawiając w rzeczywistości niczego.
Optymalizacja nie jest bezkosztowa.
Memoizacja w szczególności wiąże się z rzeczywistymi kosztami.
Powoduje dodatkowe obciążenie pamięci, konieczność utrzymywania tablic zależności, większe obciążenie poznawcze oraz nowe miejsca, gdzie mogą się ukrywać subtelne błędy.
Zmierz efekty przed użyciem tej metody.
Jeśli jakiś obliczenie trwa 0,2 ms i jest wykonywane rzadko, jego optymalizacja nic nie da.
Jeśli zajmuje 50 ms i jest uruchamiane przy każdym naciśnięciu klawisza, to zupełnie inna sytuacja.
Celem nie jest wykrywanie każdego ponownego renderowania.
Celem jest sprawienie, by interakcje, które są ważne, wydawały się wystarczająco szybkie.
Te dwa cele nie są identyczne.
Koncentruj się na interakcjach, a nie na poszczególnych komponentach
Tutaj wiele zespołów mogłoby skorzystać ze zmiany podejścia.
Użytkownicy nie postrzegają komponentów jako oddzielnych jednostek.
Doświadczają tego, co robią.
Piszą zapytanie wyszukiwawcze.
Wypełniają pola.
Przełączają się między stronami.
Zasyłają formularz.
Otwierają spadającą listę.
Przełączają się między kartami.
Wgrzewają plik.
Przewijają feed.
Siedzą i czekają, aż coś się pojawi.
Zamiast pytać:
Czy ten komponent jest dobrze zoptymalizowany?
Spróbuj zapytać:
Czy wpisywanie tekstu w to pole wyszukiwania odbywa się natychmiastowo?
Zamiast:
Czy ta lista jest renderowana efektywnie?
Zadaj pytanie:
Czy ktoś może płynnie przewijać tę listę, bez opóźnień w interfejsie?
Zamiast:
Czy zmniejszyliśmy liczbę renderowań React?
Zadaj pytanie:
Czy naciśnięcie tego przycisku daje użytkownikowi natychmiastową, istotną informację zwrotną?
To drugie zestawienie pytań znacznie lepiej odzwierciedla to, co naprawdę ma znaczenie dla produktu.
Mądra optymalizacja, która sprawia, że ważna interakcja pozostaje tak sama powolna jak wcześniej, niekoniecznie stanowi wartościową inżynierię, bez względu na to, jak elegancko wygląda w porównaniu.
Karta sieci zazwyczaj przewyższa ponownie pisany pętla
Zanim dotkniesz choćby jednej pętli, otwórz panel sieci swojego przeglądarki.
To nie jest przypadkowa sugestia.
Często odkryjesz tam więcej możliwości ulepszeń niż w setkach linii ręcznie dostosowanego JavaScriptu.
Zwracaj uwagę na takie rzeczy jak:
- tę samą prośbę wysyłaną więcej niż raz
- prośby wykonywane jedna po drugiej, mimo że mogłyby być realizowane równolegle
- prośby wysyłane przed tym, jak faktycznie potrzebne są dane
- odpowiedzi znacznie większe, niż to co jest wyświetlane
- caching, który powinien istnieć, ale go nie ma
- polling odbywający się częściej, niż to konieczne
Jedno niepotrzebne żądanie może przeważyć nad skutkami dziesiątek małych modyfikacji w JavaScript.
Weźmy jako przykład funkcję wyszukiwania.
Oto prosty podejście:
User types "j"
→ request
User types "ja"
→ requestUser types "jav"
→ requestUser types "java"
→ request
Zespół może wtedy poświęcić realny czas na przyspieszenie wyświetlania wyników.
Jednak prawdziwym wąskim gardłem może być to, że aplikacja wysyła cztery oddzielne żądania, podczas gdy wystarczyłoby jedno.
Dodanie mechanizmów opóźniania żądań, ich anulowania, cache’owania oraz lepiej zaprojektowanych zapytań zazwyczaj przynosi znacznie większe korzyści.
To właśnie tego typu ulepszenia zachodzą na poziomie systemu, a nie wewnątrz pojedynczej funkcji.
Nie optymalizuj niewłaściwego urządzenia
Prowadzenie testów wydajności na własnym komputerze do rozwoju niewiele mówi o tym, jak aplikacja faktycznie działa na starym telefonie z ograniczoną wydajnością procesora i niestabilnym połączeniem.
To ma ogromne znaczenie dla JavaScriptu.
Nowoczesny sprzęt potrafi obsłużyć zaskakująco dużą ilość kodu, bez że programista zauważy spowolnienia.
Mocny laptop może maskować problemy.
Telefon flagowy może maskować problemy.
Szybka sieć biurowa może maskować problemy.
Praca lokalnie może maskować problemy.
Następnie rzeczywała osoba otwiera aplikację na tanim urządzeniu przy niestabilnym połączeniu.
Nagle ta starannie dostrojona aplikacja wydaje się ciężka i nieresponsywna.
Dlatego właśnie tak ważne jest testowanie w rzeczywistych warunkach.
Nie musisz tego robić ciągle.
Jednak musisz to robić wystarczająco często, aby zespół miał rzeczywiste pojęcie o tym, jak aplikacja zachowuje się poza bezpiecznym środowiskiem deweloperskim.
Prawidłowe pytanie to nie:
Czy na moim sprzęcie działanie jest szybkie?
A raczej:
Czy jest wystarczająco szybkie dla osób, które nią faktycznie korzystają?
Architektura zwykle przewyższa mikrooptymalizację
To chyba najważniejsza lekcja ze wszystkich.
To architektura określa górny limit wydajności.
Jeśli twoja aplikacja musi wysłać pięć zapytań API kolejno, zanim będzie mogła pokazać główny ekran, żadne sprytne manewry z tablicami nie poprawią tej sytuacji.
Jeśli każda trasa ładuje cały plik aplikacji, usunięcie kilku funkcji pomocniczych nie rozwiąże prawdziwego problemu.
Jeśli klient pobiera ogromny zbiór danych, a następnie filtruje go w przeglądarce, udoskonalenie logiki tego filtrowania prawdopodobnie jest mniej ważne niż naprawa samego kontraktu API.
Jeśli jakakolwiek akcja użytkownika usuwa dużą część stanu z pamięci cache, otaczanie komponentów mechanizmem memoization nie naprawi uszkodzonej strategii unieważniania danych.
Jeśli renderujesz tysiące węzłów DOM jednocześnie, zmiana sposobu używania funkcji map() nie pomoże ci zaoszczędzić zasobów.
Ulepszenia, które rzeczywiście przynoszą efekt, zazwyczaj polegają na zmianie gdzie wykonywane są zadania, kiedy je wykonuje się lub czy w ogóle muszą one mieć miejsce.
Są to decyzje architektoniczne, a nie drobne modyfikacje na poziomie kodu.
To, na co faktycznie zwracam uwagę podczas badania wydajności
Gdy coś wydaje się wolne, należy oprzeć się instynktowi natychmiastowego zagłębiania się w kod.
Najpierw spróbuj odtworzyć problem.
Następnie zapytaj, na co dokładnie użytkownik tam siedzi i czeka.
Od tego momentu dochodzenie zazwyczaj przebiega według określonego porządku.
1. Ładowanie
Cóż musi się wydarzyć, zanim użytkownik będzie mógł faktycznie zobaczyć i używać ważnych elementów strony?
Zlokalizuj kluczową ścieżkę.
Które zasoby są rzeczywiście potrzebne?
Które żądania blokują postępy?
Cóż jest ładowane, czego w rzeczywistości nie trzeba?
2. Sieć
Otwórz panel Sieć.
Rozglądaj się za wzorcami typu „waterfall”.
Łańcuchy sekwencyjnych żądań wymagają uważnej analizy.
Tak samo jak duplikowane wywołania oraz dane przesyłane w większych rozmiarach, niż powinny być.
3. Renderowanie
Następnie przyjrzyj się temu, co robi sam przeglądarka.
Czy strona wysyła ogromną ilość danych DOM?
Czy kroki związane z układem i rysowaniem są kosztowne?
Czy wykonywane są kosztowne operacje w odpowiedzi na interakcję użytkownika?
4. JavaScript
Dopiero na tym etapie mają znaczenie szczegóły na poziomie funkcji.
Które operacje faktycznie zużywają czas procesora?
Które z nich są wykonywane wielokrotnie?
Które są powiązane z czymś, co użytkownik właśnie zrobił?
5. Pamięć
Jeśli aplikacja działa coraz gorzej w miarę dłuższego pozostawania otwartej, warto sprawdzić poziom pamięci.
Ucieczki pamięci i niekontrolowany wzrost mogą przypominać ogólne spowolnienie.
6. Rzeczywisty wpływ na użytkownika
Na koniec powiąż to, co odkryłeś, z rzeczywistym doświadczeniem użytkownika.
Czy uruchomienie aplikacji stało się szybsze?
Czy wyszukiwanie wydaje się bardziej szybkie?
Czy nawigacja uległa poprawie?
Czy jakiś proces pracy stał się mniej uciążliwy?
Jeśli nic z tego się nie zmieniło, warto zakwestionować, czy problem został w ogóle zidentyfikowany.
Budżety wydajności są przydatne — jeśli są powiązane z rzeczywistością
Często zdarza się, że zespoły ustalają takie reguły jak:
Bund JavaScripta powinien mieścić się poniżej 300 KB.
To rozsądny punkt wyjścia, lepszy niż brak jakichkolwiek ograniczeń.
Jednak budżet oparty na rzeczywistym doświadczeniu jest zazwyczaj bardziej przydatny:
- Główna treść powinna pojawiać się szybko
- Szukanie nie powinno sprawiać wrażenia powolnego
- Nawigacja powinna dawać natychmiastową informację zwrotną
- Kluczowe strony nie powinny polegać na serii kolejnych żądań
- JavaScript potrzebny do pierwszej interakcji powinien być zminimalizowany
- Wielkie zestawy danych nie powinny być wyświetlane na ekranie jednocześnie
Tych elementów trudniej sprowadzić do jednej, precyzyjnej liczby.
Jednak one znacznie lepiej odzwierciedlają to, na co faktycznie zwracają uwagę i co jest ważne dla użytkowników.
Metryki są przydatne, gdy pomagają nam zrozumieć, co naprawdę się dzieje.
Stają się niebezpieczne w momencie, gdy osiągnięcie określonej liczby staje się głównym celem.
Pułapka optymalizacji
Istnieje subtelny czynnik psychologiczny, który skłania programistów na tę drogę.
Optymalizacja czegoś wydaje się postępem.
Można wtedy wskazać na konkretną zmianę i powiedzieć:
Wielkość pliku skrócono o 14%.
Lub wskazać na wyniki testów i powiedzieć:
Ta funkcja teraz działa o 32% szybciej.
Lub pokazać komuś zrzut ekranu z narzędzia do analizy jako dowód.
Takie sukcesy sprawiają przyjemność.
Jednak niektóre z najskuteczniejszych poprawek wydajności są, szczerze mówiąc, nudne.
Usunięcie niepotrzebnego wywołania API nie stanowi ekscytującej demonstracji.
Zmiana struktury odpowiedzi serwera również nie jest imponująca.
To nie jest też przycinanie łańcucha zależności.
To nie jest też poprawianie zapytania, które pobiera 5 000 wierszy, podczas gdy wystarczyłoby 50.
To nie jest też dodawanie pamięci cache.
Zadania, których unikasz, z natury są niewidoczne.
Dlatego właśnie tak łatwo o nich zapomnieć.
Najlepsze ulepszenia wydajności mogą skutkować mniej kodu, mniej żądań, mniej obliczeń oraz mniej procesów działających jednocześnie.
Może nie być niczego, co warto zrobić skrиншот.
Aplikacja po prostu jest przyjemniejsza w użyciu.
Optymalizacja powinna zaczynać się od dowodów
Oto zasada, którą warto stosować we wszystkich przypadkach:
Nie optymalizuj kodu. Optymalizuj potwierdzone problemy.
To nie wymaga tworzenia skomplikowanego procesu inżynierii wydajności dla każdej funkcji, którą wdrażasz.
Oznacza to po prostu zebranie wystarczająco dużo dowodów, aby wiedzieć, gdzie faktycznie traci się czas.
Korzystaj z narzędzi do profilowania.
Używaj wbudowanych paneli wydajności przeglądarki.
Zapisuj ślady sieciowe.
Przyjmij dane telemetrii z środowiska produkcyjnego.
Ustaw monitorowanie rzeczywistych użytkowników tam, gdzie to ma sens.
Testuj na urządzeniach odpowiadających twojej rzeczywistej publiczności.
A przede wszystkim odtwórz problem tak, jak został zgłoszony.
Jeśli interesariusz mówi, że „panel sterowania działa wolno”, opieraj się chęci natychmiastowego zagłębienia się w kod komponentu i zaczynania usuwania operacji renderowania.
Najpierw dowiedz się, co dokładnie oznacza „wolne działanie”.
Czy problemem jest powolna odpowiedź serwera?
Czy wąskim gardłem jest konkretny wywołanie API?
Czy plik z JavaScriptem jest zbyt duży?
Czy analiza danych trwa zbyt długo?
Czy renderowanie jest najdroższą częścią procesu?
Czy jakaś długa operacja blokuje główny wątek?
Czy zapytanie do bazy danych nie działa wystarczająco szybko?
Czy istnieją opóźnienia spowodowane kolejnymi żądaniami?
Czy przeglądarka stoi w bezczynności, czekając na coś, co mogło zostać rozpoczęte wcześniej?
Czy też aplikacja jest technicznie responsywna, ale nie dostarcza użytkownikowi żadnej wizualnej informacji zwrotnej?
Rozwiązywanie problemów z wydajnością to praca detektywa.
To nie jest wyścig, by zobaczyć, kto może usunąć najwięcej kodu JavaScript.
Najlepsza optymalizacja JavaScript może polegać na mniejszej ilości kodu JavaScript
To może brzmieć dziwnie z ust programisty JavaScript.
Jednak im bardziej rozwijają się aplikacje, tym jest to ważniejsze.
Każdy fragment kodu uruchamianego na stronie klienta wiąże się z kosztem.
Należy go pobrać.
Może być konieczne jego analizowanie.
Może być potrzebna jego kompilacja.
Musi zostać wykonywany.
Używa pamięci.
Konkujuje z przeglądarką o czas renderowania.
Może powodować trudności podczas interakcji użytkownika.
To wszystko nie oznacza, że JavaScript jest z natury zły.
Oznacza to, że każde obliczenie wykonywane po stronie klienta musi uzasadniać swoje istnienie.
Czasami lepszym rozwiązaniem jest renderowanie na serwerze.
Czasami chodzi o transmisję treści strumieniowo zamiast jej blokowania.
Czasami polega to na przeniesieniu logiki całkowicie na stronę serwera.
Czasami chodzi o wprowadzenie pamięci cache.
Czasami polega to na ograniczeniu tego, co zwraca API.
Czasami chodzi o ładowanie elementów stopniowo, a nie wszystkich naraz.
Czasami wystarczy po prostu usunąć funkcję, której nikt tak naprawdę nie używa.
A czasami JavaScript, który już istnieje, jest w pełni wystarczający bez żadnych zmian.
Główny wniosek to konieczność porzucenia założenia, że optymalizacja musi odbywać się wewnątrz samego JavaScriptu.
Czego nauczą się w końcu doświadczeni programiści
Na początku kariery programisty optymalizacja zazwyczaj oznacza sprawienie, by istniejący kod działał szybciej.
Gdy przybywa doświadczenia, nacisk przesuwa się w kierunku eliminacji działań, które w ogóle nie były konieczne.
Ostatecznie pytania stają się głębsze, dotycząc tego, dlaczego te działania w ogóle istnieją.
To znacząca ewolucja w sposobie myślenia.
Zamiast pytać:
Czy tę pętlę można sprawić, by działała szybciej?
Pytanie staje się:
Dlaczego w ogóle w przeglądarce przechodzimy przez 20 000 rekordów w ramach pętli?
Zamiast pytać:
Jak można powstrzymać ponowną renderizację tego komponentu?
Pytanie staje się:
Dlaczego ta jedna interakcja powoduje zmiany we wszystkim stanie strony?
Zamiast pytać:
Jak można zmniejszyć rozmiar tego pliku?
Pytanie brzmi:
Dlaczego użytkownik musi pobrać ten kod, zanim będzie mógł coś znaczącego zrobić?
Zamiast pytać:
Jak można uczynić tę prośbę bardziej efektywną?
Pytanie brzmi:
Czy ta prośba w ogóle jest konieczna?
Zadawanie tak głębszych pytań prowadzi do naprawdę lepszej architektury.
Celem nie jest szybki kod
To jest lekcja, której zrozumienie zajmuje najdłużej.
Praca nad wydajnością nie polega na tworzeniu najszybszego możliwego JavaScriptu.
Chodzi o stworzenie produktu, który wydaje się wystarczająco szybki dla osób, które go faktycznie używają.
Te dwa cele to nie to samo.
Niesamowicie optymalizowany algorytm jest bezwartościowy, jeśli użytkownik musi czekać dwa sekundy na żądanie, które go uruchamia.
Doskonale zmemoryzowany komponent nie pomaga, jeśli strona renderuje 40 komponentów, których użytkownik nigdy nawet nie zobaczy.
Mniejszy plik nie oznacza automatycznie niczego pozytywnego, jeśli aplikacja nadal blokuje główną interakcję przez bezsensowne operacje.
A lepsza wartość w testach nie ma znaczenia, jeśli żaden rzeczywisty użytkownik nie odczuje różnicy.
Rozwijający aplikacje w JavaScript mają dziś dostęp do większej liczby technik optymalizacji niż kiedykolwiek wcześniej.
Dlatego tym ważniejsze jest wiedzenie, co nie warto optymalizować.
Zacznij od skupienia się na użytkowniku.
Zlokalizuj rzeczywisty opóźnienie.
Pomierz je prawidłowo.
Prześledź je w całym systemie, od początku do końca.
Usuń wszystko, co nie jest konieczne.
Dopiero wtedy należy pracować nad przyspieszeniem tego, co pozostało.
Ta sekwencja ma znaczenie.
Ponieważ najcenniejsza optymalizacja to niekoniecznie ta, która wygląda najbardziej imponująco pod względem technicznym.
To ta, która sprawia, że użytkownik przestaje zauważać, iż aplikacja była od początku wolna.
Literatura pokrewna
- Native Browser APIs Replacing Popular npm Packages in 2026 — Wyjaśnia, w jaki sposób wbudowane funkcje JavaScript i CSS, takie jak Signals, operator pipeline, Temporal oraz Anchor Positioning, zastępują popularne pakiety npm.