React Compiler analizuje komponenty podczas buildu i dodaje optymalizacje memoizacji bez ręcznego useMemo lub useCallback w większości przypadków. 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 ta zmiana
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', 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:
A tak — w uproszczeniu — Compiler przekształca go podczas buildu:
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 odeszło na emeryturę?
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:
-
Integracje z systemami spoza Reacta. Gdy podłączasz
addEventListener,IntersectionObserveralbo zewnętrzne SDK, te API wymagają stabilnej referencji funkcji, której Compiler nie zawsze zagwarantuje. Tutaj świadomie sięgasz pouseCallback, żeby cleanup działał poprawnie. -
Zależności
useEffectoparte 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. -
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.
-
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 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 — realna oszczędność milisekund, które przekładają się na płynność i konwersję.
Jak wdrożyć Compiler
Compiler instaluje się jako plugin Babela, niezależnie od bundlera (Vite, Next.js):
Vite + React
Next.js
W Next.js 15 funkcja jest jeszcze za flagą eksperymentalną:
W Next.js 16 straciła status eksperymentalny i przeniosła się na główny poziom konfiguracji:
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
W istniejącej aplikacji nie włączaj Compilera globalnie jednym przełącznikiem. Potraktuj to jak wdrożenie nowej funkcji — etapami.
Krok 1: Zabezpiecz się linterem
Linter wskaże naruszenia Zasad Reacta — mutacje propsów, warunkowe hooki, 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 (annotation)
Na początek włącz Compiler tylko tam, gdzie świadomie go oznaczysz dyrektywą 'use memo':
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:
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
- 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 globalnyinferzostaw na koniec. - Przetestuj krytyczne ścieżki E2E — logowanie, płatności, koszyk, formularze — przed wypuszczeniem na produkcję.
- Nie usuwaj starego
useMemomasowo 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:
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 stoi Compiler
Compiler robi swoje, dopóki Twój kod trzyma się Zasad Reacta (Rules of React). To nie kaprys — to warunek, bez którego automatyczna memoizacja byłaby niebezpieczna. Trzy najczęstsze naruszenia:
1. Nie mutuj stanu ani propsów.
2. Nie wykonuj efektów ubocznych w renderze.
3. Nie wywołuj hooków warunkowo.
eslint-plugin-react-hooks wykrywa praktycznie wszystkie te naruszenia automatycznie — dlatego linter to pierwszy krok każdej migracji.
Czego 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ę — i 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 bundle. Compiler nie zmniejszy wagi paczki — od tego jest Tree shaking to usuwanie nieużywanego kodu podczas budowania — bundler zostawia tylko te funkcje, które faktycznie importujesz. Działa najlepiej z importami nazwanymi z modułów ESM; zawodzi przy imporcie całego pakietu albo przy bibliotekach w formacie CommonJS. 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.
Compiler przyspiesza Reconciliation to proces Reacta porównujący poprzednie i nowe drzewo komponentów, aby wyznaczyć minimalne zmiany w UI. — 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 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.
