Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Правила чистоты для React Compiler: как подготовить кодовую базу к автоматической мемоизации

Тимофей Ищенко

Коротко: Руководство по аудиту и подготовке кодовой базы 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 работает принципиально иначе:

  1. Анализирует абстрактное синтаксическое дерево (AST) компонентов и пользовательских хуков во время сборки (через плагины для Babel, Vite, Metro, Rsbuild или интеграцию с SWC в Next.js).
  2. Выделяет независимые блоки вычислений и структуры JSX.
  3. Генерирует код с низкоуровневой мемоизацией отдельных блоков и переменных, исключая каскадные ререндеры дочерних компонентов даже при передаче инлайн-функций или новых объектных литералов.

Что происходит при нарушении 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) в специфических сценариях:

  1. Стабилизация зависимостей для внешних библиотек: когда ссылка на объект или функцию передается в сторонний non-React SDK или низкоуровневый useEffect, где критически важно предотвратить повторный вызов эффекта.
  2. Тяжелые вычислительные алгоритмы: если требуется гарантировать строгое сохранение кэша для ресурсоемких математических операций или трансформаций больших объемов данных.

Инструменты проверки готовности кодовой базы

Подготовку проекта следует начинать не с изменения сборочных пайплайнов, а с внедрения автоматизированного статического анализа.

┌────────────────────────────────────────────────────────┐
│               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

Перед включением компилятора в основном пайплайне сборки убедитесь, что выполнены ключевые шаги аудита:

  1. Включены строгие линтеры: В проект добавлены правила eslint-plugin-react-hooks и eslint-plugin-react-compiler, все предупреждения устранены.
  2. Проверена чистота рендеринга: В теле компонентов отсутствуют прямые мутации массивов (sort, splice) и объектов из props / state.
  3. Изолированы сайд-эффекты: Все обращения к API браузера, таймеры, аналитика и запись в глобальные объекты вынесены в useEffect или обработчики пользовательских событий.
  4. Рендер детерминирован: Случайные значения (Math.random()) и временные метки (Date.now()) генерируются внутри эффектов, событий или передаются сверху через пропсы.
  5. Настроен поэтапный запуск: Определен пилотный модуль или набор UI-компонентов, на которых компилятор тестируется перед раскаткой на весь проект.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также