open atlas
↑ К треку
React с нуля до senior RCT · 08 · 04

React Compiler честно: автоматический memo с молчаливыми отказами

React Compiler мемоизирует на этапе сборки, доказывая соблюдение Rules of React. Где доказать нельзя — молча отступает: код остаётся немемоизированным. Контракт — eslint-плагин, ручной memo сохраняет узкие задачи, а выигрыш меряется в Profiler, а не на веру.

RCT Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Платформенная команда включила React Compiler на продукте в 400 компонентов, объявила в инженерном канале «ручная мемоизация теперь легаси» и открыла PR-зачистку, удаляющую useMemo и useCallback по всей кодовой базе. Через две недели загорелась очередь поддержки: таблица истории заказов — самый тяжёлый экран — стала заметно медленнее. Profiler показывал, что она перерисовывается сверху донизу на каждое обновление родителя, без какой-либо мемоизации. В компоненте была сортировка, мутирующая массив, пришедший через props, — rows.sort() в рендере, нарушение Rules of React, годами сидевшее безвредно. Компилятор его увидел, не смог доказать безопасность мемоизации и сделал то, что делает всегда с кодом, который не может верифицировать: пропустил весь компонент. Ни ошибки, ни предупреждения сборки в их конфигурации, ни флага в рантайме — на выходе просто оригинальный, немемоизированный код. Рукописный useMemo, который раньше прикрывал эту цену, удалили в зачистке, и экран деградировал, пока дашборд рапортовал «compiler: enabled». Нашли это только потому, что кто-то прогнал eslint-плагин и увидел нарушение, от которого компилятор молча ушёл. Компилятор реален, и выигрыши реальны — но это оптимизатор с правом вето, и вето он применяет молча.

Что он автоматизирует: мемоизация как проход компилятора

React Compiler — сборочная трансформация (Babel-плагин, встраиваемый в конвейеры Vite/Next/Metro), переписывающая компоненты и хуки так, чтобы они были мемоизированы по умолчанию. Он статически анализирует каждую функцию, отслеживает, какие значения от каких входов зависят, и кэширует промежуточные результаты — значения, JSX-поддеревья, идентичности функций, — чтобы ререндер пересчитывал только то, до чего реально дотягиваются изменившиеся входы. Три честных уточнения о масштабе:

  • Он гранулярнее вашего useMemo. Рукописная мемоизация работает с той детализацией, на которую у вас хватило усердия; компилятор мемоизирует на уровне выражений, включая сами JSX-элементы, так что неизменившееся поддерево пропускается, даже когда компонент перевыполняется. Он регулярно обыгрывает приличную ручную мемоизацию одной только широтой покрытия.
  • Он не меняет вашу семантику — там, где применяется. Скомпилированный выход обязан вести себя идентично исходнику для кода, следующего правилам. Вся лицензия компилятора на переписывание — допущение, что компоненты чисты, props и state не мутируются, а хуки вызываются безусловно: Rules of React, теперь несущая конструкция.
  • Он не делает медленный код быстрым. Он убирает стоимость лишних ререндеров. Дорогое вычисление, которое действительно должно выполниться на новых входах, выполняется как раньше; график, перерисовывающийся потому, что данные реально изменились, перерисовывается. Команды, измеряющие «компилятор улучшил p95 времени коммита на 30%», измеряют избегнутые ререндеры, а не ускоренные.

Bailout: молчаливый по замыслу

Когда компилятор не может доказать, что компонент следует правилам, он не выпускает полуоптимизацию и не валит сборку — он целиком пропускает компонент, оставляя оригинальный код нетронутым. Такой отказ от оптимизации называется bailout (буквально «выход из игры»). Триггеры, мимо которых он не может пройти: мутация props, state или производных от них (rows.sort(), obj.x = y на не-локальном объекте); чтение или запись ref во время рендера; динамические паттерны доступа, о которых он не может рассуждать; значения, утекающие в замыкания с неясным временем жизни. Разница на выходе в рантайме невидима: компонент работает, рендерится корректно — и просто перерисовывается так же часто, как до существования компилятора.

Эта тишина — осознанное инженерное решение: корректность важнее покрытия; неверная мемоизация портит состояние UI, пропущенная стоит только CPU. Но она порождает режим отказа из пролога: «компилятор включён» — не то же самое, что «кодовая база мемоизирована». Покрытие — свойство, которое нужно наблюдать, а не предполагать. Точек наблюдения две: eslint-плагин во время написания кода и React DevTools в рантайме — скомпилированные компоненты помечаются бейджем «Memo ✨» в панели компонентов, так что выборочная проверка самых тяжёлых экранов говорит, взял ли оптимизатор сделку.

Викторина

После включения компилятора команда удаляет ручные useMemo в тяжёлом компоненте. Экран становится медленнее, но сборка зелёная и приложение работает. Что вероятнее всего произошло?

Eslint-плагин — поверхность контракта

eslint-plugin-react-compiler гоняет тот же анализ, что и компилятор, и сообщает во время написания кода о каждом месте, где компилятор откажется. Это меняет его роль со стайл-чекера на контракт покрытия: нарушение — не придирка, а именованный компонент, который молча останется немемоизированным. Операционный паттерн внедрения:

  1. Прогоните плагин по кодовой базе до включения компилятора — список нарушений и есть ваша карта отказов.
  2. Чините нарушения сперва в горячих путях (там мемоизация стоит денег); холодные принимайте осознанно.
  3. Закрепите в CI ноль новых нарушений. С этого момента компилируемая поверхность только растёт, и «компилятор покрывает этот экран» — проверяемое утверждение, а не ощущение.

Вместе эти три шага делают покрытие проверяемым раньше, чем вы удалите хоть одну строку ручного memo, — и этот порядок важен: деградация из пролога случилась именно потому, что третий шаг был полностью пропущен.

Что по-прежнему делает ручная мемоизация

Компилятор не отправляет ручной инструментарий на пенсию — он сужает его до задач, которые компилятору структурно недоступны:

  • Межмодульные границы, которые вы не компилируете. Библиотека компонентов, опубликованная в npm, поставляет то, что произвела её сборка; компиляторы потребителей не лезут в node_modules. Если компонентам вашей библиотеки нужны гарантии мемоизации, React.memo и стабильные колбэки остаются частью публикуемого контракта.
  • Кэширование вне компонентов. Кэши на уровне модуля, мемоизированные селекторы (reselect и компания), дорогие чистые функции вне рендера — компилятор мемоизирует внутри компонентов и хуков; кэши для слоя данных он не изобретает.
  • Компоненты с отказом — осознанно. Пока нарушение не починено, ручная мемоизация — мост; именно поэтому удалять её следует после проверенного покрытия, а не до.
  • Семантика, а не скорость. useMemo, охраняющий ссылочную идентичность, от которой зависит эффект ниже (deps-массив, значение контекста), кодирует корректность — компилятор сохраняет ваши рассуждения о ней, но не заменяет их.

Зрелость и внедрение, честно

Где он находится: годы продакшена в масштабе Meta до публичной стабильности, первый стабильный релиз — конец 2025-го после долгого RC, крупные фреймворки дают его как флаг конфигурации. Чего это не означает: «щёлкнул и забыл». Честная поза внедрения — инкрементальная: компилятор поддерживает постепенный раскат (режим аннотаций компилирует только компоненты с пометкой "use memo"; Babel-overrides по каталогам ограничивают его пакетом или зоной), и успешные команды неделями гоняют канарейку, сравнивая полевые метрики, прежде чем расширять охват. Код экосистемы с нарушениями правил обычен в старых зависимостях и легаси-углах; каждое — молчаливый отказ, а не падение, так что радиус поражения широкого включения — неравномерность производительности, а не поломка.

И мерьте по-взрослому: запишите React DevTools Profiler на трёх самых тяжёлых взаимодействиях до включения; включите; перезапишите. Ищите меньше компонентов на коммит и более короткие коммиты; проверяйте бейдж Memo на компонентах, которые вам важны; следите за полевым INP на протяжении релизного цикла. Команды публикуют реальные числа — Meta называла ощутимые выигрыши на поверхностях уровня Quest Store, — но ваше число зависит от того, сколько лишних ререндеров у вас было и какую долю дерева компилятор может доказать. База, уже дисциплинированная по memo, увидит единицы процентов; база, не мемоизировавшая никогда, — драматическое удешевление взаимодействий. Вера — не метрика.

Викторина

Команда публикует библиотеку React-компонентов в npm и спрашивает, можно ли с приходом компилятора убрать React.memo из публичных компонентов. Какое решение верное?

Вспомните перед уходом
  1. 01
    Объясните, что React Compiler на самом деле делает с компонентом, что заставляет его отказаться и почему отказ операционно опасен, хотя технически безопасен.
  2. 02
    Сформулируйте плейбук внедрения компилятора в масштабе 400 компонентов, включая то, за чем остаётся ручная мемоизация.
Итог

React Compiler переводит мемоизацию из рукописной дисциплины в проход компилятора: на этапе сборки он по каждому компоненту доказывает соблюдение Rules of React — чистота, отсутствие мутаций props и state, безусловные хуки — и там, где доказательство проходит, переписывает функцию так, чтобы значения, идентичности функций и JSX-поддеревья кэшировались на уровне выражений, гранулярнее заменяемых useMemo. Там, где доказательство падает, он молча уходит: поставляется исходный код, корректный и немемоизированный, в рантайме неотличимый от невключённого компилятора. Это вето-в-тишине — вся операционная история. Деградация истории заказов случилась не потому, что компилятор что-то сломал, а потому, что rows.sort() в рендере заставил его отказаться, а компенсировавший ручной memo удалили в предположении покрытия. Поверхность контракта — eslint-plugin-react-compiler: тот же анализ, вердикты при написании кода, каждое нарушение называет компонент, который компилятор пропустит, — закреплённый в CI, он делает покрытие проверяемым; аудиторский след — бейдж Memo в DevTools, время коммитов в Profiler и полевой INP. Ручная мемоизация сужается до структурно недосягаемого: компоненты публикуемых библиотек (ваши node_modules никто не компилирует), кэши вне компонентов и осознанные мосты над известными отказами. Зрелость реальна — годы продакшена в Meta, стабильность с конца 2025-го, флаги во фреймворках, — а внедрение по-прежнему инкрементально по замыслу: аннотации или каталоги, канарейка с профилями до и после, зачистка только после измеренных выигрышей. Компилятор автоматизирует контракт из урока про мемоизацию — но не отменяет его. Теперь, когда экран стал медленнее после включения компилятора при зелёной сборке и работающем приложении — первым делом откройте панель компонентов в DevTools: если на нужном компоненте нет бейджа Memo, eslint-плагин назовёт нарушение, от которого компилятор молча ушёл.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.