React Compiler w projekcie legacy — jak wdrożyć krok po kroku

React Compiler wdrożenie w projekcie legacy

Nowe projekty to bajka. Zaczynacie od czystej kartki, instalujecie React Compiler od razu, konfigurujecie ESLint i wszystko działa. Ale większość z nas nie ma tego luksusu.

Mam w swoim portfolio projekty, które zaczęły się w czasach, gdy useMemo i useCallback były jedynym sposobem na optymalizację. Kod pisany przez kilka lat, przez kilka zespołów, z różnymi standardami — albo bez nich. I właśnie dla takich projektów ten wpis powstał.

Pokażę Wam, jak podejść do wdrożenia React Compilera nie w idealnym świecie, ale w tym prawdziwym — gdzie macie 300 komponentów, połowa naruszeń Rules of React jest nieświadoma, a testy… są albo ich nie ma.

1. Dlaczego legacy to osobny przypadek

React Compiler działa najlepiej, gdy kod przestrzega Rules of React. W nowych projektach, gdzie od początku macie ESLint z odpowiednimi regułami, to naturalne. W projektach legacy — niekoniecznie.

Co typowo spotyka się w starszych bazach kodu:

Mutowanie stanu

// Stary nawyk — modyfikowanie obiektu bezpośrednio
const handleUpdate = () => {
  user.name = 'Anna'; // ❌
  setUser(user);
};

Efekty uboczne w renderze

// "Tymczasowe" logowanie, które zostało na zawsze
function UserCard({ user }) {
  console.log('render', user.id); // ❌ side effect w renderze
  analytics.track('view'); // ❌ to samo
  return <div>{user.name}</div>;
}

Niestabilne referencje w zależnościach

useEffect(() => {
  fetchData(config);
}, [config]); // ❌ jeśli config jest tworzony inline przy każdym renderze

Nieużywane, ale “bezpieczne” useMemo

// Ktoś dodał "na wszelki wypadek" — kompilator może to zignorować
const value = useMemo(() => data, []); // ❌ pusta tablica zależności + zmienna z zewnątrz

W nowym projekcie te problemy nie istnieją lub są wyłapywane na bieżąco. W legacy — są wszędzie i często nikt już nie pamięta, dlaczego dany fragment jest napisany tak, a nie inaczej.

Dobra wiadomość: React Compiler ma na to odpowiedź. Ma tryb, który pozwala wdrażać go stopniowo, komponent po komponencie.

2. Zanim zaczniesz — audyt projektu

Nie zaczynaj od konfiguracji. Zacznij od danych.

Krok 1: Zainstaluj eslint-plugin-react-compiler

pnpm install -D eslint-plugin-react-compiler

Jeśli używacie flat config (ESLint 9+) w pliku musi się znaleźć:

// eslint.config.js
import reactCompiler from 'eslint-plugin-react-compiler';

export default [
  {
    plugins: {
      'react-compiler': reactCompiler,
    },
    rules: {
      'react-compiler/react-compiler': 'error',
    },
  },
];

Jeśli używacie starszego formatu .eslintrc:

{
  "plugins": ["react-compiler"],
  "rules": {
    "react-compiler/react-compiler": "error"
  }
}

Krok 2: Zbierz raport

npx eslint src --format json > react-compiler-audit.json

Nie musicie tego parsować ręcznie. Możecie użyć prostego skryptu:

const fs = require('fs');
const data = JSON.parse(fs.readFileSync('react-compiler-audit.json'));

const issues = data
  .filter(file => file.messages.length > 0)
  .map(file => ({
    path: file.filePath,
    count: file.messages.filter(m => m.ruleId === 'react-compiler/react-compiler').length,
    messages: file.messages
      .filter(m => m.ruleId === 'react-compiler/react-compiler')
      .map(m => m.message)
  }))
  .filter(f => f.count > 0)
  .sort((a, b) => b.count - a.count);

console.log(`Łączna liczba naruszeń: ${issues.reduce((sum, f) => sum + f.count, 0)}`);
console.log(`Pliki z naruszeniami: ${issues.length}`);
console.log('\nNajbardziej problematyczne pliki:');
issues.slice(0, 10).forEach(f => {
  console.log(`  ${f.count}x ${f.path.split('/src/')[1]}`);
});

Uruchomienie skryptu:

node audit-summary.js

Krok 3: Oceń skalę

Wynik audytu powie Wam bardzo dużo. Na przykład w projekcie, który niedawno stworzyłam i opisywałam tutaj jako demo React Compiler mamy:

Łączna liczba naruszeń: 7
Pliki z naruszeniami: 1

Najbardziej problematyczne pliki:
  7x BadExamples.tsx

Ale w prawdziwym projekcie to może wyglądać np tak:

Łączna liczba naruszeń: 47
Pliki z naruszeniami: 23

Najbardziej problematyczne pliki:
  8x xxx.tsx
  6x xxy.tsx
  4x xxz.tsx
  ...

To nie jest katastrofa — 47 naruszeń w 23 plikach przy bazie 300+ komponentów to oznacza, że ponad 90% kodu jest gotowe na kompilator od zaraz.

Co oznaczają liczby?

WynikCo to znaczy
0–20 naruszeńMożecie rozważyć włączenie kompilatora globalnie od razu
20–100 naruszeńPodejście iteracyjne, kilka tygodni pracy
100+ naruszeńDługoterminowy plan migracji, zacznijcie od opt-in

3. Strategia wdrożenia — tryb opt-in

React Compiler oferuje trzy tryby pracy:

  • infer (domyślny) — kompilator sam decyduje, co optymalizować
  • annotation — optymalizuje tylko komponenty z dyrektywą  use memo
  • all — optymalizuje wszystko

Dla projektów legacy tryb annotation jest najbezpieczniejszy. Dajecie kompilatorowi wyraźny sygnał: “optymalizuj tylko to, co ci wskażę”.

Instalacja

pnpm install -D babel-plugin-react-compiler

Konfiguracja w trybie annotation

Jeśli używacie Vite:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', {
            compilationMode: 'annotation',
          }],
        ],
      },
    }),
  ],
});

Jeśli używacie Next.js:

// next.config.js
const nextConfig = {
  experimental: {
    reactCompiler: {
      compilationMode: 'annotation',
    },
  },
};

Dyrektywy opt-in i opt-out

// Opt-in: "skompiluj ten komponent"
function UserCard({ user }) {
  'use memo';
  return <div>{user.name}</div>;
}
// Opt-out: "nie dotykaj tego komponentu"
function LegacyWidget({ data }) {
  'use no memo';
  <em>// stary kod, który jeszcze nie jest gotowy</em>
  return <div>{data}</div>;
}

W trybie infer (gdy chcecie kompilować wszystko poza wyjątkami) "use no memo" jest szczególnie przydatne — możecie globalnie włączyć kompilator i wykluczyć tylko te komponenty, które jeszcze nie są gotowe.

4. Krok po kroku — iteracyjne wdrożenie

Mam sprawdzoną kolejność, która minimalizuje ryzyko.

Etap 1: Nowe komponenty (tydzień 1)

Zacznijcie od komponentów, które tworzycie teraz. Zero ryzyka — piszecie je od początku z myślą o kompilatorze.

Zasada: każdy nowy komponent dostaje use memo i jest pisany zgodnie z Rules of React.

To jest też moment, żeby wyrobić nawyk w zespole.

Etap 2: Izolowane, dobrze przetestowane komponenty (tydzień 2–3)

Wróćcie do wyników audytu. Znajdźcie komponenty:

  • bez naruszeń ESLint
  • z dobrym pokryciem testami
  • możliwie izolowane (mało zależności od reszty)

To są Wasi “bezpieczni kandydaci”. Dodajcie im use memo i obserwujcie:

npm test -- --watch

Włączcie też React DevTools Profiler i zróbcie baseline przed i po — za chwilę wrócimy do mierzenia efektów.

Etap 3: Komponenty z naruszeniami — po naprawie (tydzień 4+)

Każde naruszenie ESLint to zadanie do zrobienia. Naprawcie je, a potem dodajcie use memo.

Nie próbujcie naprawiać wszystkiego na raz. Idźcie plik po pliku, zaczynając od tych z najmniejszą liczbą naruszeń.

// Przed naprawą
function Cart({ items }) {
  const total = { value: 0, currency: 'PLN' }; // ❌ nowy obiekt przy każdym renderze
  items.forEach(item => { total.value += item.price }); // ❌ mutacja
  
  return <div>{total.value} {total.currency}</div>;
}
// Po naprawie
function Cart({ items }) {
  'use memo';
  
  const total = {
    value: items.reduce((sum, item) => sum + item.price, 0),
    currency: 'PLN'
  };
  
  return <div>{total.value} {total.currency}</div>;
}

Zwróćcie uwagę — nie ma tu już useMemo. React Compiler zadba o memoizację sam, więc przy okazji naprawiania naruszeń możecie od razu pozbywać się ręcznych useMemo i useCallback.

Etap 4: Przejście na tryb infer (opcjonalne)

Gdy większość komponentów jest naprawiona i przetestowana, możecie zmienić tryb na infer i dodać use no memo tylko do tych, które jeszcze nie są gotowe.

// vite.config.ts — zmiana z annotation na infer</em>
['babel-plugin-react-compiler', {
  compilationMode: 'infer', // kompiluje wszystko, co spełnia Rules of React
}],

To naturalny punkt docelowy — kompilator sam decyduje i nie musicie ręcznie oznaczać każdego komponentu.

5. Pułapki, na które możecie się natknąć

Zewnętrzne biblioteki, które łamią reguły

To największy ból głowy. Biblioteki komponentów (szczególnie starsze wersje) często nie przestrzegają Rules of React wewnętrznie.

Symptom: po włączeniu use memo w komponencie, który używa zewnętrznej biblioteki, zaczynają się dziwne bugi — komponenty nie aktualizują się, albo aktualizują się za często.

Rozwiązanieuse no memo na wrappery wokół problematycznych bibliotek, dopóki biblioteka nie zostanie zaktualizowana lub zastąpiona.

// Wrapper wokół starszej biblioteki
function LegacyChartWrapper({ data }) {
  'use no memo'; // dopóki biblioteka nie jest kompatybilna
  return <OldChartLibrary data={data} />;
}

Konteksty z niestabilnymi wartościami

// ❌ Nowy obiekt przy każdym renderze providera
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  
  return (
    <AppContext.Provider value={{ user, setUser }}> {/* nowy obiekt przy każdym renderze */}
      {children}
    </AppContext.Provider>
  );
}

// ✅ React Compiler sam to zoptymalizuje — nie trzeba ręcznie dodawać useMemo
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  
  return (
    <AppContext.Provider value={{ user, setUser }}> 
      {children}
    </AppContext.Provider>
  );
}

Jeśli kompilator jest włączony, nie trzeba ręcznie owijać w useMemo.

Efekty uboczne w obliczeniach

// ❌ Kompilator nie zoptymalizuje — side effect w "czystej" funkcji
const processedData = useMemo(() => {
  logToAnalytics('processing'); // ❌ side effect
  return data.map(transform);
}, [data]);

// ✅ Side effecty oddzielone od obliczeń</em>
useEffect(() => {
  logToAnalytics('processing');
}, [data]);

const processedData = data.map(transform); // useMemo jest tu zbędne — kompilator zajmie się memoizacją sam.

useRef jako mutable state

<em>// ❌ Częsty antywzorzec w starszym kodzie</em>
function Timer() {
  const count = useRef(0);
  
  const tick = () => {
    count.current++; <em>// ❌ mutacja ref używana do śledzenia stanu UI</em>
    setDisplay(count.current);
  };
}

<em>// ✅ Stan do stanu, ref do refów</em>
function Timer() {
  const [count, setCount] = useState(0);
  
  const tick = () => {
    setCount(c => c + 1);
  };
}

6. Jak mierzyć efekty

Nie wdrażajcie kompilatora bez mierzenia — inaczej nie wiecie, czy cokolwiek zrobiliście dobrego.

React DevTools Profiler

  1. Otwórzcie DevTools → zakładka Profiler
  2. Kliknijcie “Record”
  3. Wykonajcie kilka typowych akcji w aplikacji (nawigacja, interakcje z formularzami, aktualizacje danych)
  4. Zatrzymajcie nagrywanie
  5. Zapiszcie wyniki jako baseline

Powtórzcie po wdrożeniu kompilatora w danym obszarze.

Co możecie zaobserwować przede wszystkim:

  • Liczba re-renderów — powinna spaść dla komponentów zależnych od zoptymalizowanych
  • Czas renderowania — dla ciężkich komponentów z kosztownymi obliczeniami
  • “Wasted renders” — komponenty, które renderują się bez zmiany propsów

Prosta miara: liczba zoptymalizowanych komponentów

React Compiler oznacza zoptymalizowane komponenty w DevTools — zobaczycie ikonkę ✨ przy nazwie komponentu. To dobry wskaźnik postępu migracji.

Mierzenie daje Wam również konkretne argumenty do rozmowy z zespołem i product ownerem.


7. Podsumowanie — checklista wdrożenia

Wklejcie do Notion, Jiry albo wydrukujcie i powieście na monitorze 😄

Przygotowanie

  • [ ] Zainstaluj eslint-plugin-react-compiler
  • [ ] Uruchom audyt ESLint i zapisz raport
  • [ ] Policz naruszenia i oceń skalę (< 20 / 20–100 / 100+)
  • [ ] Pogrupuj naruszenia według typu
  • [ ] Zidentyfikuj “bezpiecznych kandydatów” (bez naruszeń + testy)

Konfiguracja

  • [ ] Zainstaluj babel-plugin-react-compiler
  • [ ] Skonfiguruj w trybie annotation (dla legacy)
  • [ ] Sprawdź, czy build działa poprawnie
  • [ ] Zrób baseline w React DevTools Profiler

Iteracyjne wdrożenie

  • [ ] Etap 1: Nowe komponenty oznaczaj "use memo" od razu
  • [ ] Etap 2: Dodaj "use memo" do bezpiecznych kandydatów
  • [ ] Etap 3: Napraw naruszenia ESLint, zanim dodasz "use memo"
  • [ ] Etap 4: Przetestuj po każdym pliku / module
  • [ ] Etap 5 (opcjonalne): Przejdź na tryb infer gdy większość jest gotowa

Weryfikacja

  • [ ] Testy automatyczne przechodzą
  • [ ] Profilowanie pokazuje poprawę (lub brak regresji)
  • [ ] Ręczne testy kluczowych flow aplikacji
  • [ ] Code review z uwzględnieniem nowych wzorców

Wdrożenie React Compilera w legacy projekcie to maraton, nie sprint. Ale iteracyjne podejście — zaczynając od audytu, przez opt-in, po stopniowe rozszerzanie zakresu — pozwala robić to bezpiecznie i bez rewolucji.


To piąty wpis z serii o React Compiler. Poprzednie:

You might also like