Историческая модель React опирается на принцип чистых функций: при каждом изменении стейта или пропсов компонент выполняет ре-рендер всего поддерева. Чтобы избежать избыточных вычислений и пересоздания инстансов виртуального дерева (VDOM), инженеры были вынуждены вручную управлять мемоизацией с помощью примитивов useMemo, useCallback и HOC React.memo.
Такой подход создал системный технический долг в масштабах индустрии:
deps) влечет за собой устаревшие замыкания (stale closures), а добавление нестабильных объектов — каскадную инвалидацию нижестоящих поддеревьев.useMemo кэширует лишь крупные ветви вычислений, оставляя немемоизированными промежуточные выражения, конкатенации JSX и литералы объектов.React Compiler (кодовое имя React Forget) переносит ответственность за ссылочную стабильность и кэширование вычислений с разработчика на фазу статической компиляции. Он трансформирует код компонентов в форму HIR, преобразует его в статическое однократное присваивание (SSA), строит граф потока управления (CFG), проводит анализ времени жизни переменных и выводит автоматические реактивные области (Reactive Scopes).
Финальным шагом компилятор заменяет ручные хуки мемоизации на сгенерированные плоские кэш-слоты рантайма _c(N) через встроенный примитив useMemoCache.
В основе компилятора лежит классический пайплайн оптимизирующих компиляторов языков со статической семантикой. Компилятор не работает с голым исходным JavaScript AST напрямую во время глубокого анализа, поскольку AST слишком неоднороден: в нем существуют десятки способов выразить циклы, присваивания и замыкания.
Вместо этого Babel AST парсится и «уплощается» в High-Level Intermediate Representation (HIR), где весь императивный поток управления сводится к базовым блокам и линейным инструкциям.
[ Исходный JSX / TypeScript AST ]
│
▼
[ Lowering: Babel AST -> HIR ]
│
▼
[ SSA-трансформация (вставка Phi-узлов) ]
│
▼
[ Анализ времени жизни и мутаций (CFG & Liveness) ]
│
▼
[ Выведение Reactive Scopes (Границы скоупов) ]
│
▼
[ Codegen: Инъекция _c(N) runtime cache slots ]
│
▼
[ Оптимизированный JS-код без useMemo/useCallback ]В форме SSA каждая переменная версионируется: x_0, x_1, x_2. Если переменная модифицируется в разных ветках if / else, в точке слияния потоков управления базового блока вставляется специальная функция слияния — $\phi$ (phi-node), разрешающая актуальное значение в зависимости от предшествующего блока графа.
Это позволяет компилятору математически точно доказать:
Компилятор анализирует AST на соблюдение инвариантов: отсутствие безусловных вызовов хуков в циклах/ветвлениях и неизменяемость пропсов во время рендера.
Древовидная структура AST разворачивается в ориентированный граф базовых блоков (Basic Blocks), состоящих из плоских трехадресных инструкций.
Устраняются повторные присваивания: каждая мутация генерирует новую версию идентификатора, а точки схождения ветвей получают phi-инструкции.
Компилятор строит интервалы жизни значений (Live Ranges), определяя, какие переменные разделяют общие мутационные контексты.
Инструкции с пересекающимися мутациями и зависимостями объединяются в реактивные блоки с единым набором входных сигналов и выходных значений.
Реактивные блоки трансформируются обратно в JavaScript с вызовами рантайм-кэша react/compiler-runtime, полностью заменяя ручные useMemo.
С какой фундаментальной целью React Compiler преобразует исходное дерево AST в форму SSA (Static Single Assignment) с phi-узлами перед анализом зависимостей?
В корпоративных финансовых терминалах десятки графиков и таблиц получают единый поток котировок через WebSocket. Без компилятора даже тривиальное изменение счетчика тиков приводило к ре-рендеру сотен дочерних строк из-за пересоздания инлайн-стилей style={{ width: percent + '%' }} и обработчиков onClick={() => selectRow(id)}. React Compiler автоматически помещает каждый JSX-элемент и его пропсы в изолированный реактивный скоуп, локализуя DOM-патчи исключительно в изменившихся ячейках.
В масштабных монорепозиториях (более 500 000 строк кода) разработчики компонентов дизайн-системы часто злоупотребляют useCallback «на всякий случай» или, напротив, забывают обернуть тяжелые функции форматирования чисел в useMemo. Компилятор устраняет человеческий фактор: он делает ручные оптимизации ненужными, удаляя сотни килобайт бойлерплейта и предотвращая регрессии производительности при рефакторинге.
В визуальных редакторах блок-схем и векторных редакторах геометрия связей рассчитывается на основе координат вершин. React Compiler формирует реактивные скоупы вокруг математических вычислений матриц трансформаций. Если координаты узла A не изменились, результат перемножения матриц извлекается из _c[index] без выполнения математических циклов.
Рассмотрим компонент, вычисляющий отфильтрованный список и форматирующий заголовок:
export function AnalyticsWidget({ items, threshold, title }: { items: number[]; threshold: number; title: string }) {
const filtered = items.filter(x => x > threshold);
const total = filtered.reduce((acc, curr) => acc + curr, 0);
return (
<div className="widget-card">
<h3>{title}</h3>
<span>Итого: {total}</span>
</div>
);
}На этапе компиляции AST разбирается на базовые блоки (Basic Blocks). Каждый блок содержит линейную последовательность инструкций и завершается терминатором (goto, if-else, return).
bb0 (Entry):
[1] items = LoadParam 0
[2] threshold = LoadParam 1
[3] title = LoadParam 2
[4] filterFn = CreateFunctionExpression (x => x > threshold)
[5] filtered_0 = CallMethod items.filter(filterFn)
[6] reduceFn = CreateFunctionExpression ((acc, curr) => acc + curr)
[7] total_0 = CallMethod filtered_0.reduce(reduceFn, 0)
[8] jsxTitle = CreateJSX <h3>, [title]
[9] jsxSpanText = BinaryAdd "Итого: ", total_0
[10] jsxSpan = CreateJSX <span>, [jsxSpanText]
[11] jsxRoot = CreateJSX <div className="widget-card">, [jsxTitle, jsxSpan]
Terminal: Return jsxRootКомпилятор анализирует граф зависимостей инструкций. Он выявляет:
filtered_0 и total_0 зависит исключительно от items и threshold.jsxTitle зависит только от title.jsxSpan зависит только от total_0.Вместо одного массивного блока мемоизации компилятор нарезает вычисления на независимые Reactive Scopes:
Построение графа распространения мутаций. Определение интервалов жизни total_0 и filtered_0.
Значения, мутирующие совместно, объединяются в один неделимый скоуп.
Ограничение инструкций [4..7] в ReactiveScope $0, инструкций [8] в ReactiveScope $1.
Каждый скоуп получает четкий контракт входных зависимостей (inputs) и экспортируемых значений (outputs).
Вычисление общего числа слотов кэширования N и генерация защитных условий проверки равенства.
Используется тождественное сравнение Object.is для каждого входного значения скоупа.
_c(N)#Результат компиляции преобразуется в плоский массив кэша. Хук _c (сокращение от useMemoCache) резервирует непрерывный массив в оптоволокне Fiber текущего компонента.
import { c as _c } from "react/compiler-runtime";
export function AnalyticsWidget(t0) {
const $ = _c(8);
const { items, threshold, title } = t0;
let total;
if ($[0] !== items || $[1] !== threshold) {
const filtered = items.filter(x => x > threshold);
total = filtered.reduce((acc, curr) => acc + curr, 0);
$[0] = items;
$[1] = threshold;
$[2] = total;
} else {
total = $[2];
}
let t1;
if ($[3] !== title) {
t1 = <h3>{title}</h3>;
$[3] = title;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== total) {
t2 = <span>Итого: {total}</span>;
$[5] = total;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== t1 || $[8] !== t2) {
t3 = (
<div className="widget-card">
{t1}
{t2}
</div>
);
$[7] = t1;
$[8] = t2;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}Сравните структуру кэша: вместо создания отдельных объектов замыканий, функций-оберток и проверок массивов для каждого useMemo, рантайм React Compiler обращается к плоскому индексированному массиву $[index] по фиксированному числовому смещению. Это дает нулевой оверхед на аллокацию памяти и максимальную дружелюбность к механизму Inline Cache движка V8.
Что предпримет React Compiler, если встретит внутри компонента явное нарушение правил React (например, мутацию глобального объекта или вызов хука внутри тернарного оператора)?
React Compiler анализирует граф потоков данных, предполагая, что входные аргументы функции (пропсы) являются иммутабельными значениями. Если компонент напрямую мутирует переданный массив или объект (например, props.data.sort()), компилятор ошибочно посчитает, что ссылка осталась прежней, и не инвалидирует зависимый кэш-слот.
// ❌ ПЛОХО: мутация входного пропса ломает граф зависимостей компилятора
export function OrderList({ orders }: { orders: Order[] }) {
// sort() мутирует исходный массив по ссылке in-place!
const sorted = orders.sort((a, b) => a.timestamp - b.timestamp);
return <Table data={sorted} />;
}
// ✅ ПРАВИЛЬНО: иммутабельное создание копии перед мутацией
export function OrderList({ orders }: { orders: Order[] }) {
const sorted = orders.toSorted((a, b) => a.timestamp - b.timestamp);
return <Table data={sorted} />;
}Если компонент читает мутабельное значение, объявленное за пределами React-дерева (например, глобальный let globalCounter = 0), компилятор не сможет отследить изменение этого значения через параметры функции. Скоуп будет возвращать устаревший кэш-слот до тех пор, пока не изменятся явные пропсы или хуки компонента. Используйте только useSyncExternalStore или локальный стейт для синхронизации с внешними мутациями.
Разработчики иногда добавляют директиву "use no memo" в начало файла, сталкиваясь с неожиданным поведением кэширования. В 99% случаев проблема заключается в нарушении чистоты рендера (side-effects during render). Отключение компилятора маскирует архитектурный баг вместо его устранения.