Тихие сбои React Compiler и как их исправить
- суббота, 14 февраля 2026 г. в 00:00:07
Полагаться на 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-колбэк для инпута постоянно «колбасило».
Я понял, что мне нужен способ узнавать, когда компиляция не удалась, и что в таких случаях сборка должна падать.
Покопавшись в исходниках 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. Если у вас есть критичные участки, где компоненты должны компилироваться (и тем самым быть нормально мемоизированы), настройте 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 и графические библиотеки: визуализация данных». Записаться