Коротко: Руководство по аудиту и подготовке кодовой базы React к внедрению React Compiler: соблюдение Rules of React, устранение мутаций и настройка линтеров.
Ручная оптимизация производительности в React долгое время оставалась источником ментальной нагрузки и скрытого техдолга. Разработчикам приходилось вручную оборачивать вычисления в useMemo, функции в useCallback, а компоненты — в React.memo, следя за целостностью массивов зависимостей. Малейшая ошибка приводила либо к потере оптимизации из-за нестабильных ссылок, либо к устаревшим замыканиям (stale closures).
React Compiler решает эту проблему на уровне этапа сборки (build-time), автоматически анализируя граф зависимостей и расставляя мемоизацию на гранулярном уровне. Однако компилятор не исправляет архитектурные ошибки: он опирается на строгую модель чистоты функций. Если кодовая база нарушает базовые соглашения React (Rules of React), компилятор либо пропустит оптимизацию проблемного участка, либо поведение интерфейса станет непредсказуемым.
Подготовка кодовой базы к компилятору — это прежде всего аудит чистоты рендеринга, устранение мутаций и структурирование побочных эффектов.
Как работает React Compiler и почему чистота кода критична
Build-time анализ вместо ручных хуков
Традиционная мемоизация в React происходит в рантайме. Когда компонент перерисовывается, хук useMemo сравнивает текущие значения зависимостей с предыдущими по ссылке (Object.is). Если хотя бы одна ссылка изменилась, вычисление запускается заново, создавая новые объекты для всех дочерних элементов.
React Compiler работает принципиально иначе:
- Анализирует абстрактное синтаксическое дерево (AST) компонентов и пользовательских хуков во время сборки (через плагины для Babel, Vite, Metro, Rsbuild или интеграцию с SWC в Next.js).
- Выделяет независимые блоки вычислений и структуры JSX.
- Генерирует код с низкоуровневой мемоизацией отдельных блоков и переменных, исключая каскадные ререндеры дочерних компонентов даже при передаче инлайн-функций или новых объектных литералов.
Что происходит при нарушении Rules of React
Компилятор анализирует код консервативно. Если статическому анализатору не удается гарантировать безопасность оптимизации из-за потенциальной мутации или недетерминированного поведения, он применяет безопасный откат (bailout): компонент остается в исходном виде и рендерится по стандартным правилам рантайма без мемоизации.
В худшем сценарии — если мутация была скрыта за сложной цепочкой ссылок, которую анализатор посчитал безопасной — мемоизация зафиксирует мутированный объект, что приведет к рассинхронизации UI и фактического состояния приложения.
Фундаментальные правила чистоты (Rules of React) под микроскопом
Чтобы компилятор работал корректно и с максимальной отдачей, код компонентов должен соответствовать трем фундаментальным принципам.
1. Идемпотентность функции компонента
Компонент React должен вести себя как чистая функция: при одинаковом наборе входных параметров (props, state, context) он обязан возвращать идентичный результат в виде JSX.
Процесс рендеринга не должен влиять на внешнее окружение приложения. Если вызов функции меняет что-либо за пределами области видимости самого рендера, функция перестает быть чистой.
// ❌ Нарушение: сайд-эффект в теле функции рендеринга
let renderCount = 0;
export const CounterBad = () => {
renderCount++; // Мутация глобальной переменной
return <div>Рендеров: {renderCount}</div>;
};
// ✅ Корректно: сайд-эффекты вынесены в эффекты или события
export const CounterGood = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// Безопасная синхронизация с внешними системами
document.title = `Кликов: ${count}`;
}, [count]);
return <button onClick={() => setCount(c => c + 1)}>Клик: {count}</button>;
};
2. Мутации входных данных (props, state, context)
Входные параметры в React доступны исключительно только для чтения. Прямое изменение свойств объектов, переданных через props или полученных из хуков состояния, ломает механизм сравнения ссылок и сбивает внутренний граф зависимостей компилятора.
3. Недетерминированные вызовы во время рендера
Использование генераторов случайных чисел, обращений к текущему времени или чтение динамических параметров окружения прямо в теле компонента делает результат рендера непредсказуемым для оптимизатора.
// ❌ Нарушение: результат зависит от момента системного времени
export const SessionBad = () => {
const expiresAt = Date.now() + 3600 * 1000;
return <div>Сессия истекает: {expiresAt}</div>;
};
// ✅ Корректно: фиксация значения в состоянии или передача через пропсы
export const SessionGood = ({ initialTimestamp }: { initialTimestamp: number }) => {
const [expiresAt] = useState(() => initialTimestamp + 3600 * 1000);
return <div>Сессия истекает: {expiresAt}</div>;
};
Пошаговый аудит и рефакторинг: типичные антипаттерны
При подготовке существующего проекта к переходу на React Compiler необходимо локализовать и устранить распространенные антипаттерны мутаций и побочных эффектов.
Антипаттерн 1. Мутация массивов методами sort, reverse, splice
Методы JavaScript Array.prototype.sort(), reverse() и splice() мутируют исходный массив in-place. Если применить их напрямую к пропсам или стейту, компилятор не сможет корректно отследить изменение данных.
// ❌ Нарушение: мутация входящего массива
export const UserListBad = ({ users }: { users: User[] }) => {
const sorted = users.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{sorted.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
};
// ✅ Корректно: создание копии перед сортировкой (или toSorted в ES2023)
export const UserListGood = ({ users }: { users: User[] }) => {
const sorted = [...users].sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{sorted.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
};
Антипаттерн 2. Запись в глобальные объекты и синглтоны
Чтение и модификация глобальных хранилищ, кэшей или свойств объекта window во время выполнения функции рендеринга приводят к трудноуловимым багам при конкурентном рендеринге (Concurrent Rendering) и оптимизации компилятором.
// ❌ Нарушение: запись во внешний кэш во время вычисления JSX
const metricsCache: Record<string, number> = {};
export const MetricCardBad = ({ id, value }: { id: string; value: number }) => {
metricsCache[id] = value; // Сайд-эффект в рендере
return <div>{id}: {value}</div>;
};
// ✅ Корректно: перенос записи в useEffect
export const MetricCardGood = ({ id, value }: { id: string; value: number }) => {
useEffect(() => {
metricsCache[id] = value;
}, [id, value]);
return <div>{id}: {value}</div>;
};
Антипаттерн 3. Локальные мутации объектов после передачи в дочерние ветки
Если объект создается внутри компонента, передается в качестве пропа дочернему элементу, а затем модифицируется ниже по коду, граф зависимостей становится неоднозначным.
// ❌ Нарушение: модификация объекта после использования в JSX
export const ConfigBad = () => {
const settings = { theme: 'dark' };
const view = <ThemeViewer settings={settings} />;
// Мутация объекта, который уже передан в дерево элементов
(settings as { theme: string; debug?: boolean }).debug = true;
return view;
};
// ✅ Корректно: полная инициализация объекта до передачи
export const ConfigGood = () => {
const settings = {
theme: 'dark',
debug: true,
};
return <ThemeViewer settings={settings} />;
};
Судьба useMemo, useCallback и React.memo
Нужно ли удалять старые хуки перед внедрением компилятора?
Массовое удаление useMemo, useCallback и React.memo перед включением компилятора не требуется. React Compiler спроектирован с учетом обратной совместимости:
- Он распознает существующие хуки мемоизации и сохраняет их поведение.
- В большинстве случаев компилятор выполняет мемоизацию глубже и точнее, чем это было сделано вручную.
- Рефакторинг и удаление бойлерплейта мемоизации рекомендуется проводить поэтапно, избавляясь от лишнего кода в рамках плановой работы с техдолгом.
Случаи-исключения: когда ручной контроль всё еще оправдан
Хуки useMemo и useCallback остаются доступным инструментом (escape hatch) в специфических сценариях:
- Стабилизация зависимостей для внешних библиотек: когда ссылка на объект или функцию передается в сторонний non-React SDK или низкоуровневый
useEffect, где критически важно предотвратить повторный вызов эффекта. - Тяжелые вычислительные алгоритмы: если требуется гарантировать строгое сохранение кэша для ресурсоемких математических операций или трансформаций больших объемов данных.
Инструменты проверки готовности кодовой базы
Подготовку проекта следует начинать не с изменения сборочных пайплайнов, а с внедрения автоматизированного статического анализа.
┌────────────────────────────────────────────────────────┐
│ 1. Анализ кодовой базы │
│ Подключение eslint-plugin-react-compiler │
│ (Аудит нарушений чистоты функций и хуков) │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 2. Устранение нарушений │
│ Рефакторинг мутаций, сайд-эффектов и недетерминизма │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 3. Поэтапное включение │
│ Включение компилятора на уровне отдельных директорий │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 4. Полная интеграция │
│ Сборка всего проекта (Vite / Next.js / Rsbuild) │
└───────────────────────────┘
Настройка ESLint
Официальный плагин линтера эмулирует проверки компилятора и подсвечивает опасные конструкции:
npm install -D eslint-plugin-react-compiler
Конфигурация в .eslintrc.json:
{
"plugins": ["react-compiler"],
"rules": {
"react-compiler/react-compiler": "error"
}
}
Этот плагин работает в связке с eslint-plugin-react-hooks и блокирует:
- Мутации переменных, полученных из аргументов или вызовов хуков;
- Вызовы сайд-эффектов в теле рендера;
- Использование нестабильных значений внутри хуков без явных зависимостей.
Стратегия поэтапного roll-out
Для масштабных проектов безопаснее использовать поэтапное включение компилятора через конфигурацию компилятора (например, в Babel или Vite plugin), ограничивая область его работы определенными директориями:
// babel.config.js / vite.config.js
const ReactCompilerConfig = {
sources: (filename) => {
// Включаем компилятор только для изолированных модулей
return filename.includes('src/components/ui/') || filename.includes('src/features/dashboard/');
},
};
module.exports = {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig],
],
};
Такой подход позволяет локализовать тестирование, проверить отсутствие регрессий на отдельных страницах и планомерно расширять зону покрытия.
FAQ (Часто задаваемые вопросы)
1. Нужно ли полностью переписывать проект для перехода на React Compiler?
Нет. Компилятор работает со стандартным JavaScript/TypeScript кодом. Если в компоненте обнаруживается сложная динамическая логика или нарушение правил чистоты, компилятор безопасно пропускает его оптимизацию, сохраняя исходное поведение React-компонента.
2. Стоит ли массово удалять все useMemo и useCallback из проекта?
Специально тратить ресурсы на их тотальное удаление перед внедрением не нужно. Компилятор работает поверх существующего кода. Удаление ручной мемоизации имеет смысл проводить постепенно в процессе текущей разработки, сокращая визуальный шум и сложность поддержки.
3. Заменяет ли React Compiler управление глобальным состоянием (Redux, Zustand)?
Нет. Компилятор оптимизирует процесс рендеринга компонентов и устраняет лишние перерисовки на уровне дерева React. Он не управляет бизнес-логикой, потоками данных и клиентским состоянием приложения.
4. Какие сборщики и фреймворки поддерживают React Compiler?
Компилятор официально поддерживает интеграцию через Babel-плагин, сборщики Vite, Metro, Rsbuild, а также поддерживается в Next.js (начиная с версий 15+ через компиляторную цепочку SWC).
5. Поможет ли компилятор, если в приложении медленные сетевые запросы или неоптимальный DOM?
Нет. React Compiler оптимизирует исключительно этап вычислений JavaScript внутри компонентов и синхронизацию виртуального дерева. Медленные сетевые ответы, тяжелая верстка или layout thrashing в браузере требуют профилирования и отдельных архитектурных решений.
Чеклист готовности проекта к React Compiler
Перед включением компилятора в основном пайплайне сборки убедитесь, что выполнены ключевые шаги аудита:
- Включены строгие линтеры: В проект добавлены правила
eslint-plugin-react-hooksиeslint-plugin-react-compiler, все предупреждения устранены. - Проверена чистота рендеринга: В теле компонентов отсутствуют прямые мутации массивов (
sort,splice) и объектов изprops/state. - Изолированы сайд-эффекты: Все обращения к API браузера, таймеры, аналитика и запись в глобальные объекты вынесены в
useEffectили обработчики пользовательских событий. - Рендер детерминирован: Случайные значения (
Math.random()) и временные метки (Date.now()) генерируются внутри эффектов, событий или передаются сверху через пропсы. - Настроен поэтапный запуск: Определен пилотный модуль или набор UI-компонентов, на которых компилятор тестируется перед раскаткой на весь проект.




.svg.webp)





