Полагаться на React Compiler означает знать, когда он не срабатывает
Я разрабатываю высокоинтерактивные интерфейсы на React с 2017 года: визуальные редакторы, инструменты для дизайна, приложения, где пользователи перетаскивают элементы, меняют свойства в реальном времени и ожидают, что каждое действие будет отзываться так же быстро, как в Figma или Photoshop. Один лишний ререндер может разрушить иллюзию «прямого управления», из-за чего интерфейс начинает тормозить и раздражать.
Восемь лет я приучал себя думать через useMemo и useCallback. В голове сформировался внутренний «компилятор», который подсвечивал любое значение, способное вызвать лишние ререндеры. Это стало второй натурой.
А потом React Compiler стёр всё это за считанные недели.
Проблема ручной мемоизации
Ручная мемоизация не просто утомляет, это когнитивный налог на каждый компонент, который вы пишете. Приходится постоянно держать в уме:
-
Нужен ли этому обработчику событий
useCallback? -
Нужно ли вынести это в отдельный файл
ComponentItem.tsxтолько ради стабилизации пропсов в.map(...)? -
Нужно ли «поднять» этот объект стилей или обернуть его в
useMemo? -
Не вызовет ли этот провайдер контекста лишние ререндеры ниже по дереву компонентов?
Ошиблись — либо убили производительность, либо захламили кодовую базу преждевременными оптимизациями. Сделали всё правильно — всё равно потратили когнитивные ресурсы на обвязку вместо продукта.
React Compiler убирает это полностью. Я использую его в продакшене уже полгода. Он стал одним из тех незаменимых инструментов, без которых я больше не представляю работу — как «горячая» замена модулей или автоматические форматтеры кода.
Я больше не думаю о мемоизации. Колеи, накатанные годами привычки, сгладились.
Проблема «тихих» сбоев
Это хорошие новости. Но вот что меня удивило: когда React Compiler не может скомпилировать компонент, он молча откатывается к обычному поведению React.
Философия понятна. Компилятор существует, чтобы делать ваш код лучше, а не чтобы он вообще работал. Если он не может что-то оптимизировать, он откатывается к стандартному поведению React. Приложение продолжает работать.
Но теперь, когда я больше ничего не мемоизирую вручную, стало очевидно, что ручная мемоизация — это форма технического долга. Это лишняя сложность, из-за которой логику компонента труднее читать, а массивы зависимостей превращаются в обузу для поддержки. А в мире с React Compiler это ещё и преждевременная оптимизация, корень всех зол. Я не хочу видеть её в своей кодовой базе.
А значит, теперь я завишу от того, что компилятор успешно обработает некоторые компоненты, особенно те, которые обеспечивают высокочастотные взаимодействия или управляют «тяжёлыми» провайдерами контекста. Если он тихо «провалится» на них, пользовательский опыт ухудшится и даже может полностью сломать некоторые UX-сценарии. Я обнаружил это на анимации «печатной машинки» на главной странице.
Мы отрефакторили её, заменив SSE на обычный fetch, и добавили try/catch с оператором нулевого слияния (??) внутри блока try. Из-за этого код оказался несовместим с React Compiler, и возник странный цикл ререндеров, где ref-колбэк для инпута постоянно «колбасило».
Я понял, что мне нужен способ узнавать, когда компиляция не удалась, и что в таких случаях сборка должна падать.
Недокументированное правило ESLint
Покопавшись в исходниках react-compiler-babel-plugin, я нашёл решение:
case ErrorCategory.Todo: {
return {
category,
severity: ErrorSeverity.Hint,
name: 'todo',
description: 'Unimplemented features',
preset: LintRulePreset.Off,
};
}
Имя правила — todo, поэтому в большинстве конфигураций (если только вы не настроили eslint-plugin-react-hooks с другим именем) полное имя правила будет react-hooks/todo (если плагин подключён под неймспейсом react-hooks). Я нигде не нашёл его в документации (например, в этих ESLint-правилах для React Compiler), но если включить его как ошибку, сборка будет падать на любом компоненте, где используется синтаксис, который компилятор пока не умеет обрабатывать.
После этого в примере с главной страницей такой код:
const handleGeneration = useEffectEvent(async (fetchURL: string) => {
try {
const response = await fetch(fetchURL);
const data = (await response.json()) as { response?: string };
const finalResult = (data.response ?? '').trim();
const prompt = getPromptFromResponse(finalResult);
if (!prompt) {
handleError();
} else {
setPromptSuggestion(prompt);
setEventSourceURL('');
}
} catch (error) {
logError('Home fetch error', error);
}
});
приводит к такой lint-ошибке:
/outlyne/app/components/Home.tsx
86:34 error Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
/outlyne/app/components/Home.tsx:86:34
84 | const response = await fetch(fetchURL);
85 | const data = (await response.json()) as { response?: string };
> 86 | const finalResult = (data.response ?? '').trim();
| ^^^^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
87 | const prompt = getPromptFromResponse(finalResult);
88 | if (!prompt) {
89 | handleError();
Вот как это настроить:
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: { 'react-hooks': reactHooks },
// ...
rules: {
// разворачиваем пресет, чтобы не перезаписать его конкретными правилами ниже
...reactHooks.configs.recommended.rules,
// https://github.com/facebook/react/blob/3640f38/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts#L807-L1111
'react-hooks/todo': 'error',
// другие полезные правила:
'react-hooks/capitalized-calls': 'error', // не вызывайте функции с заглавной буквы (нужно использовать JSX)
'react-hooks/hooks': 'error', // во многом пере-реализует некомпиляторное правило «rules-of-hooks»
'react-hooks/rule-suppression': 'error', // проверяет корректность подавления других правил
'react-hooks/syntax': 'error', // проверяет некорректный синтаксис
'react-hooks/unsupported-syntax': 'error', // по умолчанию `warn`, используйте `error`, чтобы ронять сборку
// ...
},
},
];
Включите это — и удивитесь, сколько компонентов «падают». Пока я не выучил паттерны, которые React Compiler пока не поддерживает, у меня было больше сотни компонентов, которые не удавалось скомпилировать.
Что ломает компилятор
Самый частый неподдерживаемый паттерн, с которым я столкнулся: деструктурировать пропсы и затем мутировать их.
Такой код ломает компиляцию:
function MyComponent({ value }) {
// Если value не задан, берём значение из состояния
value = value ?? someStateValue;
// Или нормализуем значение
value = normalizeValue(value);
// Используем value...
}
К счастью, исправление простое и, пожалуй, даже лучше: просто создайте новую переменную, чтобы не мутировать деструктурированные пропсы:
function MyComponent({ value: valueFromProps }) {
const value = valueFromProps ?? someStateValue;
// Используем value...
}
Ещё одно ограничение: блоки try/catch с любой нетривиальной логикой. Если в компоненте есть асинхронная работа с try/catch, нельзя использовать:
-
Условия внутри блока
tryилиcatch -
Тернарные операторы, опциональную цепочку илиили оператор нулевого слияния (??)
-
Оператор
throw
Часть про «никаких условий» — это настоящая боль. Чаще всего, когда у меня есть компонент, который делает что-то, что может выбросить исключение, у меня есть какая-то условная логика либо в блоке try, либо в catch.
try {
const response = await fetch(url);
if (response.ok) {
// Ломает компиляцию
setResponse(await response.json());
} else {
setError(`Error ${response.status}`);
}
} catch (error) {
setError(`${error}`);
}
Формально это всё временные ограничения — на это намекают и название («todo»), и описание («Нереализованные возможности») у lint-правила. Я уверен, что большинство, если не все, из них со временем будут устранены. Хотя стоит упомянуть, что перед todo-ошибкой Support ThrowStatement inside of try/catch в коде стоит такой комментарий:
/*
* ПРИМЕЧАНИЕ: мы могли бы это поддержать, но использование конструкции `throw` внутри `try/catch` — это использование исключений
* для управления потоком выполнения и обычно считается антипаттерном. Мы, вероятно, можем
* просто не поддерживать этот шаблон, если только это действительно не станет необходимым по какой-либо причине.
*/
Так что, возможно, не все? Иронично, но ошибку Support value blocks… я обходил, полагаясь на небезопасный доступ к свойствам внутри блока try, фактически неявно рассчитывая на выброшенное исключение как на механизм управления потоком.
Как бы то ни было, пока что я поймал себя на том, что начинаю запоминать эти ограничения — примерно так же, как раньше запоминал «лучшие практики» оптимизации, чтобы не допускать лишних ререндеров в React. Это точно не тот результат, который мне нужен.
Используйте линтинг
Вот почему ESLint-правило настолько ценно: оно избавляет от необходимости держать в голове все эти паттерны. Но в некоторых компонентах используются приёмы, которые я не готов усложнять только ради того, чтобы умилостивить компилятор.
Для таких случаев я явно отключаю это правило:
/* eslint-disable react-hooks/todo */
function NonCriticalPathComponent() {
// Этот компонент необязательно компилировать, чтобы приложение работало быстро,
// и я не готов рефакторить логику try/catch
}
Такой подход даёт мне лучшее из двух миров:
-
Критичные компоненты обязаны компилироваться, иначе сборка падает
-
Некритичные компоненты могут использовать любые приёмы, которые делают код понятнее
-
Я вообще не думаю о мемоизации
Стоит ли использовать React Compiler?
Однозначно да. Особенно если вы делаете интерактивные интерфейсы, где важна производительность. Одна только когнитивная разгрузка этого стоит.
Но заходите с пониманием, что по умолчанию компиляция будет молча откатываться к обычному поведению React. Если у вас есть критичные участки, где компоненты должны компилироваться (и тем самым быть нормально мемоизированы), настройте ESLint-правило и пусть сборка падает. А дальше принимайте осознанные решения, каким компонентам нужна компиляция, а каким нет.
Ограничения временные. Изменение того, как вы строите UI, — надолго.

Если хотите расти дальше «магии хуков» и понимать, где на самом деле рождаются лишние ререндеры, полезно прокачать базу фронтенда системно. На курсе React.js Developer разбирают production-SPA: TypeScript, Redux (Saga/Thunk), тесты, GraphQL и сборку (Webpack/Babel). Пройдите вступительный тест, чтобы узнать, подойдет ли вам программа курса.
А чтобы узнать больше о формате обучения и задать вопросы экспертам, приходите на бесплатные демо-уроки:
-
17 февраля 20:00. «Custom Hooks в React — как выносить логику и переиспользовать код». Записаться
-
5 марта 20:00. «Как создавать реальные React-приложения: от компонента до архитектуры». Записаться
-
19 марта 20:00. «React и графические библиотеки: визуализация данных». Записаться
Автор: kmoseenk


