Jak przyspieszyć aplikację w React: memoizacja, lazy loading i analiza bundle

0
27
3/5 - (1 vote)

Nawigacja:

Po czym poznać, że aplikacja React jest „za wolna”

Objawy po stronie użytkownika

Użytkownik nie patrzy na wykresy w DevTools, tylko na to, czy strona reaguje na jego działania. „Wolna aplikacja React” objawia się zwykle na kilka powtarzających się sposobów: biała strona na starcie, opóźnione reakcje na kliknięcia i irytujące przycinki przy przewijaniu.

Warto odróżnić subiektywne wrażenie od realnych problemów mierzonych liczbami. Najczęściej używane metryki to:

  • TTFB (Time To First Byte) – czas od żądania do pierwszego bajtu odpowiedzi z serwera. Jeśli TTFB jest wysoki, przyczyna zwykle leży poza Reactem (backend, sieć).
  • TTI (Time To Interactive) – moment, w którym aplikacja jest w pełni interaktywna, a główny wątek JS nie jest już zablokowany długimi zadaniami.
  • LCP (Largest Contentful Paint) – kiedy użytkownik widzi główną treść (np. hero, duże zdjęcie, listę głównych elementów).
  • Opóźnienia w interakcji – od kliknięcia przycisku do widocznej reakcji: zmiany UI, przejścia routingu, otwarcia modala.

Przy Reactowych SPA częstym problemem jest długi czas TTI. Strona „wygląda” na załadowaną (HTML i CSS są już widoczne), ale paczka JavaScript jest jeszcze parsowana i wykonywana. Użytkownik klika, a nic się nie dzieje lub pojawia się znany „przycinek”, bo główny wątek jest właśnie zajęty dekodowaniem wielkiego bundle. To typowy sygnał, że rozmiar i struktura bundla oraz nadmierne renderowanie zaczynają ciążyć.

Inny klasyczny objaw to lag przy wpisywaniu w pola formularza – użytkownik pisze w input, a tekst pojawia się z wyraźnym opóźnieniem. To wprost wskazuje na zbyt ciężką logikę wywoływaną przy każdym re-renderze komponentu formularza (walidacje, filtrowanie, sortowanie list) albo na zbyt szeroki state/context powodujący kaskadowe re-rendery.

Objawy po stronie programisty

Deweloper widzi inne symptomy, zanim nawet ktoś zgłosi narzekania. Przy budowie dużych ekranów pojawia się zauważalne „mielenie” przeglądarki, DevTools pokazuje setki renderów jednego komponentu przy jednej akcji, a w React Profilerze widać całe poddrzewa odmalowywane bez wizualnej potrzeby.

Typowe sygnały ostrzegawcze:

  • Minimalna zmiana stanu globalnego (np. w Reduxie lub Context API) powoduje renderowanie całej strony, nawet statycznych sekcji.
  • Komponenty „kontenerowe” trzymają dużo stanu i rozbudowane obiekty w propsach – zmiana jednego pola aktualizuje wszystkie dzieci.
  • Wersja produkcyjna działa wyraźnie wolniej na słabszych urządzeniach mobilnych, mimo że lokalnie w dev-mode wszystko wydawało się akceptowalne.

W praktyce, gdy zaczynasz debugować wydajność, pojawia się podejrzenie: czy to na pewno React? Może backend jest wolny, może API zwraca zbyt dużo danych, może grafika jest ogromna. Dlatego tak ważny jest krótki, powtarzalny proces diagnozy przed wejściem w refaktoryzacje.

Jak odróżnić winę backendu, sieci i frontendu

Najprostszym testem jest odcięcie frontendu od skomplikowanego UI. Warto:

  • Wywołać to samo API z prostego HTML + fetch lub narzędzia REST (Postman, curl) i zmierzyć czas odpowiedzi.
  • Wyłączyć na chwilę ciężkie operacje po stronie Reacta (np. filtrowanie, sortowanie, liczenie agregatów) i zobaczyć, jak zmieni się czas reakcji.
  • Sprawdzić w zakładce Network w DevTools rozmiar przesyłanych danych: duże JSON-y, pliki, grafiki.

Jeśli API odpowiada szybko, a mimo to użytkownik czeka kilka sekund na interakcję, problem leży najczęściej po stronie JS i renderowania. Jeżeli natomiast odpowiedź z serwera trwa długo, optymalizacja Reacta poprawi tylko wrażenie po otrzymaniu danych, ale nie skróci czekania na same dane.

React może być też współwinny przez złą strategię ładowania. Przykład: przy pierwszym wejściu użytkownika ładujesz wszystkie ekrany aplikacji (monolityczny bundle), zamiast tylko aktualnie potrzebny widok. Tutaj wchodzi do gry lazy loading i code splitting.

Prosty proces diagnozy przed optymalizacją

Zanim rozpocznie się „polowanie na mikrooptymalizacje”, lepiej przejść kilka kroków:

  1. Pomiary w przeglądarce – uruchom aplikację w trybie produkcyjnym, włącz Performance i React Profiler w DevTools i zarejestruj typowe scenariusze (pierwsze wejście, nawigacja, intensywne użycie list/formularzy).
  2. Zapis czasu działań – dla podejrzanych operacji (np. filtrowanie, sortowanie, pobieranie danych) dodaj proste logi czasu z użyciem console.time / console.timeEnd lub własnej funkcji pomiarowej.
  3. Porównanie środowisk – sprawdź różnice między development buildem a production buildem; dev bywa wolniejszy przez dodatkowe sprawdzenia, co bywa mylące.
  4. Profilowanie bundle – zmierz rozmiar paczki (webpack-bundle-analyzer, Source Map Explorer, narzędzia Vite/Rollup/Next) i zobacz, które biblioteki dominują.

Taki proces pozwala zidentyfikować, czy główna dźwignia to memoizacja (nadmiarowe re-renderingi), lazy loading (za duży bundle na starcie), czy cięcie samej logiki JS (zbyt ciężkie obliczenia). W kolejnych sekcjach każde z tych narzędzi jest omawiane osobno i w połączeniu.

Zbliżenie laptopa z otwartą stroną wyszukiwarki internetowej
Źródło: Pexels | Autor: cottonbro studio

Skąd się bierze wolna aplikacja w React – trzy główne źródła

Renderowanie i ponowne renderowanie komponentów

React opiera się na częstych re-renderach i porównywaniu wirtualnego drzewa z realnym DOM. Sam mechanizm jest zoptymalizowany, ale łatwo go przeciążyć. Główny problem to nadmiarowe renderowanie całych poddrzew przy zmianie fragmentu stanu.

Do typowych przyczyn należą:

  • Zbyt szeroki state w komponencie nadrzędnym, który przekazuje wiele propsów w dół, a zmiana jednego pola powoduje aktualizację wszystkich dzieci.
  • Niewłaściwe użycie Context API – globalny context z dużym obiektem stanu, który zmienia się często, „wysadza” renderowanie wszystkich komponentów korzystających z contextu.
  • Przekazywanie za każdym razem nowych referencji funkcji i obiektów, co psuje memoizację niżej (przez różne identity propsów).

Memoizacja w React (React.memo, useMemo, useCallback) działa jak filtr, który zatrzymuje niepotrzebne re-rendery. Jednak reaguje ona tylko na zmiany propsów (w przypadku React.memo) lub wartości w zależnościach hooka. Jeśli logika architektury powoduje ciągłe zmiany, sama memoizacja bywa zbyt słabym lekarstwem.

Rozmiar i struktura bundle

Druga grupa problemów wynika z ilości JavaScriptu dostarczanej użytkownikowi na start. Im większy bundle:

  • tym dłużej trwa jego pobranie po sieci,
  • tym dłużej przeglądarka go parsuje i kompiluje,
  • tym wolniej użytkownik otrzymuje interaktywną stronę.

Przy jednym, monolitycznym bundlu cała aplikacja React, wszystkie widoki, wszystkie rzadko używane funkcje (np. panel administracyjny, kreator raportów) są ładowane od razu. Dla użytkownika, który wchodzi jedynie na prostą listę danych, to czysty koszt bez korzyści. Tutaj szczególnie dużo daje lazy loading komponentów i code splitting – podział kodu na mniejsze fragmenty ładowane w momencie potrzeby.

Różnica między dev a prod jest znacząca. W trybie produkcyjnym bundler robi:

  • minifikację (usuwanie spacji, skracanie nazw),
  • tree shaking (usuwanie nieużywanych eksportów),
  • code splitting (jeśli włączony) – generuje osobne pliki – chunki.

Dlatego analiza bundle ma sens głównie na produkcyjnym buildzie lub na lokalnym buildzie z produkcyjnymi ustawieniami.

Koszt logiki w JavaScript – obliczenia, pętle, selektory

Trzeci obszar to czysty koszt CPU – ciężkie obliczenia, które blokują główny wątek JS. React nie jest tu ani winny, ani wybawcą – jeśli każdorazowo przy renderze przetwarzasz duże kolekcje danych, efekt będzie odczuwalny niezależnie od frameworka.

Przykładowe źródła problemów:

  • Sortowanie i filtrowanie dużej listy (np. kilkanaście tysięcy rekordów) przy każdym wpisie znaku w polu wyszukiwarki.
  • Złożone przeliczenia po stronie klienta (agregacje, statystyki, parsowanie rozbudowanych struktur JSON) uruchamiane przy każdym re-renderze.
  • Selektory Redux/RTK bez memoizacji, które za każdym razem tworzą nowy wynik na podstawie dużego stanu.

Memoizacja obliczeń (useMemo, memoizowane selektory) pomaga zminimalizować liczbę takich kosztownych operacji. Gdy wejścia się nie zmieniają, rezultat zostaje z pamięci. Trzeba jednak coś za coś: zwiększa się zużycie pamięci i złożoność kodu, a w skrajnych przypadkach lepszym rozwiązaniem bywa przeniesienie części logiki na backend lub do Web Workera.

Kiedy która dźwignia jest skuteczna

Trzy główne narzędzia – memoizacja, lazy loading i analiza bundle – rozwiązują różne klasy problemów.

  • Memoizacja pomaga, gdy głównym problemem są nadmiarowe renderingi i powtarzane obliczenia przy interakcjach.
  • Lazy loading / code splitting pomaga, gdy długo ładuje się pierwsza strona, a użytkownik się nudzi patrząc na biały ekran.
  • Cięcie funkcjonalności lub logiki bywa jedynym rozsądnym krokiem, gdy próbujesz robić bardzo ciężkie operacje po stronie przeglądarki (np. generowanie raportu PDF z tysięcy rekordów).

Dobry proces optymalizacji zwykle łączy te trzy podejścia: najpierw diagnoza i analiza bundle, potem rozbicie kodu na logiczne części (lazy loading), a na końcu dołożenie memoizacji tam, gdzie faktycznie przyspiesza powtarzalne scenariusze.

Zbliżenie ekranu laptopa z kodem JavaScript aplikacji React
Źródło: Pexels | Autor: Markus Winkler

Podstawy memoizacji w React – kiedy naprawdę ma sens

Na czym polega memoizacja i jaki ma koszt

Memoizacja to technika zamiany czasu procesora na pamięć. Zamiast liczyć rezultat funkcji za każdym razem od nowa, przechowujesz wynik dla danych wejściowych i przy kolejnych wywołaniach sprawdzasz, czy wejścia się nie zmieniły. Jeśli są takie same – zwracasz zapamiętany rezultat.

W przypadku Reacta memoizować można:

  • całe komponenty funkcyjne – za pomocą React.memo,
  • wartości i rezultaty obliczeń wewnątrz komponentu – za pomocą useMemo,
  • funkcje (callbacki przekazywane jako propsy) – za pomocą useCallback.

Koszt memoizacji to:

  • dodatkowe porównania wejść (propsów, zależności),
  • utrzymywanie w pamięci poprzednich wartości i wyników,
  • bardziej skomplikowany kod (więcej hooków, więcej warunków brzegowych).

Dlatego memoizacja pomaga głównie w przypadku komponentów i obliczeń „średnio ciężkich” i ciężkich. Przy bardzo lekkich komponentach, które tylko renderują kilka prostych elementów, narzut porównań może być większy niż potencjalna oszczędność.

React.memo, useMemo, useCallback – różne poziomy działania

Te trzy mechanizmy działają na różnych poziomach:

  • React.memo(Component) – memoizuje cały komponent. React porównuje propsy poprzedniego i aktualnego renderu; jeśli się nie zmieniły (płytkie porównanie), komponent nie jest ponownie renderowany.
  • useMemo(factory, deps) – memoizuje wynik obliczenia w obrębie jednego renderu komponentu. Jeśli zależności się nie zmieniły, zwraca poprzedni wynik.
  • useCallback(fn, deps) – zwraca stabilną referencję funkcji, która zmienia się tylko wtedy, gdy zmienią się zależności. To zwykle wsparcie dla React.memo i integracji z bibliotekami.

Dobrze jest myśleć o nich jak o trzech warstwach:

  • React.memo – filtr dla całych drzew komponentów (dzieci).
  • useMemo – filtr na poziomie obliczeń w funkcji komponentu.
  • useCallback – filtr na poziomie referencji funkcji przekazywanych do dzieci.

W praktyce często stosuje się je razem: komponent dziecka opakowany w React.memo, a rodzic używa useCallback, aby nie generować nowych funkcji przy każdym renderze, co pozwala React.memo skutecznie wykryć brak zmian propsów.

Kryteria wyboru: kiedy warto memoizować

Kilka kryteriów, które pomagają zdecydować, czy sięgnąć po memoizację:

Jak rozpoznać komponenty-kandydatów do memoizacji

Zanim w kodzie pojawi się pierwsze React.memo czy useMemo, przydaje się prosty filtr: które fragmenty interfejsu faktycznie generują koszt?

Dobrymi kandydatami na memoizację są przede wszystkim:

  • Listy i tabele – pojedynczy wiersz tabeli może być lekki, ale jeśli renderujesz ich kilkaset, suma kosztów rośnie geometrycznie przy każdym re-renderze.
  • Widgety z zagnieżdżonym drzewem – np. drzewo katalogów, rozbudowany sidebar, wielopoziomowe menu.
  • Komponenty z ciężkimi obliczeniami wewnątrz – selekcja, sortowanie, przeliczanie danych, mapy, wykresy.

Słabymi kandydatami są natomiast:

  • bardzo małe komponenty prezentacyjne wyświetlające kilka tekstów lub ikon,
  • komponenty, które i tak zmieniają propsy przy prawie każdym re-renderze rodzica (np. licznik, który co sekundę zwiększa wartość),
  • elementy o niewielkiej liczbie wystąpień (np. pojedyncze przyciski, pola formularza) – koszt ich ponownego renderu jest znikomy.

Dobrym testem bywa „eksperyment mentalny”: jeśli komponent uruchamiałby się 1000 razy pod rząd przy jednej interakcji, czy faktycznie byłoby to odczuwalne? Jeśli odpowiedź brzmi „raczej nie”, z dużym prawdopodobieństwem memoizacja jest tam jedynie szumem w kodzie.

Najczęstsze mity wokół memoizacji w React

Memoizacja bywa traktowana jak „tryb turbo” włączany wszędzie. W praktyce część popularnych przekonań prowadzi do odwrotnego efektu.

  • Mit 1: „React.memo przyspiesza każdy komponent” – w małych komponentach narzut porównań propsów potrafi być droższy niż ponowny render. Efekt: więcej kodu, brak mierzalnej korzyści.
  • Mit 2: „useCallback trzeba dawać przy każdej funkcji w propsach” – nadmiar useCallback zwiększa złożoność, a jeśli komponent-dziecko nie jest memoizowany, stabilność referencji nie przynosi żadnego zysku.
  • Mit 3: „useMemo należy otaczać wszystkie obliczenia” – lekkie obliczenia (formatowanie daty, prosty map/filter na małej tablicy) szybciej policzyć niż zarządzać ich cachem.

Kontrast jest prosty: globalne dodanie memoizacji „na wszelki wypadek” często spowalnia kod, podczas gdy celowane użycie tylko w naprawdę gorących ścieżkach potrafi przynieść spektakularną poprawę responsywności.

React.memo w zastosowaniach: kiedy działa najlepiej

React.memo jest najskuteczniejsze wtedy, gdy spełnione są jednocześnie trzy warunki:

  • komponent jest względnie ciężki lub występuje w wielu instancjach,
  • jego propsy zmieniają się rzadziej niż jego rodzic się re-renderuje,
  • propsy są stabilne referencyjnie (brak nowych obiektów/funkcji tworzonych w locie).

Typowy przykład to lista elementów, gdzie lista jako całość odświeża się często (np. dodawanie/usuwanie rekordów, odświeżanie danych), ale pojedynczy wiersz rzadko zmienia swoje wartości. Opakowanie pojedynczego wiersza w React.memo znacząco redukuje liczbę niepotrzebnych re-renderów.

React.memo – przykładowy wzorzec z listą

Prosty schemat porównawczy pokazuje różnicę między podejściami:


// Bez React.memo – każdy <Row /> renderuje się przy każdej zmianie listy
function Row({ item, onSelect }: { item: Item; onSelect: (id: string) => void }) {
  console.log('render Row', item.id);
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
}

function List({ items, onSelect }: Props) {
  return (
    <ul>
      {items.map(item => (
        <Row key={item.id} item={item} onSelect={onSelect} />
      ))}
    </ul>
  );
}

Wariant zoptymalizowany wygląda tak:


const Row = React.memo(function Row({
  item,
  onSelect,
}: {
  item: Item;
  onSelect: (id: string) => void;
}) {
  console.log('render Row', item.id);
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
});

function List({ items, onSelect }: Props) {
  const handleSelect = React.useCallback((id: string) => {
    onSelect(id);
  }, [onSelect]);

  return (
    <ul>
      {items.map(item => (
        <Row key={item.id} item={item} onSelect={handleSelect} />
      ))}
    </ul>
  );
}

Różnica: wcześniej każdy nowy render List generował nowe funkcje inline i nowe referencje propsów, więc wszystkie wiersze renderowały się ponownie. Teraz stabilny callback i React.memo filtrują te przypadki, w których item faktycznie się nie zmienił.

React.memo – kiedy jest antywzorcem

Są też scenariusze, w których React.memo wprowadza więcej szkody niż pożytku:

  • Komponent zmienia się przy każdym re-renderze roditca – np. licznik, który zawsze dostaje nowe value. React.memo nic nie zatrzyma, a doda tylko koszt porównania propsów.
  • Propsy są niestabilne z definicji – jeśli do dziecka przekazywany jest new Date(), losowe ID albo zagnieżdżony obiekt tworzony inline, React.memo będzie ciągle „przepuszczał” render.
  • Komponent jest bardzo mały – np. opakowanie pojedynczego <button> czy <span> bez ciężkiego wnętrza przynosi zwykle marginalne zyski.

W takich sytuacjach lepszym pomysłem bywa zmiana struktury stanu (wydzielenie mniejszego komponentu, lokalny state) niż wprowadzanie memoizacji na siłę.

useMemo i useCallback – porównanie ról

Oba hooki służą do stabilizowania wartości, jednak ich rola jest inna:

  • useMemo – „pamięta” wynik obliczeń. Liczy coś tylko, gdy zmienią się zależności.
  • useCallback – „pamięta” referencję funkcji. Nie robi żadnych obliczeń poza konstrukcją samej funkcji.

W praktyce useMemo wykorzystuje się dla wszystkiego, co ma koszt obliczeniowy: filtrowanie listy, transformacje danych, tworzenie drogich struktur (np. instancje klas, konfiguracje wykresów). useCallback natomiast zabezpiecza interfejsy API oczekujące stabilnych funkcji (np. React.memo, useEffect w dziecku, biblioteki typu virtualized list).

useMemo – przykładowe zastosowanie z drogim filtrowaniem

Zestawienie bez i z memoizacją dobrze widać na listach z filtrowaniem.


// Bez useMemo – filtr uruchamia się przy każdym renderze
function Products({ products, search }: Props) {
  const filtered = products.filter(p =>
    p.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <ul>
      {filtered.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Jeśli products to duża lista, a komponent renderuje się często z tych samych danych (np. zmienia się tylko inne pole formularza), lepszy będzie wariant:


function Products({ products, search }: Props) {
  const filtered = React.useMemo(() => {
    const lower = search.toLowerCase();
    return products.filter(p => p.name.toLowerCase().includes(lower));
  }, [products, search]);

  return (
    <ul>
      {filtered.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Różnica jest wyraźna, gdy użytkownik wpisuje tekst w innym polu formularza na tym samym ekranie, a lista produktów się nie zmienia. Bez useMemo filtrowanie wykonywałoby się przy każdym znaku, mimo braku zmian listy.

useMemo jako wsparcie dla referencji

useMemo pomaga też w utrzymaniu stabilności obiektów przekazywanych w dół. Różnica między tworzeniem obiektu inline a memoizacją jest dla React.memo kluczowa.


// Każdy render tworzy nowy obiekt options
<Child options={{ pageSize, sortBy }} />

// Stabilny obiekt – zmieni się tylko przy zmianie pageSize/sortBy
const options = React.useMemo(
  () => ({ pageSize, sortBy }),
  [pageSize, sortBy]
);

<Child options={options} />

W pierwszym wariancie dziecko opakowane w React.memo i tak będzie się renderowało, bo options ma za każdym razem inną referencję. W drugim, dopóki pageSize i sortBy pozostają bez zmian, referencja jest ta sama i memoizacja komponentu dziecka faktycznie „zaskakuje”.

useCallback – kiedy faktycznie daje przewagę

Porównując dwa podejścia, różnica sprowadza się do częstotliwości tworzenia nowych funkcji:


// Bez useCallback – nowa funkcja przy każdym renderze
<Row onClick={() => onSelect(id)} />

// Z useCallback – stabilna referencja dopóki id/onSelect się nie zmieni
const handleClick = React.useCallback(() => {
  onSelect(id);
}, [id, onSelect]);

<Row onClick={handleClick} />

useCallback zaczyna mieć sens przy:

  • komponentach-dzieciach opakowanych w React.memo,
  • callbackach przekazywanych głęboko w drzewo (np. do hooków w środku dziecka, które reagują na zmianę referencji),
  • integracji z bibliotekami wymagającymi stabilnych funkcji (np. biblioteki drag&drop, mapy, wirtualizacja list).

Jeśli dziecko nie jest memoizowane i nie ma wewnętrznych efektów reagujących na zmianę callbacka, useCallback nie zmieni niczego poza zwiększeniem liczby hooków.

Typowe antywzorce z useMemo i useCallback

Dwa podejścia pojawiają się wyjątkowo często i prowadzą do problemów:

  • „Otaczanie wszystkiego” – każda funkcja i każde obliczenie trafia do useCallback/useMemo. Taki kod jest trudny do czytania, a zysk z memoizacji często zerowy.
  • Błędne zależności – pominięcie wartości w tablicy deps powoduje używanie „starych” danych. To wygeneruje subtelne bugi, których nie wychwyci linter.

Bezpieczniejsza strategia to ruch w odwrotnym kierunku: najpierw kod bez memoizacji, potem pomiar (np. w Profilerze), a dopiero po identyfikacji najwolniejszych miejsc – dokładanie useMemo/useCallback tylko tam, gdzie występują realne zyski.

Kolorowy kod źródłowy JavaScript na ekranie monitora
Źródło: Pexels | Autor: Markus Spiske

React.memo w praktyce – przykłady, wzorce i antywzorce

Porównanie domyślne vs. własna funkcja porównująca

React.memo domyślnie używa płytkiego porównania propsów. Dla prostych typów (string, number, boolean) i płytkich obiektów zazwyczaj to wystarcza. W bardziej zaawansowanych przypadkach można przekazać własną funkcję porównującą:


type Props = { value: Data; loading: boolean };

const Chart = React.memo(function Chart(props: Props) {
  // render wykresu
}, areEqual);

function areEqual(prev: Props, next: Props) {
  if (prev.loading !== next.loading) return false;
  // porównanie tylko wybranej części value
  return prev.value.id === next.value.id;
}

Własna funkcja porównująca ma sens, gdy:

  • komponent ma rozbudowane propsy, ale tylko część z nich wpływa na wynik renderu,
  • aktualizacja części pól jest bardzo częsta, a pozostałych – sporadyczna,
  • domyślne płytkie porównanie prowadziłoby do częstszych niż potrzebne re-renderów.

Z kolei, gdy areEqual staje się długą funkcją z wieloma warunkami, zwykle wygodniej zreorganizować propsy lub wydzielić mniejszy komponent, zamiast przerzucać całą logikę na etap porównywania.

Lista jako całość vs. pojedynczy element listy

W przypadku list często pojawia się pytanie: memoizować całą listę czy pojedyncze elementy? Porównanie dwóch podejść:

  • Memoizacja całej listyReact.memo(List) chroni renderowanie listy tylko wtedy, gdy items nie zmieniają się w ogóle. Zmiana jednego elementu wymusi render całej listy.
  • Memoizacja elementuReact.memo(Row) pozwala, aby zmiana jednego elementu listy ponownie wyrenderowała tylko ten wiersz.

Najczęściej zadawane pytania (FAQ)

Po czym poznać, że moja aplikacja React jest za wolna?

Najczęstsze objawy po stronie użytkownika to długie wczytywanie pierwszego widoku (biała strona), opóźnione reakcje na kliknięcia i wyraźne „przycinki” podczas przewijania. Czasem widać też lag przy wpisywaniu w input – tekst pojawia się z opóźnieniem, a animacje lub przejścia między widokami szarpią.

Od strony metryk problem ujawnia się w TTI (strona wygląda na gotową, ale nie reaguje), wysokim LCP oraz dużych opóźnieniach interakcji. Jeśli użytkownik widzi layout, ale nic się nie dzieje po kliknięciu, zwykle winny jest zbyt duży bundle JS lub nadmiar renderowań komponentów.

Jak odróżnić, czy za wolne działanie odpowiada backend czy React?

Najprostsze porównanie to wykonanie tych samych zapytań API poza aplikacją: w Postmanie, curl albo w czystym HTML z prostym fetch. Jeśli odpowiedź przychodzi szybko, a w React użytkownik wciąż długo czeka na interakcję, problem leży głównie po stronie frontendu (JS, renderowanie, strategia ładowania).

Dodatkowo w zakładce Network można sprawdzić czasy odpowiedzi i rozmiar danych. Scenariusz: API jest szybkie, JSON niewielki, ale po jego otrzymaniu interfejs „mieli” kilka sekund – to sygnał, że trzeba przyjrzeć się logice po stronie Reacta (ciężkie obliczenia, sortowanie, filtry, niepotrzebne re-rendery).

Kiedy używać memoizacji w React (React.memo, useMemo, useCallback)?

Memoizacja ma sens, gdy komponenty renderują się zbyt często przy tych samych danych albo gdy pojedyncze renderowanie jest kosztowne (np. duże listy, złożone obliczenia, drogie komponenty tabel/wykresów). React.memo ogranicza niepotrzebne re-rendery dzieci, a useMemo/useCallback stabilizują referencje obiektów i funkcji, żeby porównania propsów były skuteczne.

Nie ma sensu „owijać” wszystkiego na ślepo. Przy prostych, tanich komponentach narzut memoizacji bywa większy niż zysk. Typowy kompromis: memoizować elementy list, cięższe widoki i funkcje przekazywane głęboko w drzewo, a pomijać drobne kontrolki bez zauważalnego kosztu.

Czym różni się lazy loading od zwykłego importu w React i kiedy go stosować?

Zwykły import ładuje kod komponentu od razu, w głównym bundle. Lazy loading (np. React.lazy + Suspense) dzieli kod na chunki i wczytuje konkretny komponent dopiero w momencie użycia. Różnica jest podobna jak między jedną grubą książką a serią cienkich zeszytów, które bierzesz z półki tylko wtedy, gdy ich potrzebujesz.

Lazy loading opłaca się szczególnie dla rzadko używanych widoków: panel admina, kreatory raportów, ekrany ustawień czy duże moduły analityczne. Ekrany, które są pierwszym punktem kontaktu dla większości użytkowników (np. dashboard po zalogowaniu), często lepiej trzymać w głównym bundle, żeby uniknąć zbędnego opóźnienia przy każdym wejściu.

Jak sprawdzić, czy mój bundle React jest za duży i co z tym zrobić?

Pierwszy krok to zbudowanie aplikacji w trybie produkcyjnym i sprawdzenie wielkości wygenerowanych plików JS. Kolejny – użycie narzędzi takich jak webpack-bundle-analyzer, Source Map Explorer lub wbudowane analizatory w Vite/Next. Pozwalają one zobaczyć, które biblioteki zajmują najwięcej miejsca i jak wygląda podział na chunki.

Jeśli okazuje się, że jeden plik JS dominuje (monolit), pomocne są: code splitting (dynamiczne importy), lazy loading ciężkich widoków, zastąpienie „ciężkich” bibliotek lżejszymi odpowiednikami i usunięcie nieużywanych modułów. Dobre porównanie: zamiast dźwigać cały plecak na każdy spacer, pakujesz tylko to, co potrzebne na daną trasę.

Dlaczego aplikacja React działa dobrze w dev, a wolno w produkcji na telefonie?

Środowisko developerskie zwykle jest uruchamiane na szybkim komputerze, w sieci lokalnej, często z mniejszym realnym obciążeniem i innymi ustawieniami bundlera. Produkcyjny bundle, mimo optymalizacji (minifikacja, tree shaking), trafia na słabsze urządzenia mobilne, gdzie CPU i przeglądarka radzą sobie gorzej z dużą ilością JS i częstymi renderami.

Dobry zwyczaj to testowanie produkcyjnego builda lokalnie, włączanie throttlingu CPU i sieci w DevTools oraz profilowanie działań typowego użytkownika na realnym telefonie. Różnica między „szybko na laptopie dev” a „mułowato na budżetowym smartfonie” często ujawnia problemy z wielkością bundla i nieoptymalnym przepływem stanu.

Jaki prosty proces diagnozy wydajności React warto stosować przed refaktorem?

Najpraktyczniejsze jest połączenie kilku kroków: najpierw uruchomić aplikację w trybie produkcyjnym i zrobić nagrania w zakładce Performance i React Profiler dla typowych scenariuszy (pierwsze wejście, nawigacja, intensywna praca z listą lub formularzem). To pokazuje, które komponenty renderują się najczęściej i gdzie JS blokuje główny wątek.

Następnie dobrze jest dodać proste pomiary czasu (np. console.time) wokół podejrzanych operacji: filtrowanie, sortowanie, przeliczanie agregatów. Na końcu – sprawdzić rozmiar bundla i strukturę importów. Dzięki temu łatwiej zdecydować, czy główną dźwignią będzie memoizacja, lazy loading, czy uproszczenie samej logiki JS.