Упродовж майже десятиліття екосистема React покладалася на ручну оптимізацію рендерингу за допомогою тріади React.memo, useMemo та useCallback. У промисловій розробці цей підхід спричинив системну кризу продуктивності та супроводу коду:
Object.is) у тисячах хуків нерідко перевищують вигоду від пропуску рендерингу легковагових компонентів.React Compiler (первинна кодова назва — Project Forget) радикально змінює цю парадигму виконання коду. Це оптимізуючий випереджальний (AOT-компілятор), який глибоко аналізує семантику JavaScript та інваріанти Rules of React.
Компілятор трансформує компоненти та кастомні хуки, автоматично інтегруючи гранулярну мемоїзацію — від констант і обробників подій до складних об'єктів та самих повернених віртуальних JSX-дерев.
У React 19 ручне використання useMemo та useCallback стає застарілою практикою: компілятор оптимізує проміжне представлення на рівні графа потоку керування значно точніше, ефективніше і без ризику людської помилки.
В основі React Compiler лежить класична архітектура оптимізуючих компіляторів (схожа на LLVM та TurboFan у V8), адаптована до реактивної парадигми React:
[ Вихідний код JSX / TSX ]
│
▼ (Парсер Babel / SWC)
[ Синтаксичне дерево (AST) ]
│
▼ (Побудова графа потоку керування)
[ HIR (High-level Intermediate Representation) ]
│
▼ (Зведення до SSA-форми)
[ SSA-форма: Унікальні регістри v0, v1, v2 ]
│
├───► [ Liveness Analysis (Час життя змінних) ]
├───► [ Escape Analysis (Аналіз витоків посилань) ]
└───► [ Infer Mutation Ranges (Аналіз мутацій) ]
│
▼ (Обчислення меж реактивності)
[ Виділення реактивних скоупів (Reactive Scopes) ]
│
▼ (Генерація коду з рантайм-кешем c())
[ Оптимізований JS: $ = c(slotCount) ]Компілятор перетворює вихідне AST у HIR (High-level Intermediate Representation) — орієнтований граф базових блоків інструкцій. Потім HIR зводиться до SSA (Static Single Assignment), де кожна змінна отримує значення рівно один раз.
Завдяки SSA та аналізу часу життя компілятор чітко розрізняє:
На основі цих меж формуються Reactive Scopes. У згенерованому JavaScript кожен скоуп захищений зверненням до плоского масиву пам'яті — рантайм-слотів useMemoCache ($ / c).
React Compiler кешує не лише проміжні обчислення, але й самі повернені дескриптори React Element ($[i] = jsx(...)). Якщо вхідні пропси не змінилися, компонент повертає точно те саме посилання на JSX-елемент. Двигун Fiber у фазі beginWork бачить рівність посилань oldProps === newProps і за O(1) повністю пропускає рендеринг усього вкладеного піддерева!
Перетворення коду TSX на синтаксичне дерево. Перевірка інваріантів чистоти: незмінність пропсів, заборона виклику хуків в умовах і циклах.
Розбиття коду на базові блоки розгалуження (if/else, switch, цикли) з єдиною точкою входу та виходу.
Присвоєння змінним унікальних віртуальних регістрів (v0, v1, v2). Вставка Phi-функцій у точках злиття потоків для відстеження значень.
Аналіз витоків і часу життя змінних. Групування пов'язаних інструкцій у реактивні скоупи з фіксацією входів і виходів.
Заміна тіла функції на прямі операції за числовим індексом над масивом пам'яті $[i], усуваючи накладні витрати викликів хуків.
Чому перетворення синтаксичного дерева на SSA-форму (Static Single Assignment) є критично важливим етапом для компілятора React?
Аналіз вихідного коду TSX. Перевірка інваріантів чистоти: заборона мутації пропсів, чистота рендерингу, правила виклику хуків.
Перетворення синтаксичного дерева на плоский список базових блоків. Призначення унікальних регістрів (v0, v1, v2).
Визначення інтервалів мутабельності об'єктів (InferMutableRanges). Перевірка, чи мутує функція свої аргументи або повертає клон.
Групування інструкцій у реактивні скоупи та розрахунок слотів пам'яті. Заміна тіла функції на перевірку кешу $ = c(N).
Розглянемо компонент аналітичної картки до та після обробки компілятором.
Вихідний код розробника (написаний без жодного хука мемоїзації):
interface MetricProps {
label: string;
value: number;
threshold: number;
onAlert: (msg: string) => void;
}
export function MetricCard({ label, value, threshold, onAlert }: MetricProps) {
const isCritical = value > threshold;
const statusMessage = `Показник ${label}: ${isCritical ? 'Критичний' : 'У нормі'}`;
const handleNotify = () => {
if (isCritical) {
onAlert(`Увага: ${statusMessage}`);
}
};
return (
<div className={`metric-card ${isCritical ? 'critical' : 'normal'}`}>
<h3>{label}</h3>
<p>{value}</p>
<button onClick={handleNotify}>Перевірити статус</button>
</div>
);
}А ось у що перетворює цей компонент React Compiler на виході збирача (декомпільоване представлення генератора HIR):
import { c as _c } from "react/compiler-runtime";
export function MetricCard(props: MetricProps) {
// Компілятор виділяє фіксований блок пам'яті на 11 слотів для цього інстансу Fiber
const $ = _c(11);
const { label, value, threshold, onAlert } = props;
const isCritical = value > threshold;
// Реактивний скоуп 1: Обчислення statusMessage
let statusMessage: string;
if ($[0] !== isCritical || $[1] !== label) {
statusMessage = `Показник ${label}: ${isCritical ? 'Критичний' : 'У нормі'}`;
$[0] = isCritical;
$[1] = label;
$[2] = statusMessage;
} else {
statusMessage = $[2];
}
// Реактивний скоуп 2: Створення обробника handleNotify
let handleNotify: () => void;
if ($[3] !== isCritical || $[4] !== onAlert || $[5] !== statusMessage) {
handleNotify = () => {
if (isCritical) {
onAlert(`Увага: ${statusMessage}`);
}
};
$[3] = isCritical;
$[4] = onAlert;
$[5] = statusMessage;
$[6] = handleNotify;
} else {
handleNotify = $[6];
}
// Реактивний скоуп 3: Мемоїзація самого поверненого JSX-дерева!
let jsxContent: JSX.Element;
if (
$[7] !== handleNotify ||
$[8] !== isCritical ||
$[9] !== label ||
$[10] !== value
) {
jsxContent = (
<div className={`metric-card ${isCritical ? 'critical' : 'normal'}`}>
<h3>{label}</h3>
<p>{value}</p>
<button onClick={handleNotify}>Перевірити статус</button>
</div>
);
$[7] = handleNotify;
$[8] = isCritical;
$[9] = label;
$[10] = value;
} else {
jsxContent = $[7];
}
return jsxContent;
}Зверніть увагу на збереження дескриптора JSX-елемента в комірці $[7]. Якщо пропси label, value, threshold та функція onAlert не змінилися, компонент повертає точно те саме посилання на об'єкт. У фазі beginWork рушій Fiber бачить oldProps === newProps і миттєво здійснює O(1) bailout, пропускаючи обхід усіх дочірніх DOM-вузлів.
"use memo" та "use no memo"#У великих корпоративних кодових базах компілятор можна впроваджувати поетапно або виключати модулі з імперативними маніпуляціями:
// Повне вимкнення оптимізації для компонента з імперативними маніпуляціями DOM
export function HeavyWebGLCanvas({ contextId }: { contextId: string }) {
"use no memo";
// Цей компонент не буде перетворено в HIR-кеш,
// що необхідно під час взаємодії з низькорівневими WebGL-буферами
return <canvas id={contextId} />;
}
// Примусове увімкнення в режимі вибіркової оптимізації (opt-in)
export function OptimizedWidget({ data }: { data: string[] }) {
"use memo";
return <ul>{data.map(item => <li key={item}>{item}</li>)}</ul>;
}Що відбудеться, якщо в тілі компонента розробник спробує відсортувати масив із пропсів на місці через props.items.sort()?
Для активації компілятора в сучасному стеку (Vite + React 19 + TypeScript) використовується офіційний Babel-плагін разом із перевірками ESLint:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
const ReactCompilerConfig = {
target: '19', // Орієнтація на рантайм React 19
sources: (filename: string): boolean => {
// Компілюємо вихідний код компонентів, виключаючи тести та сторонні модулі
return filename.includes('src/') && !filename.includes('.test.') && !filename.includes('.spec.');
},
panicThreshold: 'all_errors', // У CI викликає помилку у разі неможливості оптимізації
};
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig],
],
},
}),
],
resolve: {
alias: {
'@': '/src',
},
},
});React Compiler спирається на контракт чистоти рендерингу (Purity of Render). Спроба відсортувати переданий масив пропсів на місці (props.items.sort()) або модифікувати поля об'єкта ламає SSA-аналіз діапазонів мутації (InferMutableRanges). Компілятор або повністю вимкне оптимізацію компонента, або згенерує некоректний кеш.
// ❌ ПОГАНО: мутація масиву пропсів на місці руйнує SSA форму
export function ProductList({ items }: { items: string[] }) {
const sorted = items.sort(); // Мутація вхідного масиву
return <div>{sorted.join(', ')}</div>;
}
// ✅ ПРАВИЛЬНО: використання іммутабельних методів ES2023
export function ProductList({ items }: { items: string[] }) {
const sorted = items.toSorted(); // Створення безпечної копії
return <div>{sorted.join(', ')}</div>;
}Рефи (useRef) призначені виключно для збереження імперативних значень між рендерами або посилань на DOM. Читання або мутація ref.current безпосередньо у фазі обчислення JSX робить рендеринг недетермінованим. Компілятор не може відстежити зміни ref.current, оскільки мутація рефа не ініціює новий рендер і оминає ланцюг реактивності.
// ❌ ПОГАНО: читання мутабельного рефа в логіці рендерингу ламає кеш
export function Timer({ initialSeconds }: { initialSeconds: number }) {
const countRef = useRef(initialSeconds);
if (countRef.current > 0) {
countRef.current -= 1; // Сайд-ефект під час рендерингу
}
return <span>{countRef.current}</span>;
}
// ✅ ПРАВИЛЬНО: використання явного стейту та ефектів
export function Timer({ initialSeconds }: { initialSeconds: number }) {
const [count, setCount] = useState(initialSeconds);
useEffect(() => {
const timer = setInterval(() => setCount(c => Math.max(0, c - 1)), 1000);
return () => clearInterval(timer);
}, []);
return <span>{count}</span>;
}Вимкнення правила react-hooks/exhaustive-deps за допомогою коментаря // eslint-disable-next-line при використанні React Compiler гарантовано призводить до непередбачуваної поведінки: компілятор довіряє зазначеним межам видимості і побудує некоректний HIR-граф, що спричинить відображення застарілих даних.
// ❌ ПОГАНО: приглушення лінтера маскує порушення інваріантів
useEffect(() => {
trackUserVisit(userId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// ✅ ПРАВИЛЬНО: розділення реактивної логіки за допомогою useEffectEvent
const handleTrack = useEffectEvent((id: string) => {
trackUserVisit(id);
});
useEffect(() => {
handleTrack(userId);
}, [userId]);Збереження старих useCallback та useMemo зі штучно скороченими масивами залежностей створює консервативні межі оптимізації в HIR-графі компілятора. Дозвольте компілятору оптимізувати стандартний декларативний JavaScript самостійно.