Dlaczego INP boli właśnie aplikacje React
Aby precyzyjnie podejść do problemu, musimy rozłożyć INP na podstawowe części. Każda interakcja przechodzi przez trzy krytyczne etapy, które bezpośrednio decydują o odczuciach użytkownika. Pierwszym z nich jest , określający czas niezbędny na podjęcie obsługi zdarzenia przez przeglądarkę. Następnie do akcji wkracza faza przetwarzania napędzana przez event handler oraz wynikający z niego render. Całość zamyka , który ostatecznie weryfikuje opóźnienie przed wyświetleniem zmodyfikowanego interfejsu. Nowa metryka Google bezwzględnie rejestruje najgorszy z tych incydentów na przestrzeni całej sesji klienta.
W potężnych aplikacjach zbudowanych w React każdy ze wspomnianych etapów potrafi skutecznie spowolnić działanie produktu. Główny wątek notorycznie dusi się podczas procesu hydratacji lub przy przeciągającym się renderowaniu. Sam event handler niemal nieustannie wyzwala kaskadowe re-rendery i zmusza algorytmy do przeliczania bardzo dużych ilości informacji bez żadnego sensownego uzasadnienia. Masywne drzewo komponentów z łatwością powstrzymuje ostateczny krok, w którym przeglądarka w końcu rysuje nowy widok. Wszystkie prezentowane dziś wzorce bezbłędnie potwierdzają absolutnie jedną tezę. Każda interakcja użytkownika uruchamia istną lawinę operacji, z którymi silnik systemu po prostu nie potrafi się uporać przed wygenerowaniem kolejnej klatki.
Wzorzec 1: Re-render całego drzewa przy każdej interakcji
Najczęstszym błędem obciążającym procesory jest osadzenie stanu na zbyt wysokim poziomie architektury. Przypadkowe kliknięcie lub wprowadzenie chociażby jednego znaku w formularzu natychmiast inicjuje przebudowę tych komponentów, które kompletnie nie wykorzystują tej zmodyfikowanej wartości.
Tutaj każde naciśnięcie klawisza w polu wyszukiwania zmusza Reacta do ponownego renderu ExpensiveList, mimo że lista nie korzysta z wartości search. Przy dwóch tysiącach elementów czas przetwarzania interakcji rośnie do poziomu, który INP bezględnie wyłapie.
Rozwiązanie zaczyna się od memoizacji komponentu, który nie powinien reagować na zmianę:
Dzięki memo React pomija ponowny render listy, dopóki jej propsy się nie zmienią. W świeżym projekcie React Compiler zrobi część tej pracy automatycznie, ale nadal musisz rozumieć granice renderowania. Kompilator nie naprawi źle ustawionego stanu.
Wzorzec 2: Ciężkie zadanie synchroniczne w event handlerze
Drugi wzorzec to klasyk: użytkownik klika, a w odpowiedzi odpala się sortowanie, filtrowanie albo transformacja kilku tysięcy rekordów — wszystko synchronicznie, w jednym bloku.
Problem polega na tym, że heavyCompare na dużym zbiorze potrafi zająć główny wątek na kilkaset milisekund. Przez ten czas przeglądarka nie odrysuje niczego, czyli ani efektu kliknięcia, ani nawet wskaźnika ładowania. INP zmierzy całe to opóźnienie jako jedną fatalną interakcję.
React 18 dał na to czytelne narzędzie. Dzięki useTransition możemy oznaczyć aktualizację jako i dzięki czemu przeglądarka najpierw zareaguje na kliknięcie, a dopiero potem dokończy ciężką pracę:
Interakcja staje się responsywna, ponieważ użytkownik od razu widzi reakcję, czyli wskaźnik sortowania zamiast zamrożonego interfejsu. Jeśli zadanie jest naprawdę ciężkie i czysto obliczeniowe, przenieś je do . Wtedy w ogóle zdejmujesz je z głównego wątku.
Wzorzec 3: Renderowanie tysięcy elementów naraz
Trzeci wzorzec dotyczy długich list i tabel. Nawet jeśli każdy wiersz jest lekki, sama ich liczba sprawia, że przeglądarka musi utrzymać i przeliczyć ogromne drzewo DOM. Przy każdej interakcji — przewinięciu, rozwinięciu, filtrze — ten ciężar wraca.
Odpowiedzią jest , czyli renderujemy tylko te wiersze, które faktycznie są w widocznym oknie, a resztę zastępujemy odpowiednio wysokim pustym obszarem. Najwygodniej zrobić to dziś biblioteką TanStack Virtual:
Niezależnie od tego, czy danych jest pięćset czy pięćdziesiąt tysięcy, w DOM żyje zaledwie kilkanaście węzłów. Interakcje przestają zależeć od rozmiaru zbioru, a to dokładnie ta zależność, która rozsadzała INP. Warto przy tym pamiętać, że wirtualizacja nie zawsze jest konieczna, ponieważ przy umiarkowanych listach często wystarczy zwykła paginacja, która w ogóle nie wpuszcza nadmiaru danych do DOM.
Wzorzec 4: Stan w jednym wielkim kontekście
Czwarty wzorzec jest bardziej architektoniczny. Kiedy cały stan aplikacji ląduje w pojedynczym kontekście React, każda jego zmiana powiadamia wszystkich konsumentów — także te komponenty, którym zmieniła się jedna, zupełnie obojętna dla nich wartość.
Przełączenie motywu nie powinno mieć żadnego wpływu na komponent koszyka, a jednak ma — bo obiekt value powstaje na nowo przy każdym renderze providera, więc React traktuje go jako zmieniony dla wszystkich. Najprostsze i najtrwalsze rozwiązanie to rozdzielenie kontekstów według tego, co faktycznie zmienia się razem:
Konsument koszyka subskrybuje teraz wyłącznie CartContext i pozostaje obojętny na zmiany motywu czy danych użytkownika. Jeśli stan jest naprawdę złożony, alternatywą jest sięgnięcie po bibliotekę z selektorami, jak Zustand, gdzie komponent przerenderowuje się tylko wtedy, gdy zmieni się konkretny wycinek stanu, który czyta.
Wzorzec 5: Synchroniczna walidacja i formatowanie przy każdym keystroke
Ostatni wzorzec to formularze, które przy każdym wciśniętym klawiszu uruchamiają kosztowną walidację, formatowanie albo przeliczenie zależnych pól.
Walidacja całego schematu przy każdym znaku to praca, której użytkownik w danym momencie wcale nie potrzebuje — liczy się dopiero przy opuszczeniu pola albo próbie wysłania. Tutaj naturalnym narzędziem jest useDeferredValue, który pozwala odłożyć kosztowną reakcję na zmianę, zachowując natychmiastową responsywność samego pola:
Pole formularza aktualizuje się od razu, ponieważ korzysta z values, podczas gdy ciężka walidacja pracuje na deferredValues i nadąża w tempie, które nie blokuje wpisywania. W praktyce jeszcze lepiej połączyć to z biblioteką taką jak React Hook Form, która domyślnie waliduje przy zdarzeniach typu blur albo submit, a nie na każdy znak — o całym stacku formularzowym pisałem w osobnym artykule o React Hook Form i Zod.
Jak namierzyć, który wzorzec psuje Twój INP
W realnym projekcie nie zaczynaj od strzelania na ślepu i zacznij od danych terenowych, bo INP jest metryką sesyjną i tylko realni użytkownicy klikają w miejsca, które są problematyczne. Search Console pokaże adresy z problemem, a Googleweb-vitals podepniesz pod własną analitykę i zbierzesz atrybucję: konkretny element, typ interakcji, czas.
Gdy już wiesz, gdzie szukać, panel Performance w DevTools i sekcja Interactions pozwolą zobaczyć rozbicie pojedynczej interakcji na input delay, przetwarzanie i prezentację. To rozbicie od razu kieruje Cię do właściwego wzorca: długi input delay sugeruje zajęty główny wątek, długie przetwarzanie wskazuje na ciężki handler albo kaskadę re-renderów, a długa prezentacja zdradza zbyt duże drzewo DOM.
