Przejdź do treści

React Compiler a manualna optymalizacja wydajności w kodzie

Poznaj realne możliwości nowej wersji kompilatora Reacta. Sprawdź kiedy usunięcie starych hooków optymalizacyjnych jest całkowicie bezpieczne dla Twojej aplikacji.

Maciej Sala

Founder StriveLab

9 min czytaniaOpublikowano 24 kwietnia 2026 (Aktualizacja 31 maja 2026)

osiągnął stabilną wersję 1.0 w październiku 2025 roku, po okresie testów na produkcyjnych aplikacjach Meta. To moment, w którym dyskusja „czy warto" zamieniła się w „jak wdrożyć". W tym artykule wyjaśniam, na czym polega ta zmiana, dlaczego część dawnych chwytów memoizacyjnych wciąż warto trzymać pod ręką i jak bezpiecznie włączyć Compiler w działającym projekcie.

Krótki skrót praktyczny: w zdecydowanej większości codziennych przypadków useMemo jest dziś zbędne. W nowym projekcie na Next.js po prostu włączasz reactCompiler: true i piszesz czysty kod. W starszym, działającym serwisie idziesz ostrożniej. Najpierw linter wyłapuje złe nawyki, potem tryb annotation pozwala na próby na wybranych komponentach, a na końcu globalny infer z codemodem sprzątającym po drodze.

Na czym polega React Compiler?

Compiler działa na etapie buildu. Analizuje Twoje komponenty i automatycznie wstawia memoizację wszędzie tam, gdzie skróci to czas renderowania. Ty piszesz zwykły, czysty kod, bez żadnej dyrektywy w stylu 'use compiled' i bez ręcznego oznaczania. Standardowa architektura React zostaje, a narzędzie podczas kompilacji robi resztę.

Najlepiej widać to na przykładzie. Tak wygląda Twój kod:

Code
// Piszesz zwykły, czysty komponent
function Component({ items, onClick }) {
  const sorted = [...items].sort((a, b) => a.price - b.price)
  const handler = () => onClick(sorted[0])
 
  return <Button onClick={handler}>Najtańsza opcja: {sorted[0].name}</Button>
}

A tak, w uproszczeniu, Compiler przekształca go podczas buildu:

Code
// Compiler generuje z tego (poglądowy zarys mechanizmu):
function Component({ items, onClick }) {
  const cache = useMemoCache()
 
  let sorted
  // Compiler sam sprawdza, co się zmieniło, i przelicza tylko wtedy, gdy trzeba
  if (items !== cache[0]) {
    sorted = [...items].sort((a, b) => a.price - b.price)
    cache[0] = items
    cache[1] = sorted
  } else {
    sorted = cache[1]
  }
 
  let handler
  if (sorted !== cache[2] || onClick !== cache[3]) {
    handler = () => onClick(sorted[0])
    cache[2] = sorted
    cache[3] = onClick
    cache[4] = handler
  } else {
    handler = cache[4]
  }
 
  // ...render
}

Cała praca, którą wcześniej wykonywałeś ręcznie przez useMemo i useCallback, dzieje się teraz automatycznie przy każdej zależnej wartości, bez dopisywania ani jednej dyrektywy.

Czy useMemo jest nadal potrzebne przy React Compiler?

W praktyce: w zdecydowanej większości przypadków już go nie dodajesz. Compiler przejmuje memoizację, którą wcześniej trzeba było pisać z palca:

  • Nie owijasz już przeliczeń w renderze (const total = useMemo(() => items.reduce(...), [items])).
  • Nie stabilizujesz ręcznie funkcji przekazywanych w głąb drzewa (const onClick = useCallback(() => {...}, [])).
  • Nie blokujesz zbędnych renderów komponentów przez React.memo().
  • Nie martwisz się, że przekazanie nowej referencji do komponentu z zewnętrznej biblioteki wymusi jego ponowny render.

W projektach, które przepisywałem na Compiler, usunięcie tej warstwy ręcznej memoizacji nie pogorszyło niczego od strony użytkownika. Kod stał się czytelniejszy, a zachowanie pozostało identyczne.

Są jednak sytuacje, w których Compiler potrzebuje pomocy, bo nie ma pełnego wglądu w to, co robisz:

  1. Integracje z systemami spoza Reacta. Gdy podłączasz addEventListener, IntersectionObserver albo zewnętrzne SDK, te wymagają stabilnej referencji funkcji, której Compiler nie zawsze zagwarantuje. Tutaj świadomie sięgasz po useCallback, żeby cleanup działał poprawnie.

  2. Zależności useEffect oparte na zewnętrznych danych. Jeśli efekt zależy od wartości pochodzącej spoza Reacta, sam zadbaj o stabilność referencji w tablicy zależności, żeby nie odpalał się częściej, niż powinien.

  3. Własne hooki ze złożoną logiką. Compiler radzi sobie z większością przypadków, ale przy nietypowych własnych hookach warto zweryfikować zachowanie testami, zanim mu w pełni zaufasz.

  4. Celowe ponowne renderowanie. Jeśli komponent ma się odświeżać świadomie (np. zegar tykający co sekundę), oznacz go dyrektywą 'use no memo' na początku funkcji. Compiler go wtedy pominie.

Gdzie React Compiler realnie daje zysk?

Postawmy sprawę uczciwie: Compiler nie wskrzesi zapuszczonej aplikacji. Przy małym, lekkim projekcie różnica w wydajności jest kosmetyczna i niewidoczna w pomiarach. Realny zysk pojawia się tam, gdzie jest dużo komponentów, częste aktualizacje stanu i złożone drzewa renderowania.

Z moich wdrożeń, m.in. dla StriveLab oraz aplikacji Army Builder (rozbudowany konfigurator operujący na setkach modeli i tysiącach rekordów):

  • Proste wizytówki na kilkanaście komponentów. Różnica praktycznie niemierzalna. Strona działała dobrze i bez Compilera; zysk to przede wszystkim wygoda pisania, nie wydajność.
  • Dashboardy z wyszukiwarką i listami na setki pozycji. Tutaj widać już realne odciążenie renderowania, na poziomie porównywalnym z ręczną, staranną memoizacją.
  • Ciężkie konfiguratory z głęboko zagnieżdżonymi strukturami (jak edycja modeli w Army Builderze). Zauważalny spadek czasu blokowania głównego wątku przy interakcjach, przy zerowym nakładzie pracy nad ręczną memoizacją.

Wniosek jest prosty: dla projektów marketingowych Compiler to przede wszystkim komfort i mniej kodu do utrzymania. Dla aplikacji z dużym ruchem i intensywnymi interakcjami oznacza realną oszczędność milisekund, które przekładają się na płynność i konwersję.

Jak wdrożyć React Compiler?

Compiler instaluje się jako plugin Babela, niezależnie od bundlera (Vite, Next.js):

Code
npm install --save-dev --save-exact babel-plugin-react-compiler@latest

React Compiler w Vite i React

Code
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
 
const ReactCompilerConfig = {
  target: '19', // dostosuj do wersji React w projekcie
}
 
export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]],
      },
    }),
  ],
})

React Compiler w Next.js

W Next.js 15 funkcja jest jeszcze za flagą eksperymentalną:

Code
// next.config.js (Next.js 15)
module.exports = {
  experimental: {
    reactCompiler: true,
  },
}

W Next.js 16 straciła status eksperymentalny i przeniosła się na główny poziom konfiguracji:

Code
// next.config.js (Next.js 16)
module.exports = {
  reactCompiler: true,
}

W żadnej wersji nie jest włączona domyślnie. Musisz to zrobić świadomie, bo Compiler wydłuża czas builda (korzysta z Babela). Zanim ją włączysz, przepuść kod przez linter, żeby wyłapać naruszenia Zasad Reacta.

Bezpieczna migracja starszego projektu do React Compiler

W istniejącej aplikacji nie włączaj Compilera globalnie jednym przełącznikiem. Potraktuj to jak wdrożenie nowej funkcji i rób je etapami.

Krok 1: zabezpieczenie projektu linterem

Code
npm install --save-dev eslint-plugin-react-hooks@latest
Code
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks'
 
export default [reactHooks.configs.flat.recommended]

Linter wskaże naruszenia Zasad Reacta, takie jak mutacje propsów, warunkowe hooki i efekty uboczne w renderze, zanim w ogóle dotkniesz Compilera. To miejsca, które i tak musisz naprawić, bo Compiler je pominie.

Krok 2: tryb opt-in przez annotation

Na początek włącz Compiler tylko tam, gdzie świadomie go oznaczysz dyrektywą 'use memo':

Code
const ReactCompilerConfig = {
  compilationMode: 'annotation',
}
Code
function Dashboard() {
  'use memo'
 
  // Tylko ten komponent zostanie zoptymalizowany przez Compiler
}

Dzięki temu testujesz nową optymalizację na pojedynczych, dobrze przetestowanych komponentach, zamiast od razu na całym repozytorium.

Krok 3: tryb globalny infer

Gdy najważniejsze komponenty (koszyk, panele użytkownika, checkout) przeszły testy, włącz Compiler globalnie:

Code
const ReactCompilerConfig = {
  compilationMode: 'infer', // tryb domyślny
}

Od tej chwili każdy poprawny komponent jest optymalizowany automatycznie. Komponent, który łamie Zasady Reacta, zostanie pominięty, a w konsoli pojawi się stosowne ostrzeżenie.

Zasady bezpiecznego wdrożenia React Compiler

  • Przypinaj wersję pluginu Babela na sztywno (--save-exact), nie polegaj na „latest" na produkcji.
  • Najpierw posprzątaj kod linterem, dopiero potem włączaj Compiler.
  • Migruj starsze projekty przez annotation, a globalny infer zostaw na koniec.
  • Przetestuj krytyczne ścieżki E2E przed wypuszczeniem na produkcję, szczególnie logowanie, płatności, koszyk i formularze.
  • Nie usuwaj starego useMemo masowo z automatu. Usuwaj stopniowo, sprawdzając pomiary, i uważaj na stabilne referencje potrzebne dla zewnętrznych SDK.

Krok 4: sprzątanie ręcznej memoizacji

Na końcu, gdy Compiler działa stabilnie, możesz usunąć zbędną ręczną memoizację codemodem:

Code
npx codemod react/19/remove-memoization

W moim wdrożeniu dla Army Buildera codemod usunął bezpiecznie około 350 z 400 ręcznych wywołań useMemo/useCallback, które istniały wyłącznie po to, by ratować render. Pozostałe ~20, związane z IntersectionObserver i requestIdleCallback, zostawiłem ręcznie, bo to właśnie te przypadki brzegowe, gdzie stabilna referencja jest niezbędna.

Zasady Reacta, na których opiera się Compiler

Compiler robi swoje, dopóki Twój kod trzyma się Zasad Reacta (Rules of React). To nie kaprys, tylko warunek, bez którego automatyczna memoizacja byłaby niebezpieczna. Trzy najczęstsze naruszenia:

1. Nie mutuj stanu ani propsów.

Code
// Źle, mutacja tablicy przekazanej w propsach
function List({ items }) {
  items.sort((a, b) => a.price - b.price); // mutuje oryginał!
  return <ul>{items.map(...)}</ul>;
}
 
// Dobrze, pracuj na kopii
function List({ items }) {
  const sorted = [...items].sort((a, b) => a.price - b.price);
  return <ul>{sorted.map(...)}</ul>;
}

2. Nie wykonuj efektów ubocznych w renderze.

Code
// Źle, efekt uboczny w funkcji renderującej
function Component() {
  document.title = 'Nowy tytuł' // mutacja poza renderem
  return <div />
}
 
// Dobrze, efekt w useEffect
function Component() {
  useEffect(() => {
    document.title = 'Nowy tytuł'
  }, [])
  return <div />
}

3. Nie wywołuj hooków warunkowo.

Code
// Źle, hook pod warunkiem
function Component({ shouldFetch }) {
  if (shouldFetch) {
    const data = useFetch('...')
  }
}
 
// Dobrze, warunek wewnątrz hooka
function Component({ shouldFetch }) {
  const data = useFetch(shouldFetch ? '...' : null)
}

eslint-plugin-react-hooks wykrywa praktycznie wszystkie te naruszenia automatycznie. Dlatego linter to pierwszy krok każdej migracji.

Czego React Compiler nie naprawi?

Łatwo wpaść w pułapkę myślenia, że Compiler to lekarstwo na każdy problem z wydajnością. Nie jest. Optymalizuje render, a nie architekturę, więc są klasy problemów, których nie tknie:

  • Złe algorytmy (np. O(n²)). Jeśli komponent wykonuje kosztowną pętlę w pętli, Compiler zapamięta wynik, ale nie poprawi samej złożoności obliczeniowej.
  • Długie listy bez wirtualizacji. Renderowanie 10 tysięcy elementów DOM naraz pozostanie wolne. To zadanie dla wirtualizacji list z TanStack Virtual, nie dla Compilera.
  • Zbyt duży . Compiler nie zmniejszy wagi paczki. Od tego jest i analiza bundla.
  • Wodospady zapytań i problem N+1. Kaskadowe, nieefektywne pobieranie danych naprawia się warstwą cache i odpowiednim fetchingiem, np. TanStack Query, nie kompilatorem. Jeśli właśnie skończyłeś porządkować memoizację z pomocą Compilera, naturalnym kolejnym krokiem jest weryfikacja warstwy fetchingu. Szczegółowe porównanie TanStack Query, SWR i useEffect opisuję w osobnym artykule.

Compiler przyspiesza , czyli proces dopasowania renderu, a nie leczy źle zaprojektowanej logiki aplikacji. To ważne narzędzie w skrzynce inżyniera, ale nie srebrna kula na złą architekturę.

Gdzie React Compiler bywa kłopotliwy?

Choć w większości projektów działa bezproblemowo, są sytuacje, w których trzeba uważać:

1. Zapuszczony, stary kod łamiący Zasady Reacta. Tam, gdzie kod opiera się na mutacjach propsów i niestabilnych referencjach, Compiler wycofuje się dla bezpieczeństwa i nie przyspiesza nic. Najpierw posprzątaj kod (linter pokaże gdzie), potem myśl o optymalizacji.

2. Założenia o leniwej ewaluacji. Compiler zmienia moment przeliczania wartości. Jeśli Twój kod opierał się na tym, że ciężkie obliczenie wykona się leniwie dopiero pod warunkiem if, zweryfikuj to zachowanie po włączeniu memoizacji.

3. Starsze wersje TypeScriptu. Typowanie generowane przez Compiler może sprawiać problemy na TS sprzed 5.4. Zaktualizuj TypeScript do 5.4 lub nowszego, a problemy z typami znikają.

4. Integracje przez HOC ze starszymi bibliotekami. Wzorce oparte na HOC (np. starszy Redux czy MobX) bywają trudniejsze dla Compilera. Tam, gdzie to możliwe, przejdź na nowocześniejsze, oparte na hookach API tych bibliotek.

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

Często zadawane pytania

Czy useMemo i useCallback odeszły w 2026 roku do lamusa?

W większości codziennych scenariuszy tak. React Compiler automatyzuje memoizację bez żadnych zmian w Twoim kodzie. Ręczne useMemo i useCallback zostawiasz do zadań specjalnych: gdy musisz zachować stabilną referencję dla zewnętrznych narzędzi (event listenery, IntersectionObserver, integracje z SDK), gdy zabezpieczasz zależności useEffect albo gdy obsługujesz naprawdę kosztowne obliczenia. Zespół React rekomenduje, by w nowym kodzie polegać na Compilerze, a ręcznej memoizacji używać tam, gdzie potrzebujesz precyzyjnej kontroli.

Czy React Compiler zadziała na React 18?

Tak, ale z zastrzeżeniem. Compiler oficjalnie wspiera React od wersji 17 w górę, jednak pełnię możliwości wykorzystasz dopiero na React 19+. Dla starszych wersji (17/18) musisz ustawić w konfiguracji parametr target, np. target: '18'. Część nowszych optymalizacji będzie wtedy wyłączona, ale rdzeń działania pozostaje.

Czy włączenie Compilera może zepsuć mój istniejący projekt?

Jeśli Twój kod trzyma się Zasad Reacta (Rules of React), nie. Jeśli je łamałeś (mutowałeś propsy, odpalałeś efekty uboczne w funkcji renderującej, wywoływałeś hooki warunkowo), Compiler po prostu pominie taki komponent i zostawi ostrzeżenie, zamiast zoptymalizować go błędnie. To celowe zabezpieczenie: narzędzie nie tknie kodu, który mógłby się przez to rozpaść. Linter eslint-plugin-react-hooks wskaże takie naruszenia, zanim w ogóle włączysz Compiler.

Czy po włączeniu muszę od razu usunąć wszystkie stare useMemo?

Nie ma pośpiechu. Compiler analizuje Twój kod i nakłada własną warstwę memoizacji niezależnie od istniejącej. Zespół React zaleca, by przy migracji zostawić starą memoizację na miejscu, aż upewnisz się, że wszystko działa poprawnie (pomaga React DevTools). Z czasem możesz uruchomić codemod (npx codemod react/19/remove-memoization), ale uważnie, automat potrafi usunąć stabilne referencje potrzebne dla zewnętrznych narzędzi, więc nie traktuj go jako bezpiecznego bez testów.

Czy Compiler zastępuje React.memo?

W większości przypadków tak. React.memo robi płytkie porównanie propsów, żeby uniknąć zbędnego renderu i dokładnie to Compiler robi teraz automatycznie dla każdego komponentu. Ręcznie sięgniesz po React.memo właściwie tylko wtedy, gdy potrzebujesz własnej funkcji porównującej areEqual, bo Compiler opiera się na standardowym porównaniu Object.is.

Jak Compiler radzi sobie z Server Components?

Dobrze na obu warstwach. W Next.js z App Routerem Compiler rozpoznaje, czy komponent jest serwerowy, czy kliencki, i dostosowuje optymalizacje. Server Components z natury (brak stanu, brak własnych hooków poza use) wymagają mniej pracy memoizacyjnej niż komponenty klienckie, ale tam, gdzie jest co optymalizować, Compiler to robi.

Jak włączyć Compiler w projekcie Next.js (15 / 16)?

W Next.js 15 dodajesz do next.config.js wpis experimental: { reactCompiler: true } i instalujesz plugin Babela. W Next.js 16 funkcja straciła status eksperymentalny. Ustawiasz reactCompiler: true na głównym poziomie konfiguracji. W obu wersjach nie jest włączona domyślnie, bo wydłuża czas builda, więc musisz ją włączyć świadomie. Przed włączeniem przepuść kod przez linter, by wyłapać naruszenia Zasad Reacta.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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 rozwija 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
Next.js 15 nowości i decyzja o migracji z wersji 14

Next.js 15 zacieśnia współpracę z najnowszym React 19, stabilizuje Turbopacka w środowisku deweloperskim i wprowadza eksperymentalne Partial Prerendering . Z perspektywy czasu to jedno z największych tąpnięć od debiutu App Routera w „trzynastce" i fundament, na którym Next.js 16 dopiął model Cache Components oraz domyślny Turbopack. W tym artykule rozkładam najważniejsze zmiany na czynniki pierwsze i podpowiadam, kiedy migracja z czternastki realnie się opłaca.

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

Obsługa formularzy przez React 19 Actions i useActionState

React 19 pozwala przekazać funkcję bezpośrednio do atrybutu action elementu . Dzięki integracji z Actions, useActionState , useFormStatus i useOptimistic możesz ograniczyć ręczną obsługę onSubmit , pending state oraz optymistycznych aktualizacji , zachowując kontrolę nad błędami i stanem trwałym.

Maciej Sala

Maciej Sala

Founder StriveLab