Kurs JavaScript i React · Moduł 10: Ekosystem i przyszłość React

React Compiler (React Forget)

7 min czytania
W tej lekcji8

Wyobraź sobie, że na Twoim statku kosmicznym zamiast ręcznego konfigurowania każdego systemu optymalizacji, masz inteligentny autopilot, który automatycznie wykrywa i optymalizuje wszystkie redundantne operacje. Dokładnie to robi React Compiler (znany również jako React Forget) - automatycznie analizuje Twój kod i dodaje memoizację tam, gdzie jest potrzebna. Wersja 1.0 jest stabilna od października 2025 roku i działa z React 17, 18 i 19, najlepiej z 19.

Problem: ręczna memoizacja

Do tej pory, optymalizacja wydajności w React wymagała ręcznego stosowania React.memo, useMemo i useCallback. To jak ręczne konfigurowanie każdego systemu na statku kosmicznym - czasochłonne, podatne na błędy i łatwe do przeoczenia:

1// PRZED React Compiler - ręczna memoizacja
2import { useState, useMemo, useCallback, memo } from 'react';
3
4const MissionCard = memo(function MissionCard({ mission, onSelect }) {
5  return (
6    <div onClick={() => onSelect(mission.id)}>
7      <h3>{mission.name}</h3>
8      <p>Status: {mission.status}</p>
9    </div>
10  );
11});
12
13function MissionDashboard({ missions }) {
14  const [filter, setFilter] = useState('all');
15  const [selectedId, setSelectedId] = useState(null);
16
17  // Musisz pamiętać o useMemo...
18  const filteredMissions = useMemo(
19    () => missions.filter(m => filter === 'all' || m.status === filter),
20    [missions, filter]
21  );
22
23  // ...i o useCallback dla każdej funkcji przekazywanej jako prop
24  const handleSelect = useCallback((id) => {
25    setSelectedId(id);
26  }, []);
27
28  return (
29    <div>
30      {filteredMissions.map(m => (
31        <MissionCard key={m.id} mission={m} onSelect={handleSelect} />
32      ))}
33    </div>
34  );
35}

Problemy z ręczną memoizacją:

  • Łatwo zapomnieć - pominiesz useCallback i React.memo nie zadziała
  • Boilerplate - dużo kodu, który nie dotyczy logiki biznesowej
  • Błędne zależności - zły dependency array w useMemo/useCallback = bug
  • Nadmierna memoizacja - memoizujesz rzeczy, które nie potrzebują optymalizacji

React Compiler - automatyczna memoizacja

React Compiler to narzędzie budowania (build-time tool), które automatycznie analizuje komponenty React i dodaje odpowiednią memoizację. Działa na etapie kompilacji - Twój kod źródłowy pozostaje czysty:

1// PO React Compiler - piszesz zwykły, czysty kod
2function MissionDashboard({ missions }) {
3  const [filter, setFilter] = useState('all');
4  const [selectedId, setSelectedId] = useState(null);
5
6  // Compiler AUTOMATYCZNIE memoizuje to obliczenie
7  const filteredMissions = missions.filter(
8    m => filter === 'all' || m.status === filter
9  );
10
11  // Compiler AUTOMATYCZNIE stabilizuje tę funkcję
12  const handleSelect = (id) => {
13    setSelectedId(id);
14  };
15
16  return (
17    <div>
18      {filteredMissions.map(m => (
19        <MissionCard key={m.id} mission={m} onSelect={handleSelect} />
20      ))}
21    </div>
22  );
23}
24
25// Nie potrzebujesz React.memo - Compiler sam to ogarnie!
26function MissionCard({ mission, onSelect }) {
27  return (
28    <div onClick={() => onSelect(mission.id)}>
29      <h3>{mission.name}</h3>
30      <p>Status: {mission.status}</p>
31    </div>
32  );
33}

Jak działa React Compiler?

Compiler analizuje Twój kod podczas budowania (np. z Babel pluginem) i generuje zoptymalizowaną wersję:

1// Twój kod źródłowy (co piszesz):
2function StarList({ stars, highlight }) {
3  const visible = stars.filter(s => s.magnitude < 6);
4  const style = { color: highlight ? '#ffd700' : '#ccc' };
5
6  return (
7    <ul style={style}>
8      {visible.map(s => <li key={s.id}>{s.name}</li>)}
9    </ul>
10  );
11}
12
13// Co Compiler generuje (uproszczony przykład):
14function StarList_compiled({ stars, highlight }) {
15  // Compiler dodaje cache na filtrowanie
16  const visible = useMemo(
17    () => stars.filter(s => s.magnitude < 6),
18    [stars]
19  );
20
21  // Compiler cachuje obiekt style
22  const style = useMemo(
23    () => ({ color: highlight ? '#ffd700' : '#ccc' }),
24    [highlight]
25  );
26
27  return (
28    <ul style={style}>
29      {visible.map(s => <li key={s.id}>{s.name}</li>)}
30    </ul>
31  );
32}

Compiler robi to inteligentnie - nie memoizuje wszystkiego, tylko te części, które faktycznie mogą się powtarzać z tymi samymi danymi.

Konfiguracja React Compiler

Instalacja z Babel

Kompilator to plugin Babela babel-plugin-react-compiler, który instalujesz jako zależność deweloperską. W konfiguracji musi stać na początku listy pluginów, bo potrzebuje oryginalnego kodu, zanim zmienią go inne transformacje:

1npm install -D babel-plugin-react-compiler@latest
1// babel.config.js
2module.exports = {
3  plugins: [
4    // React Compiler musi działać jako pierwszy
5    ['babel-plugin-react-compiler', {
6      compilationMode: 'infer', // tryb domyślny
7      panicThreshold: 'none', // komponent z błędem zostaje bez optymalizacji, a build trwa dalej
8    }],
9    // ...pozostałe pluginy
10  ],
11};

W projekcie na React 17 lub 18 dodaj opcję target: '18' (albo '17') i zainstaluj pakiet react-compiler-runtime, bo skompilowany kod potrzebuje wtedy tej biblioteki. W Vite 8 z @vitejs/plugin-react 6 kompilator dodajesz przez @rolldown/plugin-babel i preset reactCompilerPreset:

1// vite.config.js
2import { defineConfig } from 'vite';
3import react, { reactCompilerPreset } from '@vitejs/plugin-react';
4import babel from '@rolldown/plugin-babel';
5
6export default defineConfig({
7  plugins: [
8    react(),
9    babel({
10      presets: [reactCompilerPreset()]
11    }),
12  ],
13});

Instalacja z Next.js

W Next.js 16 instalujesz ten sam plugin jako zależność deweloperską i włączasz opcję reactCompiler na najwyższym poziomie konfiguracji. We wcześniejszych wersjach Next.js była ona w sekcji experimental. Next.js uruchamia plugin tylko dla plików z JSX albo hookami, więc build zwalnia nieznacznie:

1// next.config.js
2const nextConfig = {
3  reactCompiler: true,
4};
5
6module.exports = nextConfig;

Kompilator możesz też włączać stopniowo: reactCompiler: { compilationMode: 'annotation' } kompiluje tylko komponenty z dyrektywą 'use memo' na początku funkcji, a dyrektywa 'use no memo' wyłącza kompilację wybranego komponentu.

Tryby kompilacji

  • infer (domyślny) - kompiluje funkcje nazwane jak komponenty (PascalCase) albo hooki (use...), które tworzą JSX lub wywołują inne hooki, oraz funkcje z dyrektywą 'use memo'
  • annotation - kompiluje tylko funkcje oznaczone dyrektywą 'use memo', co ułatwia stopniowe wdrażanie
  • all - kompiluje wszystkie funkcje najwyższego poziomu, także te, które nie są komponentami, dlatego nie jest zalecany

Zasady React (Rules of React)

React Compiler działa poprawnie TYLKO jeśli Twój kod przestrzega "Rules of React":

1// DOBRZE - czysta funkcja, brak efektów ubocznych w renderze
2function GoodComponent({ data }) {
3  const sorted = [...data].sort((a, b) => a.name.localeCompare(b.name));
4  return <ul>{sorted.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
5}
6
7// ŹLE - mutacja props (Compiler może wygenerować błędny kod!)
8function BadComponent({ data }) {
9  data.sort((a, b) => a.name.localeCompare(b.name)); // Mutacja!
10  return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
11}
12
13// ŹLE - efekt uboczny w renderze
14function BadComponent2({ userId }) {
15  // Compiler założy że to jest czyste i może je pominąć!
16  analytics.track('render', userId);
17  return <div>User: {userId}</div>;
18}

Kluczowe zasady:

  1. Komponenty muszą być czystymi funkcjami - ten sam input = ten sam output
  2. Nie mutuj props ani state bezpośrednio
  3. Efekty uboczne tylko w useEffect, event handlerach lub callbackach
  4. Nie czytaj refs podczas renderowania (tylko w efektach i handlerach)

ESLint plugin

Reguły kompilatora są dziś częścią oficjalnego pakietu eslint-plugin-react-hooks (od wersji 6, obecnie 7.x), obok rules-of-hooks i exhaustive-deps. Instalujesz go poleceniem npm install -D eslint-plugin-react-hooks@latest. Preset recommended w konfiguracji flat włącza wszystkie zalecane reguły, a poziom pojedynczej reguły ustawisz w sekcji rules:

1// eslint.config.js
2import reactHooks from 'eslint-plugin-react-hooks';
3import { defineConfig } from 'eslint/config';
4
5export default defineConfig([
6  {
7    // pliki .js i .jsx z parserem JSX (bez tego ESLint 9 pominie pliki .jsx)
8    files: ['**/*.{js,jsx}'],
9    languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
10  },
11  reactHooks.configs.flat.recommended,
12  {
13    rules: {
14      'react-hooks/purity': 'error', // render bez efektów ubocznych, np. bez Math.random()
15      'react-hooks/refs': 'error', // bez czytania i zapisu ref.current w renderze
16      'react-hooks/immutability': 'error', // bez mutowania props i stanu
17    },
18  },
19]);

Dawny pakiet eslint-plugin-react-compiler z regułą 'react-compiler/react-compiler' zastąpiły te reguły, więc w nowych projektach go nie dodawaj. Linter pokaże też, które komponenty kompilator pominie i dlaczego.

Migracja istniejącego kodu

Jeśli masz istniejący projekt z ręczną memoizacją, możesz wdrażać kompilator stopniowo, na przykład w trybie annotation. Istniejące React.memo, useMemo i useCallback nie kolidują z kompilatorem. Dokumentacja Reacta zaleca zostawić je na miejscu albo usuwać dopiero po testach, bo ich usunięcie może zmienić wynik kompilacji. W nowym kodzie polegaj na kompilatorze, a useMemo i useCallback traktuj jako furtkę, gdy potrzebujesz pełnej kontroli, na przykład nad zależnością efektu.

Podsumowanie

React Compiler 1.0 zmienia podejście do optymalizacji w React:

  1. Automatyczna memoizacja - Compiler zapamiętuje wyniki za Ciebie, tak jakbyś dodał useMemo, useCallback i React.memo tam, gdzie trzeba
  2. Czystszy kod - piszesz prostszy, łatwiejszy do utrzymania kod
  3. Mniej błędów - Compiler nie zapomni o dependency array
  4. Build-time - optymalizacje powstają przy budowaniu, a w runtime zostaje tylko lekki cache wyników
  5. Wymaga czystego kodu - komponenty muszą przestrzegać Rules of React

Pamiętaj: React Compiler to jak autopilot na statku kosmicznym - zrobi swoją pracę doskonale, ale wymaga, żeby statek był prawidłowo zbudowany.

Kod do tej lekcji: App.jsx
1import React, { useState, useMemo, useCallback, memo } from 'react';
2
3// === REACT COMPILER - Symulacja automatycznej memoizacji ===
4// Porównanie: ręczna memoizacja i kod "po kompilacji"
5
6// Licznik renderów: celowy efekt uboczny w renderze, tylko na potrzeby pomiaru.
7// W prawdziwej aplikacji rendery mierzy React DevTools Profiler.
8let renderLog = {};
9function trackRender(name) {
10  renderLog[name] = (renderLog[name] || 0) + 1;
11  return renderLog[name];
12}
13
14// === WERSJA 1: BEZ memoizacji (naiwny kod) ===
15function NaiveMissionCard({ mission, onSelect }) {
16  const count = trackRender('Naive-' + mission.id);
17  return (
18    <div style={{ ...styles.card, borderColor: 'rgba(255,107,107,0.3)' }}>
19      <div style={styles.renderBadge('#ff6b6b')}>R: {count}</div>
20      <strong>{mission.name}</strong>
21      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
22      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Wybierz</button>
23    </div>
24  );
25}
26
27// === WERSJA 2: RĘCZNA memoizacja (React.memo + useCallback + useMemo) ===
28const ManualMissionCard = memo(function ManualMissionCard({ mission, onSelect }) {
29  const count = trackRender('Manual-' + mission.id);
30  return (
31    <div style={{ ...styles.card, borderColor: 'rgba(0,212,255,0.3)' }}>
32      <div style={styles.renderBadge('#00d4ff')}>R: {count}</div>
33      <strong>{mission.name}</strong>
34      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
35      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Wybierz</button>
36    </div>
37  );
38});
39
40// === WERSJA 3: "Skompilowana" (symulacja React Compiler) ===
41// W prawdziwym projekcie taką memoizację wygenerowałby kompilator
42const CompiledMissionCard = memo(function CompiledMissionCard({ mission, onSelect }) {
43  const count = trackRender('Compiled-' + mission.id);
44  return (
45    <div style={{ ...styles.card, borderColor: 'rgba(0,255,136,0.3)' }}>
46      <div style={styles.renderBadge('#00ff88')}>R: {count}</div>
47      <strong>{mission.name}</strong>
48      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
49      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Wybierz</button>
50    </div>
51  );
52});
53
54const STATUS_LABELS = { active: 'aktywna', planned: 'planowana', completed: 'ukończona' };
55
56// Dane testowe
57const MISSIONS = [
58  { id: 1, name: 'Alpha Centauri', status: 'active' },
59  { id: 2, name: 'Europa Probe', status: 'planned' },
60  { id: 3, name: 'Mars Colony', status: 'completed' },
61  { id: 4, name: 'Titan Explorer', status: 'active' },
62];
63
64function App() {
65  const [counter, setCounter] = useState(0);
66  const [selected, setSelected] = useState(null);
67  const [filter, setFilter] = useState('all');
68
69  // NAIWNE: nowa funkcja i filtrowanie przy każdym renderze
70  const naiveFiltered = MISSIONS.filter(m => filter === 'all' || m.status === filter);
71  const naiveSelect = (id) => setSelected(id);
72
73  // RĘCZNE: useMemo + useCallback
74  const manualFiltered = useMemo(
75    () => MISSIONS.filter(m => filter === 'all' || m.status === filter),
76    [filter]
77  );
78  const manualSelect = useCallback((id) => setSelected(id), []);
79
80  // "SKOMPILOWANE": to samo co ręczne, ale dodane automatycznie przez kompilator
81  const compiledFiltered = useMemo(
82    () => MISSIONS.filter(m => filter === 'all' || m.status === filter),
83    [filter]
84  );
85  const compiledSelect = useCallback((id) => setSelected(id), []);
86
87  return (
88    <div style={styles.container}>
89      <header style={styles.header}>
90        <h1 style={styles.title}>React Compiler - porównanie</h1>
91        <p style={styles.subtitle}>Kliknij "Re-render" i obserwuj liczniki renderów</p>
92      </header>
93
94      <div style={styles.controls}>
95        <select value={filter} onChange={e => setFilter(e.target.value)} style={styles.select}>
96          <option value="all">Wszystkie</option>
97          <option value="active">Aktywne</option>
98          <option value="planned">Planowane</option>
99          <option value="completed">Ukończone</option>
100        </select>
101        <button onClick={() => setCounter(c => c + 1)} style={styles.reRenderBtn}>
102          Re-render ({counter})
103        </button>
104        {selected && <span style={styles.selectedInfo}>Wybrano ID: {selected}</span>}
105      </div>
106
107      <div style={styles.grid}>
108        <div style={styles.column}>
109          <h3 style={{ ...styles.colTitle, color: '#ff6b6b' }}>Bez memoizacji</h3>
110          <p style={styles.colDesc}>Każdy re-render = nowa funkcja + nowe filtrowanie</p>
111          {naiveFiltered.map(m => (
112            <NaiveMissionCard key={m.id} mission={m} onSelect={naiveSelect} />
113          ))}
114        </div>
115
116        <div style={styles.column}>
117          <h3 style={{ ...styles.colTitle, color: '#00d4ff' }}>Ręczna memoizacja</h3>
118          <p style={styles.colDesc}>React.memo + useCallback + useMemo</p>
119          {manualFiltered.map(m => (
120            <ManualMissionCard key={m.id} mission={m} onSelect={manualSelect} />
121          ))}
122        </div>
123
124        <div style={styles.column}>
125          <h3 style={{ ...styles.colTitle, color: '#00ff88' }}>React Compiler</h3>
126          <p style={styles.colDesc}>Automatycznie dodana memoizacja</p>
127          {compiledFiltered.map(m => (
128            <CompiledMissionCard key={m.id} mission={m} onSelect={compiledSelect} />
129          ))}
130        </div>
131      </div>
132    </div>
133  );
134}
135
136const styles = {
137  container: { fontFamily: 'system-ui', background: '#0a0e17', color: '#e0e1dd', minHeight: '100vh', padding: '14px' },
138  header: { textAlign: 'center', marginBottom: '12px' },
139  title: { fontSize: '20px', color: '#00d4ff', margin: '0 0 4px' },
140  subtitle: { fontSize: '12px', color: '#8892b0', margin: 0 },
141  controls: { display: 'flex', gap: '8px', justifyContent: 'center', alignItems: 'center', marginBottom: '14px', flexWrap: 'wrap' },
142  select: { padding: '5px 10px', background: '#1a1f2e', color: '#e0e1dd', border: '1px solid rgba(0,212,255,0.3)', borderRadius: '4px', fontSize: '12px' },
143  reRenderBtn: { padding: '5px 14px', background: 'rgba(255,107,107,0.15)', color: '#ff6b6b', border: '1px solid rgba(255,107,107,0.3)', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' },
144  selectedInfo: { fontSize: '12px', color: '#ffd700' },
145  grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '10px' },
146  column: { background: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '10px' },
147  colTitle: { fontSize: '13px', margin: '0 0 4px' },
148  colDesc: { fontSize: '10px', color: '#556070', margin: '0 0 8px' },
149  card: { display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 8px', background: 'rgba(0,0,0,0.3)', borderRadius: '4px', marginBottom: '4px', fontSize: '11px', border: '1px solid', position: 'relative' },
150  renderBadge: (color) => ({ position: 'absolute', top: '2px', right: '4px', fontSize: '9px', color, fontWeight: 'bold' }),
151  status: (s) => ({ padding: '1px 5px', borderRadius: '6px', fontSize: '9px', background: s === 'active' ? 'rgba(0,255,136,0.15)' : s === 'completed' ? 'rgba(0,212,255,0.15)' : 'rgba(255,170,0,0.15)', color: s === 'active' ? '#00ff88' : s === 'completed' ? '#00d4ff' : '#ffaa00' }),
152  selectBtn: { marginLeft: 'auto', padding: '2px 6px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '3px', color: '#8892b0', cursor: 'pointer', fontSize: '10px' },
153};
154
155export default App;

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co React Compiler robi automatycznie podczas kompilacji?

  2. 2. Która praktyka ŁAMIE zasady React i może spowodować błędy w React Compiler?

Zadania praktyczne w grze

  • Klikanie w kolejności

    Ułóż konfigurację React Compiler w next.config.js (Next.js 16):

  • Układanie w poziomie

    Ułóż regułę ESLint, która pilnuje czystości komponentów - jedną z reguł React Compiler w eslint-plugin-react-hooks:

Przydatne artykuły