Kurs JavaScript i React · Moduł 10: Ekosystem i przyszłość React
React Compiler (React Forget)
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
useCallbackiReact.memonie 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@latest1// 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żanieall- 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:
- Komponenty muszą być czystymi funkcjami - ten sam input = ten sam output
- Nie mutuj props ani state bezpośrednio
- Efekty uboczne tylko w
useEffect, event handlerach lub callbackach - 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:
- Automatyczna memoizacja - Compiler zapamiętuje wyniki za Ciebie, tak jakbyś dodał
useMemo,useCallbackiReact.memotam, gdzie trzeba - Czystszy kod - piszesz prostszy, łatwiejszy do utrzymania kod
- Mniej błędów - Compiler nie zapomni o dependency array
- Build-time - optymalizacje powstają przy budowaniu, a w runtime zostaje tylko lekki cache wyników
- 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. Co React Compiler robi automatycznie podczas kompilacji?
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: