Przejdź do treści

Jak debugować niepotrzebne renderowanie w React?

Zobacz, jak za pomocą React DevTools namierzyć przyczynę wielokrotnego renderowania, naprawić referencje i zoptymalizować Context API.

Maciej Sala

Founder StriveLab

10 min czytaniaOpublikowano 24 kwietnia 2026 (Aktualizacja 15 lipca 2026)

Pokażę proces, którego używam przy diagnozowaniu ponownych renderowań: React DevTools Profiler, , typowe przyczyny i poprawki. Bez rytuału z losowym useMemo w każdym pliku.

Czy ponowne renderowanie w React to zawsze problem?

Nie każde ponowne renderowanie jest problemem, ponieważ to wywołanie funkcji komponentu i porównanie nowego wyniku z poprzednim. Nie oznacza automatycznie zmiany DOM ani ponownego malowania ekranu, bo React wykonuje commit tylko tam, gdzie wynik faktycznie się zmienił. Ostateczny koszt zależy od urządzenia, danych i drzewa komponentów, więc sama liczba renderów nie pokaże nam precyzyjnie skali problemu lub czy problem rzeczywiście istnieje.

Ponowne renderowanie staje się problemem, gdy:

  1. Komponent jest ciężki w obliczeniach (filtrowanie 10 000 elementów, parsowanie dużych stringów, operacje na obrazach).
  2. Komponent ma wiele dzieci, a żadne z nich nie jest zoptymalizowane.
  3. Renderowania kaskadują — zmiana w rodzicu ponownie renderuje pół drzewa.
  4. Interakcje użytkownika są opóźnione — po kliknięciu przycisku odpowiedź przychodzi z 200-milisekundowym poślizgiem.

Jeśli strona działa szybko, nie optymalizuj prewencyjnie. Włączony i poprawnie działający automatycznie pokrywa wiele przypadków ręcznej memoizacji, ale nie zatrzyma aktualizacji własnego stanu, zmian Contextu, pętli useEffect ani skutków błędnych key. Sprawdziłem, co kompilator robi z useMemo i useCallback w osobnym artykule.

Narzędzia do debugowania ponownych renderowań w React

React DevTools Profiler

To pierwsza rzecz, po którą sięgam. Instalacja: rozszerzenie „React Developer Tools" do Chrome/Firefox.

Po zainstalowaniu, w DevTools pojawia się zakładka „⚛️ Profiler". Przebieg:

  1. Otwórz Profiler.
  2. Kliknij zielony „Record".
  3. Wykonaj interakcję, która wydaje się wolna (kliknij przycisk, wpisz coś w pole).
  4. Zatrzymaj nagrywanie.

Dostaniesz wykres płomieniowy z listą renderów posortowanych po czasie. Dla każdego komponentu widzisz:

  • Czas renderowania (kolorem: zielony = szybki, żółty = OK, czerwony = wolny).
  • Dlaczego się renderował (w zakładce „Why did this render?" — zmiana propsów, stanu albo hooka).
  • Liczbę renderów w zadanym czasie.

Pytanie, które zadaję w profilerze: czy ten komponent powinien się renderować przy tej interakcji? Jeśli użytkownik kliknął przycisk w stopce, a renderuje się cały układ strony, profiler pokaże łańcuch zdarzeń.

why-did-you-render

Dla głębszego debugowania instalacja @welldone-software/why-did-you-render:

Code
npm install @welldone-software/why-did-you-render --save-dev
Code
// src/wdyr.ts (ładowane przed React)
import React from 'react'
 
if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render')
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  })
}
Code
// W dowolnym komponencie
function Profile() {
  return <div>...</div>
}
Profile.whyDidYouRender = true

Przy każdym ponownym renderowaniu w konsoli pojawia się komunikat „Different reason" z dokładnym wskazaniem, który prop się zmienił i jak:

Code
[WDYR] Profile re-rendered because of props changes:
  user: different objects with the same keys
    { id: 1, name: 'John' } → { id: 1, name: 'John' }  (Object.is returned false)

Ten ostatni przypadek — „different objects with the same keys" — to najczęstszy problem. Przechodzę do niego.

React DevTools Highlight Updates

W DevTools → Profiler → Settings → „Highlight updates when components render". Każdy komponent, który się renderuje, dostaje krótkie zielone/żółte/czerwone obramowanie. To wizualna informacja zwrotna, która pokazuje aktywność Reacta na bieżąco.

Idealne do szybkiego sprawdzenia — „klikam przycisk, co się zaktualizowało?". Jeśli cały układ strony miga, masz kaskadę.

react-scan: podświetlanie renderów bez konfiguracji

Nowszy gracz w tym zestawie i najszybszy sposób na pierwszy rzut oka: react-scan podświetla renderujące się komponenty bez instalowania czegokolwiek w projekcie:

Code
npx react-scan@latest http://localhost:3000

Działa też jako tag <script> albo pakiet importowany przed Reactem. W odróżnieniu od Highlight Updates z DevTools pokazuje liczniki renderów bezpośrednio na elementach i agreguje statystyki, więc dobrze nadaje się do szybkiego audytu cudzego projektu — uruchamiasz go na działającej aplikacji i od razu widzisz, które obszary „grzeją się" przy interakcjach. Do precyzyjnej diagnozy przyczyn i tak wrócisz do Profilera i why-did-you-render, ale jako pierwsza warstwa rozpoznania react-scan jest nie do pobicia.

Typowe przyczyny niepotrzebnych renderów w React

Przyczyna 1: obiekty i tablice w propsach

Code
// ŹLE — nowy obiekt przy każdym renderze
function Parent() {
  return <ExpensiveChild config={{ theme: 'dark', locale: 'pl' }} />
}
 
function ExpensiveChild({ config }) {
  // useMemo w środku nie pomoże, bo referencja config się zmienia
  return <HeavyComponent data={config} />
}

Każdy render Parent tworzy nowy obiekt { theme: 'dark', locale: 'pl' }. JavaScript traktuje go jako różny od poprzedniego (Object.is zwraca false) i to psuje dwie rzeczy naraz. W pierwszym przypadku, gdy komponent ExpensiveChild jest owinięty w React.memo, memoizacja staje się całkowicie bezużyteczna. Porównanie propsów przy każdym renderze wykryje „zmianę”, co wymusi ponowne renderowanie komponentu. W drugim scenariuszu – nawet bez użycia memo – każdy useMemo, useEffect czy useCallback wewnątrz komponentu potomnego, który ma config w tablicy zależności, będzie bezpotrzebnie uruchamiany na nowo przy każdym renderze rodzica.

Warto tu doprecyzować, że bez React.memo dziecko renderuje się ponownie przy każdym renderze rodzica niezależnie od propsów, więc stabilna referencja sama w sobie tego nie zatrzyma. Stabilizacja referencji ma sens w parze z memoizacją dziecka albo gdy obiekt trafia do tablic zależności hooków.

Code
// DOBRZE — stabilna referencja
const CONFIG = { theme: 'dark', locale: 'pl' } // poza komponentem
 
function Parent() {
  return <ExpensiveChild config={CONFIG} />
}

Lub, gdy wartość zależy od stanu:

Code
function Parent({ theme }) {
  const config = useMemo(() => ({ theme, locale: 'pl' }), [theme])
  return <ExpensiveChild config={config} />
}

Z React Compilerem ten problem często znika automatycznie. Bez — trzeba pamiętać o stabilności referencji.

Przyczyna 2: funkcje tworzone w miejscu i przekazywane w propsach

Code
// ŹLE — nowa funkcja przy każdym renderze
function Parent() {
  return <ExpensiveButton onClick={() => console.log('click')} />
}

Dokładnie ten sam problem — każdy render tworzy nową funkcję. Jeśli ExpensiveButton jest zmemoizowany (React.memo), to i tak renderuje się ponownie, bo onClick jest „inny".

Code
// DOBRZE
function Parent() {
  const handleClick = useCallback(() => {
    console.log('click')
  }, [])
 
  return <ExpensiveButton onClick={handleClick} />
}

Przyczyna 3: Context ze zmieniającymi się wartościami

Context propaguje wszystkie zmiany do wszystkich konsumentów, nawet jeśli konsument nie używa zmienionego fragmentu.

Code
// ŹLE
const AppContext = createContext({})
 
function App() {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
 
  const value = { user, setUser, theme, setTheme } // nowy obiekt przy każdym renderze
 
  return (
    <AppContext.Provider value={value}>
      <Header />
      <Main />
      <Footer />
    </AppContext.Provider>
  )
}

Przy każdym renderze App, wartość value to nowy obiekt. To wystarcza, aby React powiadomił wszystkich konsumentów tego Contextu o zmianie, nawet jeśli user ani theme faktycznie się nie zmieniły. Nie oznacza to, że każdy komponent pod Providerem jest konsumentem: komponenty, które nie wywołują useContext(AppContext), mogą uniknąć tej aktualizacji, jeśli nie renderują się ponownie z innych powodów.

Code
// DOBRZE — osobne Contexty dla osobnych domen, memoizacja wartości
const UserContext = createContext(null)
const ThemeContext = createContext('light')
 
function App() {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
 
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Header />
        <Main />
        <Footer />
      </ThemeContext.Provider>
    </UserContext.Provider>
  )
}

Teraz zmiana użytkownika renderuje ponownie tylko konsumentów UserContext, nie ThemeContext.

Dla bardziej skomplikowanych przypadków sprawdź zewnętrzny magazyn stanu z selektorami (np. Zustand) — komponent subskrybuje tylko wycinek stanu, więc renderuje się ponownie dopiero wtedy, gdy ten konkretny wycinek się zmieni. Context tej granulacji nie ma i mieć nie będzie.

Przyczyna 4: nowy obiekt w stanie

Code
// ŹLE
function Toggle() {
  const [state, setState] = useState({ isOn: false })
 
  const handleClick = () => {
    setState({ isOn: !state.isOn })
  }
 
  return <Button onClick={handleClick} />
}

Wygląda niewinnie i w tym prostym przykładzie nawet działa poprawnie. Problem ujawnia się przez mechanizm pomijania aktualizacji: gdy wywołasz setState z wartością identyczną według Object.is, React zwykle pomija aktualizację; funkcja komponentu może zostać dodatkowo wywołana, ale jej wynik zostanie odrzucony. Prymitywy z tego korzystają — setIsOn(false), gdy isOn już jest false, nie powoduje commitu. setState({ isOn: false }) tworzy zaś zawsze nową referencję, więc React widzi zmianę, nawet jeśli wartości w środku są takie same. Drugi skutek: obiekt stanu przekazywany w dół jako props rozbraja React.memo dzieci przy każdej aktualizacji — także tej, która zmieniła zupełnie inne pole obiektu.

Code
// DOBRZE — prymitywy
function Toggle() {
  const [isOn, setIsOn] = useState(false)
 
  const handleClick = () => {
    setIsOn((prev) => !prev)
  }
 
  return <Button onClick={handleClick} />
}

Pamiętaj, jeśli stan to jedna flaga albo jedna wartość, użyj prymitywu, bo React może wtedy bezkosztowo pominąć zbędną aktualizację. Obiekt w stanie ma sens tylko wtedy, gdy wartości są logicznie ze sobą powiązane i zmieniają się razem (np. współrzędne, formularz).

Przyczyna 5: kaskada useEffect

Code
// ŹLE
function Dashboard() {
  const [filters, setFilters] = useState({})
  const [sortedFilters, setSortedFilters] = useState({})
  const [displayFilters, setDisplayFilters] = useState({})
 
  useEffect(() => {
    setSortedFilters(sortFilters(filters))
  }, [filters])
 
  useEffect(() => {
    setDisplayFilters(formatFilters(sortedFilters))
  }, [sortedFilters])
 
  return <FilterList filters={displayFilters} />
}

Trzy ponowne renderowania za każdą zmianę filters. Ten „potok danych przez efekty" to klasyczny antywzorzec: stan pochodny nie potrzebuje własnego useState ani useEffect — wystarczy policzyć go w renderze. Pokrewny problem, czyli pobieranie danych łańcuchami efektów, rozbieram w przewodniku o React Query, SWR i useEffect.

Code
// DOBRZE — transformacja w renderze
function Dashboard() {
  const [filters, setFilters] = useState({})
 
  const displayFilters = formatFilters(sortFilters(filters))
 
  return <FilterList filters={displayFilters} />
}

Jeden render zamiast trzech.

Przyczyna 6: zły key w liście

Code
// ŹLE — key jako indeks
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, i) => (
        <Todo key={i} todo={todo} />
      ))}
    </ul>
  )
}

Gdy dodajesz nowy element na początek listy, indeks każdego istniejącego elementu się przesuwa. React nie odmontowuje wtedy całej listy: zachowuje key={0}, key={1} itd., ale przypisuje istniejące instancje komponentów do innych danych. Stan i fokus mogą więc przejść do niewłaściwego zadania, a dopiero ostatni element zostanie zamontowany od nowa. To błąd tożsamości elementów, nie tylko problem wydajnościowy.

Code
// DOBRZE — stabilny, unikalny key
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <Todo key={todo.id} todo={todo} />
      ))}
    </ul>
  )
}

Przyczyna 7: useMemo z niestabilną zależnością

Code
// ŹLE
function List({ items, locale }) {
  const formatItem = (item) => new Intl.NumberFormat(locale).format(item.price)
 
  const processed = useMemo(() => {
    return items.map((item) => ({ ...item, formatted: formatItem(item) }))
  }, [items, formatItem]) // formatItem zmienia się przy każdym renderze
 
  return <ul>{processed.map(/* ... */)}</ul>
}

Funkcja zadeklarowana w ciele komponentu dostaje nową referencję przy każdym renderze, więc useMemo przelicza wynik za każdym razem. Najprostsza naprawa to nie przekazywać funkcji jako zależności: utwórz formatator bezpośrednio w obliczeniu i zależ od danych, które naprawdę wpływają na wynik.

Code
function List({ items, locale }) {
  const processed = useMemo(() => {
    const formatter = new Intl.NumberFormat(locale)
    return items.map((item) => ({
      ...item,
      formatted: formatter.format(item.price),
    }))
  }, [items, locale])
 
  return <ul>{processed.map(/* ... */)}</ul>
}

Diagnostyka: linter z eslint-plugin-react-hooks ostrzega przed brakującymi zależnościami, ale nie oceni, czy zależność jest stabilna ani czy memoizacja oszczędza zauważalny czas. Sprawdź to w Profilerze.

Naprawa bez memoizacji: kompozycja zamiast memo

Zanim owiniesz cokolwiek w React.memo, sprawdź, czy problemu nie rozwiązuje sama struktura komponentów. Dwie techniki kompozycji eliminują całe klasy ponownych renderowań bez jednej linijki memoizacji.

Technika 1: przenieś stan w dół

Jeśli stan żyje wyżej, niż jest używany, każda jego zmiana renderuje ponownie wszystko po drodze:

Code
// ŹLE — przełącznik sidebara renderuje ponownie też HeavyContent
function Page() {
  const [isOpen, setIsOpen] = useState(false)
 
  return (
    <>
      <Sidebar isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} />
      <HeavyContent />
    </>
  )
}
Code
// DOBRZE — stan zamknięty tam, gdzie jest potrzebny
function Page() {
  return (
    <>
      <SidebarWithToggle /> {/* stan isOpen żyje w środku */}
      <HeavyContent /> {/* nie renderuje się ponownie przy przełączeniu */}
    </>
  )
}

Technika 2: przekaż ciężkie dzieci przez children

Gdy stan musi zostać wysoko (np. motyw, układ), przekaż ciężką zawartość jako children. Element przekazany z zewnątrz zachowuje tę samą referencję między renderami komponentu opakowującego, więc React pomija jego ponowne renderowanie:

Code
// ThemeWrapper renderuje się ponownie przy zmianie motywu,
// ale children przyszły z zewnątrz — React ich nie rusza
function ThemeWrapper({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
 
  return (
    <div className={theme}>
      <button
        onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}
      >
        Przełącz motyw
      </button>
      {children}
    </div>
  )
}
 
// Użycie: <ThemeWrapper><HeavyContent /></ThemeWrapper>

Kiedy sięgnąć po React.memo

Dopiero gdy kompozycja nie wystarcza, w sytuacjach, gdy komponent jest realnie ciężki (duża lista, skomplikowane drzewo), a jego rodzic renderuje się ponownie często z powodów, na które nie masz realnego wpływu. Wtedy React.memo plus stabilne propsy (Przyczyny 1-2) zatrzymają kaskadę. Pamiętaj jednak, że to kosztuje, bo memo porównuje propsy przy każdym renderze rodzica i dla lekkich komponentów bywa droższy niż sam render. Pamiętaj, że memo nie naprawi niestabilnych referencji, bo nie działa zamiast nich, ale z nimi w parze.

Proces debugowania ponownych renderowań krok po kroku

Diagram
Pętla diagnozy: od odtworzenia problemu, przez ustalenie przyczyny w Profilerze, po naprawę u źródła i ponowny pomiar.

Mój typowy przebieg, kiedy zauważam problem z wydajnością:

  1. Odtwórz problem. Czy ponowne renderowania pojawiają się przy montowaniu? Przy kliknięciu? Przy pisaniu w polu? Zidentyfikuj wyzwalacz.

  2. Nagranie w Profilerze. Nagraj interakcję.

  3. Znajdź najcięższy komponent. Na wykresie płomieniowym szerokość bloku odpowiada kosztowi renderu; zacznij od najszerszych bloków w commicie wywołanym badaną interakcją.

  4. Sprawdź, dlaczego się renderuje. Zakładka „Why did this render?" — zmiana propsów? Stanu? Ponowne renderowanie rodzica?

  5. Jeśli to props — sprawdź każdy prop. Który się zmienił? Dlaczego?

  6. Idź w górę drzewa — dlaczego rodzic tworzy nowy prop? Funkcja tworzona w miejscu, obiekt tworzony w miejscu, zmiana Contextu?

  7. Napraw na poziomie źródła. Zmiana na stabilną referencję, memoizacja, podział Contextu.

  8. Zmierz ponownie. Ta sama interakcja, Profiler, sprawdzenie, czy ponowne renderowania zniknęły lub zmalały.

Dla dużych problemów robię to iteracyjnie, czyli naprawiam najcięższy komponent, mierzę, a potem idę do następnego i tak do końca.

Co mierzyć po optymalizacji renderowania?

Sama informacja „mniej renderów" jest za słaba. Po refaktorze zapisuję krótki wynik, który da się porównać za miesiąc:

MetrykaPo co ją mierzyćDobry sygnał
Czas commitu w ProfilerzePokazuje koszt faktycznego commitu ReactaSpadek dla konkretnej interakcji
Liczba renderów komponentuPomaga znaleźć kaskady i niestabilne propsySpadek po stabilizacji referencji
INP / responsywność polaPokazuje, czy użytkownik realnie czuje zmianęKrótsze opóźnienie po kliknięciu lub wpisaniu znaku
Spowolnienie CPU 4xSymuluje słabszy laptop lub telefonInterakcja nadal jest płynna

Jeśli Twoim głównym problemem jest responsywność interakcji, zejdź poziom niżej — w artykule o 5 wzorcach, które niszczą Interaction to Next Paint w React rozbieram INP od strony konkretnych wzorców kodu.

React 19: useTransition i useDeferredValue

Te mechanizmy są z nami od Reacta 18, ale w React 19 nadal pozostają podstawowym sposobem oddzielania pilnych interakcji od cięższych aktualizacji interfejsu.

useTransition — „ta zmiana nie jest pilna, React może ją przerwać":

Code
import { useTransition, useState } from 'react'
 
function SearchPage() {
  const [query, setQuery] = useState('')
  const [searchQuery, setSearchQuery] = useState('')
  const [isPending, startTransition] = useTransition()
 
  const handleSearch = (newQuery: string) => {
    setQuery(newQuery) // pilne — pole reaguje natychmiast
 
    startTransition(() => {
      setSearchQuery(newQuery) // niepilne — render wyników może zostać przerwany
    })
  }
 
  return (
    <>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <Spinner />}
      {/* ciężkie filtrowanie dzieje się W RENDERZE ResultList (np. w useMemo) */}
      <ResultList query={searchQuery} />
    </>
  )
}

React może przerwać renderowanie ResultList, jeśli użytkownik nadal pisze. Wtedy pole pozostaje responsywne, nawet gdy wyniki są ciężkie do policzenia. Warto pamiętać, że startTransition wpływa wyłącznie na priorytet renderu będącego skutkiem zmiany stanu. Sam kod wewnątrz przekazanego callbacku nadal wykonuje się synchronicznie i nie da się go przerwać. Gdybyś policzył wyniki synchronicznie w obsłudze zdarzenia (setResults(searchExpensive(query))), ciężka praca i tak zablokowałaby wątek, ponieważ przerwać da się tylko to, co dzieje się podczas renderowania.

useDeferredValue, czyli renderuj ze starszą wartością, ale przygotuj nową:

Code
import { useDeferredValue, useState } from 'react'
 
function SearchPage() {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
 
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ResultList query={deferredQuery} /> {/* renderuje się z opóźnieniem */}
    </>
  )
}

Te mechanizmy nie naprawiają niepotrzebnych renderów, ale pomagają zarządzać priorytetami, gdy render jest realnie ciężki.

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

Często zadawane pytania

Ile renderów to za dużo?

Nie ma konkretnej liczby, która stanowi granicę. Strona marketingowa z 50 komponentami może spokojnie zrobić 500 renderów przy pierwszym załadowaniu bez odczuwalnego problemu. Z kolei edytor tekstu, w którym każde wciśnięcie klawisza wyzwala 50 renderów, to już realny kłopot. Mierz czas w milisekundach i wpływ na responsywność interakcji, a nie samą liczbę renderów.

Czy `React.memo` zawsze ma sens?

Nie. React.memo dodaje narzut (porównywanie propsów). Dla prostych komponentów prezentacyjnych może być wolniejszy niż brak memoizacji. Używaj dla komponentów, które są ciężkie do renderowania (duże listy, skomplikowane drzewa, kosztowne obliczenia).

Czy Profiler działa na produkcji?

DevTools może profilować aplikację produkcyjną, ale pomiary są wtedy mniej wygodne do analizy niż w środowisku lokalnym, a dodatkowe instrumentowanie ma koszt. Najpierw odtwórz problem lokalnie na danych zbliżonych do produkcyjnych, potem potwierdź wynik w produkcji metrykami użytkowników, np. INP. Specjalny build react-dom/profiling jest potrzebny tylko w wybranych konfiguracjach zaawansowanego profilowania.

Jak sprawdzić, czy moje `useMemo` pomaga?

Użyj console.log w memoizowanej funkcji — jeśli loguje się rzadziej niż render, memoizacja działa. Alternatywnie — @welldone-software/why-did-you-render pokaże, czy referencja się zmienia.

Dlaczego mój komponent renderuje się 3 razy przy montowaniu?

Strict Mode w środowisku programistycznym wykonuje render dwukrotnie, żeby wyłapać efekty uboczne w funkcji renderującej — to daje 2 rendery przy montowaniu. Trzeci render zwykle oznacza aktualizację stanu tuż po zamontowaniu, np. useEffect ustawiający dane albo odczyt z localStorage. W produkcji Strict Mode nie podwaja renderów, ale ta aktualizacja stanu zostaje — warto sprawdzić, czy jest konieczna.

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 SEO

Czytaj dalej

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

INP ostatecznie odesłał FID na emeryturę, wymuszając zupełnie nowe spojrzenie na responsywność. Metryka ocenia teraz najgorszą interakcję z całej sesji, bez taryfy ulgowej dla aplikacji React wypełnionych bogatym stanem, potężnymi listami i rozbudowanymi zdarzeniami. Jest jednak kilka powtarzalnych wzorców, które radzą sobie z większością problemów z optymalizacją, z których każdy posiada gotowe rozwiązanie systemowe.

Maciej Sala

Maciej Sala

Founder StriveLab

React Compiler w 2026 — czy useMemo i useCallback są już martwe?

React Compiler odsyła do lamusa jedną z najczęstszych irytacji codziennej pracy z Reactem — ręczne owijanie funkcji i wartości w useMemo oraz useCallback . W dużych aplikacjach dbanie o memoizację pochłaniało godziny, a w małych projektach często dokładano ją na siłę, w złudnej pogoni za „wydajnością". Od kiedy Compiler jest stabilny, ten rytuał w większości przypadków przestał być potrzebny — ale nie zniknął całkowicie i warto wiedzieć, kiedy nadal ma znaczenie.

Maciej Sala

Maciej Sala

Founder StriveLab

TypeScript w React bez bólu: 7 wzorców do produkcyjnych komponentów

W tym artykule pokazuję siedem wzorców, które sam stosuję produkcyjnie — w StriveLab https://strivelab.pl/tworzenie-stron-internetowych/react/ , Army Builder https://army-builder.com , i u klientów. Każdy z nich rozwiązuje konkretny problem, który w 2026 roku masz rozwiązywać lepiej niż pięć lat temu, bo TypeScript i React dostały nowe narzędzia.

Maciej Sala

Maciej Sala

Founder StriveLab