React compiler to nie magia. Dlaczego zła architektura nadal zabija wydajność w React 19 i Next 16.
Włączenie flagi reactCompiler w Next.js 16 nie rozwiązuje automatycznie wszystkich problemów z wydajnością. React 19 wprowadza kompilator automatyzujący proces zapamiętywania, co w teorii eliminuje potrzebę ręcznego stosowania hooków takich jak useMemo czy useCallback. W praktyce narzędzie to optymalizuje wykonanie kodu, ale nie naprawia błędów w architekturze, szczególnie w komponentach klienckich.
Przykładem jest dashboard z tabelą transakcji i wykresami, gdzie najechanie kursorem na wiersz lub wpisanie tekstu w pole wyszukiwania powoduje zablokowanie interfejsu.
Widzisz te fioletowe obramowania? To React Scan pokazuje które komponenty się re-renderują. Przy każdym hover - cała tabela i wykres. Przy każdym keystroke w input - to samo.
Narzędzia do diagnozowania wydajności
Analiza problemów wydajnościowych wymaga odpowiednich narzędzi przed przystąpieniem do modyfikacji kodu. Profiler wbudowany w React DevTools pozwala na pomiar czasu renderowania komponentów i identyfikację przyczyn ich odświeżania, na przykład zmiany konkretnego hooka.
Z kolei
Nie zapominajmy o narzędziu które ma każda przeglądarka i powinno być naszym pierwszym wyborem, czyli Dev Tools, tutaj możemy uzyskać wiele przydatnych informacji z konsoli czy wielu innych wartościowych zakładek. Przy analizie obecnego problemu posłużyłem się zakładką Performance i nagraniu interakcji, aby zdiagnozować problem z metrykami.
Twarde dane: INP 2662ms
Spójrz na metryki. INP (Interaction to Next Paint) mierzy jak szybko aplikacja reaguje na interakcje użytkownika.
INP: 2662ms - ponad 2.5 sekundy! Google uznaje wszystko powyżej 500ms za “poor”. Mamy 5x więcej.
Co się dzieje? DevTools pokazuje:
- Scripting: 8,455ms - JavaScript blokuje main thread
- Przy każdym hover wykres wykonuje 5 milionów operacji
- I robi to przy każdym ruchu myszy
Dlaczego React “laguje”?
Zastanówmy się najpierw co może sprawić, że React może być miejscami niewydajny i powodować “lagi” UI w naszej aplikacji.
React odświeża komponent w kilku konkretnych sytuacjach:
- Zmienia się jego state
- Zmienia się props
- Re-renderuje się parent
- Zmienia się Context którego używa
Mechanika Context API powoduje najwięcej problemów w rozbudowanych interfejsach. Zmiana pojedynczej wartości wymusza re-render wszystkich komponentów subskrybujących dany kontekst, niezależnie od tego, z ilu pól faktycznie korzystają. React Compiler nie zapobiega renderowaniu w tym przypadku.
Anatomia problemu
Spójrzmy na kod. Oto jak wygląda “zła” architektura:
// Globalny Context przechowuje stan hovera
const GlobalContext = createContext<{
hoveredRowId: string | null;
setHoveredRowId: (id: string | null) => void;
}>();
// Wykres SUBSKRYBUJE Context
const HeavyChart = ({ data }) => {
const { hoveredRowId } = useGlobalContext(); // Problem!
// Ciężka kalkulacja przy KAŻDYM renderze
for (let i = 0; i < 5000000; i++) {
// ...miliony operacji
}
return <BarChart data={data} />;
};
// Wiersz też subskrybuje Context
const TableRow = ({ transaction }) => {
const { hoveredRowId, setHoveredRowId } = useGlobalContext();
return (
<tr onMouseEnter={() => setHoveredRowId(transaction.id)}>{/* ... */}</tr>
);
};
Co się dzieje przy hover?
Funkcja setHoveredRowId modyfikuje wartość w globalnym kontekście. Każdy komponent używający useGlobalContext przechodzi proces re-renderowania, włączając w to wykres, co w rezultacie składa się na tak wysokoą wartość metryki INP wynoszącą ponad 2.5 sekundy
Rozwiązanie oparte na architekturze
Kompilator wymaga odpowiednio ustrukturyzowanego kodu do poprawnego działania. Optymalizacja polega na wprowadzeniu zmian w umiejscowieniu stanu i przepływie danych.
Pierwszym krokiem jest przeniesienie stanu najbliżej miejsca jego wykorzystania (State Colocation). Stan najechania kursorem dotyczy wyłącznie pojedynczego wiersza i nie musi być dostępny globalnie.
Zmiana 1: Hover state lokalnie
Czy cała aplikacja musi wiedzieć który wiersz jest hover? Nie. Tylko ten jeden wiersz.
// PRZED: hover w globalnym Context
const TableRow = ({ transaction }) => {
const { hoveredRowId, setHoveredRowId } = useGlobalContext();
const isHovered = hoveredRowId === transaction.id;
// ...
};
// PO: hover lokalnie w wierszu
const TableRow = ({ transaction }) => {
const [isHovered, setIsHovered] = useState(false);
// ...
};
Teraz hover zmienia state tylko jednego wiersza. Wykres nie wie i nie obchodzi go że coś się zmieniło.
Zmiana 2: Wykres z niezależnymi danymi
Wykres pokazuje agregaty wszystkich transakcji. Czy musi reagować na filtrowanie tabeli? Nie.
// PRZED: wykres reaguje na każdą zmianę filtra
<HeavyChart data={filteredData} />
// PO: wykres zawsze pokazuje pełny obraz
<HeavyChart data={INITIAL_DATA} />
Teraz wykres nie re-renderuje się gdy wpisujesz w input.
Zmiana 3: React Compiler
Gdy parent się re-renderuje, domyślnie wszystkie dzieci też. Compiler automatycznie memoizuje komponenty - widzi że props wykresu (INITIAL_DATA) się nie zmieniły i pomija re-render.
// next.config.ts
const nextConfig: NextConfig = {
reactCompiler: true,
};
Porównanie wyników i spadek INP
Po lewej zła architektura, po prawej dobra. Ten sam dashboard, te same dane, ta sama “ciężka” kalkulacja w wykresie. Różnica? Architektura kodu.
| Zła architektura | Dobra architektura | |
|---|---|---|
| Hover | Lag (cała tabela + chart) | Płynnie (1 wiersz) |
| Input | Lag (chart re-render) | Płynnie (tylko tabela) |
| Kod | useGlobalContext() wszędzie | Lokalny useState + rozdzielone dane |
Rezultat: INP 218ms
Po zmianach architektonicznych i włączeniu Compilera:
INP: 218ms - ponad 12x szybciej niż wcześniej!
| Metryka | Przed | Po | Poprawa |
|---|---|---|---|
| INP | 2662ms | 218ms | 12x |
| Scripting | 8,455ms | 3,700ms | 2.3x |
React Scan (fioletowe obramowania) pokazuje że teraz przy hover re-renderuje się tylko jeden wiersz, a wykres stoi spokojnie.
Co zrobiła każda zmiana?
| Zmiana | Co naprawia | Dlaczego Compiler sam nie wystarczy |
|---|---|---|
| Hover lokalnie | Re-render 1 wiersza zamiast 250 | Compiler nie obejdzie Context - gdy Context się zmienia, MUSI re-renderować |
| Chart z INITIAL_DATA | Chart nie reaguje na filtr | Compiler memoizuje, ale gdy props się zmieniają, musi re-renderować |
| React Compiler | Automatyczna memoizacja | Działa tylko gdy architektura mu na to pozwala |
Kluczowy wniosek: Compiler optymalizuje dobrze napisany kod. Nie naprawia złych decyzji architektonicznych.
Kiedy używać Contextu, a kiedy lepiej nie?
Context sam w sobie nie jest zły, potrafi być bardzo przydatny i często jest nawet konieczny w klasycznych aplikacjach reactowych aby zapewnić dostęp do danych w całej hierarchii komponentów. Warto jednak zapamiętać jedną zasadę: Context może powodować poważne problemy z wydajnością, gdy używasz go do często zmieniającego się stanu.
Context świetnia sprawdzi się dla:
- Theme (light/dark)
- Auth (user data)
- Locale (język)
Context nie będzie dobrym wyborem dla:
- Hover state
- Selected items
- Form inputs
- Scroll position
Jeśli potrzebujesz globalnego stanu który zmienia się często, rozważ Zustand (selektory) lub Jotai (atomy).
Podsumowanie
-
Architektura > flagi w konfiguracji - żaden Compiler nie naprawi złych decyzji. State w Context = globalne re-rendery przy każdej zmianie.
-
State colocation - trzymaj stan jak najbliżej miejsca użycia. Hover wiersza? W wierszu, nie w globalnym Context.
-
Rozdzielaj niezależne dane - jeśli wykres nie musi reagować na filtr tabeli, nie dawaj mu filtrowanych danych.
-
React Compiler pomaga - ale przy dobrze napisanym kodzie. Automatyzuje memoizację, ale potrzebuje dobrej architektury żeby mieć co optymalizować.
-
INP nie kłamie - 2662ms → 218ms. Te dane pokazują jak ważne jest zrozumienie mechaniki i architektury Reacta, a nie poleganie tylko na narzędziach.
Linki
- Demo - Zła architektura: react-compiler-performance-demo.vercel.app/context-global
- Demo - Dobra architektura: react-compiler-performance-demo.vercel.app/state-colocation
- React Scan: react-scan.com
Chcesz wejść głębiej w temat wydajności już teraz? To, co tutaj pokazałem, to fundament nowoczesnego Web Performance. Jeśli chcesz nauczyć się diagnozować wąskie gardła, rozumieć metryki Core Web Vitals i optymalizować aplikacje Reactowe i Next jsowe na poziomie architektonicznym oraz pozostać na czasie z nowościami w świecie Next.js/Reacta – zapraszam Cię na nasze szkolenie:
👉 Sprawdź agendę: Szkolenie WDI - Web Performance City
