Przejdź do treści

INP w React. 5 wzorców, które niszczą Interaction to Next Paint i jak je naprawić kodem

INP powyżej 200 ms w React? Winowajcą jest zazwyczaj jeden z tych pięciu wzorców. Dowiedz się, jak je zidentyfikować i naprawić z inżynieryjną precyzją.

Maciej Sala

Founder StriveLab

7 min czytaniaOpublikowano 30 maja 2026 (Aktualizacja 6 lipca 2026)

Dlaczego INP jest problemem właśnie w aplikacjach 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.

Code
// ŹLE: jeden stan steruje wszystkim, a każda zmiana wymusza re-render całej listy
function Dashboard() {
  const [search, setSearch] = useState('')
  const [items] = useState(() => generateItems(2000))
 
  return (
    <div>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      {/* Ta lista przelicza się przy każdym znaku w search, choć od niego nie zależy */}
      <ExpensiveList items={items} />
    </div>
  )
}

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ę:

Code
// DOBRZE — ExpensiveList nie zależy od search, więc go izolujemy
const ExpensiveList = memo(function ExpensiveList({
  items,
}: {
  items: Item[]
}) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
})

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.

Code
// ŹLE — sortowanie 10 000 rekordów blokuje główny wątek przed odrysowaniem
function ProductTable({ products }: { products: Product[] }) {
  const [sorted, setSorted] = useState(products)
 
  function handleSort(key: keyof Product) {
    const result = [...products].sort((a, b) => heavyCompare(a, b, key))
    setSorted(result)
  }
 
  return <Table data={sorted} onSort={handleSort} />
}

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ę:

Code
// DOBRZE — useTransition oddaje pierwszeństwo odrysowaniu, sortowanie idzie w tle
function ProductTable({ products }: { products: Product[] }) {
  const [sorted, setSorted] = useState(products)
  const [isPending, startTransition] = useTransition()
 
  function handleSort(key: keyof Product) {
    startTransition(() => {
      const result = [...products].sort((a, b) => heavyCompare(a, b, key))
      setSorted(result)
    })
  }
 
  return (
    <>
      {isPending && <SortingIndicator />}
      <Table data={sorted} onSort={handleSort} />
    </>
  )
}

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.

Code
// ŹLE — 5000 wierszy w DOM, każda interakcja przelicza całość
function LogViewer({ logs }: { logs: LogEntry[] }) {
  return (
    <div className="h-[600px] overflow-auto">
      {logs.map((log) => (
        <LogRow key={log.id} entry={log} />
      ))}
    </div>
  )
}

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:

Code
// DOBRZE — renderujemy tylko widoczne wiersze
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
 
function LogViewer({ logs }: { logs: LogEntry[] }) {
  const parentRef = useRef<HTMLDivElement>(null)
 
  const virtualizer = useVirtualizer({
    count: logs.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
    overscan: 10,
  })
 
  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((row) => (
          <div
            key={logs[row.index].id}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${row.start}px)`,
              width: '100%',
            }}
          >
            <LogRow entry={logs[row.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}

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ść.

Code
// ŹLE — jeden kontekst na wszystko, zmiana theme przerenderowuje konsumentów cart
const AppContext = createContext<AppState | null>(null)
 
function AppProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
  const [cart, setCart] = useState<CartItem[]>([])
  const [user, setUser] = useState<User | null>(null)
 
  const value = { theme, setTheme, cart, setCart, user, setUser }
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}

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:

Code
// DOBRZE — osobne konteksty, zmiana jednego nie dotyka konsumentów drugiego
const ThemeContext = createContext<ThemeState | null>(null)
const CartContext = createContext<CartState | null>(null)
const UserContext = createContext<UserState | null>(null)

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.

Code
// ŹLE — pełna walidacja schematu przy każdym znaku
function CheckoutForm() {
  const [values, setValues] = useState(initialValues)
 
  function handleChange(field: string, value: string) {
    const next = { ...values, [field]: value }
    setValues(next)
    // Walidacja całego formularza na każdy keystroke — zbędna i kosztowna
    validateEntireSchema(next)
  }
 
  return <Form values={values} onChange={handleChange} />
}

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:

Code
// DOBRZE — pole reaguje natychmiast, walidacja podąża z opóźnieniem
function CheckoutForm() {
  const [values, setValues] = useState(initialValues)
  const deferredValues = useDeferredValue(values)
 
  const errors = useMemo(
    () => validateEntireSchema(deferredValues),
    [deferredValues],
  )
 
  return <Form values={values} errors={errors} onChange={setValues} />
}

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ć wzorzec React, który psuje 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.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czym różni się INP od FID?

FID, czyli First Input Delay, mierzył wyłącznie opóźnienie pierwszej interakcji i skupiał się wyłącznie na czasie niezbędnym przeglądarce do podjęcia przetwarzania zdarzenia. INP, czyli Interaction to Next Paint, wdrożony w 2024 roku, jest znacznie bardziej rygorystyczny. Rejestruje on wszystkie interakcje na przestrzeni pełnej sesji oraz cały ich cykl życia od opóźnienia na wejściu, przez realizację zdarzenia, aż po odrysowanie interfejsu. Przez ten dość brutalny model oceny wiele projektów spełniających wymagania FID oblewało testy INP.

Jaki INP jest uznawany za dobry?

Dobry wynik INP musi zamknąć się w granicy 200 milisekund na poziomie 75. percentyla. Oznacza to wprost, że aż 75% sesji użytkowników powinno charakteryzować się najdłuższym opóźnieniem nieprzekraczającym tej bezpiecznej wartości. Rezultat oscylujący w przedziale między 200 a 500 milisekund wymusza błyskawiczną interwencję inżynierską, a wszystko powyżej stanowi katastrofalny sygnał braku responsywności.

Czy memoizacja (React.memo, useMemo) naprawia INP?

Odpowiednio zastosowana memoizacja znacząco wspomaga proces, ale z pewnością nie jest cudownym środkiem naprawczym. Skuteczne ograniczenie re-renderów w oczywisty sposób obniża czas przetwarzania zdarzeń, gwarantując odczuwalną poprawę na poziomie INP. Niestety, mechanizm ten zawiedzie całkowicie w starciu ze skomplikowanymi zadaniami synchronicznymi czy blokującym silnik renderowaniem potężnych zbiorów danych. Architektura systemu będzie tam wymagała głębokiego rozbicia obciążających operacji, użycia asynchronicznych przejść lub pełnej wirtualizacji DOM.

Czy React Compiler rozwiązuje problemy z INP?

To potężne narzędzie częściowo rozwiązuje ten problem. Zautomatyzowanie memoizacji zwalnia z ręcznego implementowania dedykowanych hooków, systematycznie obniżając obciążenie interfejsu. Kompilator nie zlikwiduje jednak strukturalnej wagi wielkich list oraz morderczej złożoności ciężkich algorytmów synchronizujących widok. Likwidacja powtarzalnego błędu to świetna informacja, ale o fundamentalnych aspektach optymalizacji nadal musi decydować doświadczony inżynier.

Jak zmierzyć INP w aplikacji React?

Na prawdziwym placu boju wskaźniki INP perfekcyjnie rejestruje narzędzie Google Search Console, wsparte autentycznymi raportami od żywych użytkowników oraz genialną biblioteką analityczną. Warunki laboratoryjne pozwalają wykorzystać potencjał wbudowanych opcji deweloperskich i syntetycznych symulacji. Niemniej pamiętaj, że INP to przede wszystkim wynik wyciągnięty z trwającej sesji. Ostatecznym dowodem na sukces zawsze pozostają dane zgromadzone na podstawie realnych i bezwzględnych interakcji z oprogramowaniem.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat React

Czytaj dalej

Zobacz więcej wpisów
Core Web Vitals — jak przyspieszyć stronę i poprawić pozycję w Google

Google od wielu lat podkreśla, że wydajność i doświadczenie użytkownika mają znaczenie i Core Web Vitals są częścią sygnałów page experience. Najważniejsze jest jednak to, że wreszcie mierzą coś bardzo konkretnego: czy strona błyskawicznie pokazuje treść, czy odpowiednio reaguje na interakcje i czy nie „skacze" nieprzyjemnie podczas ładowania.

Maciej Sala

Maciej Sala

Founder StriveLab

Jak debugować niepotrzebne renderowanie w React?

Walka z nadmiarowymi renderami to stały punkt pracy z Reactem. O ile podwójne renderowanie w Strict Mode czy standardowa aktualizacja stanu rodzica to zachowania całkowicie naturalne, o tyle lawinowe przebudowywanie drzewa komponentów po jednym kliknięciu potrafi skutecznie zepsuć UX. React Profiler pomoże rozwiązać te problemy dostarczając twarde dane o wydajności Twojej aplikacji.

Maciej Sala

Maciej Sala

Founder StriveLab

Jak blokować kod psujący LCP i INP w Next.js?

Wydajność rzadko psuje się jednym wielkim commitem. Częściej tak: jedna biblioteka, jeden cięższy obraz, jeden skrypt third-party, jedna tabelka bez wirtualizacji. Każda zmiana z osobna wygląda niewinnie. Po miesiącu LCP świeci na czerwono, bundle jest spuchnięty, a nikt nie potrafi wskazać winnego PR-a. Bramka wydajnościowa odwraca tę kolejność — zły PR dostaje czerwony status, zanim w ogóle trafi na produkcję.

Maciej Sala

Maciej Sala

Founder StriveLab