Тихие сбои React Compiler и как их исправить. ESLint.. ESLint. JavaScript.. ESLint. JavaScript. React Compiler.. ESLint. JavaScript. React Compiler. ReactJS.. ESLint. JavaScript. React Compiler. ReactJS. useCallback.. ESLint. JavaScript. React Compiler. ReactJS. useCallback. useMemo.. ESLint. JavaScript. React Compiler. ReactJS. useCallback. useMemo. Блог компании OTUS.. ESLint. JavaScript. React Compiler. ReactJS. useCallback. useMemo. Блог компании OTUS. мемоизация.. ESLint. JavaScript. React Compiler. ReactJS. useCallback. useMemo. Блог компании OTUS. мемоизация. производительность React.. ESLint. JavaScript. React Compiler. ReactJS. useCallback. useMemo. Блог компании OTUS. мемоизация. производительность React. ререндеры.

Полагаться на 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,
      };
    }
Тихие сбои React Compiler и как их исправить - 1

Имя правила — 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);
    }
});
Тихие сбои React Compiler и как их исправить - 2

приводит к такой 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();
Тихие сбои React Compiler и как их исправить - 3

Вот как это настроить:

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 и как их исправить - 4

​​Включите это — и удивитесь, сколько компонентов «падают». Пока я не выучил паттерны, которые React Compiler пока не поддерживает, у меня было больше сотни компонентов, которые не удавалось скомпилировать.

Что ломает компилятор

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

Такой код ломает компиляцию:

function MyComponent({ value }) {
    // Если value не задан, берём значение из состояния
    value = value ?? someStateValue;

    // Или нормализуем значение
    value = normalizeValue(value);

    // Используем value...
}
Тихие сбои React Compiler и как их исправить - 5

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

function MyComponent({ value: valueFromProps }) {
    const value = valueFromProps ?? someStateValue;
    // Используем value...
}
Тихие сбои React Compiler и как их исправить - 6

Ещё одно ограничение: блоки 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}`);
}
Тихие сбои React Compiler и как их исправить - 7

Формально это всё временные ограничения — на это намекают и название («todo»), и описание («Нереализованные возможности») у lint-правила. Я уверен, что большинство, если не все, из них со временем будут устранены. Хотя стоит упомянуть, что перед todo-ошибкой Support ThrowStatement inside of try/catch в коде стоит такой комментарий:

/*
 * ПРИМЕЧАНИЕ: мы могли бы это поддержать, но использование конструкции `throw` внутри `try/catch` — это использование исключений
* для управления потоком выполнения и обычно считается антипаттерном. Мы, вероятно, можем
* просто не поддерживать этот шаблон, если только это действительно не станет необходимым по какой-либо причине.

*/
Тихие сбои React Compiler и как их исправить - 8

Так что, возможно, не все? Иронично, но ошибку Support value blocks… я обходил, полагаясь на небезопасный доступ к свойствам внутри блока try, фактически неявно рассчитывая на выброшенное исключение как на механизм управления потоком.

Как бы то ни было, пока что я поймал себя на том, что начинаю запоминать эти ограничения — примерно так же, как раньше запоминал «лучшие практики» оптимизации, чтобы не допускать лишних ререндеров в React. Это точно не тот результат, который мне нужен.

Используйте линтинг

Вот почему ESLint-правило настолько ценно: оно избавляет от необходимости держать в голове все эти паттерны. Но в некоторых компонентах используются приёмы, которые я не готов усложнять только ради того, чтобы умилостивить компилятор.

Для таких случаев я явно отключаю это правило:

/* eslint-disable react-hooks/todo */
function NonCriticalPathComponent() {
    // Этот компонент необязательно компилировать, чтобы приложение работало быстро,
    // и я не готов рефакторить логику try/catch
}
Тихие сбои React Compiler и как их исправить - 9

Такой подход даёт мне лучшее из двух миров:

  • Критичные компоненты обязаны компилироваться, иначе сборка падает

  • Некритичные компоненты могут использовать любые приёмы, которые делают код понятнее

  • Я вообще не думаю о мемоизации

Стоит ли использовать React Compiler?

Однозначно да. Особенно если вы делаете интерактивные интерфейсы, где важна производительность. Одна только когнитивная разгрузка этого стоит.

Но заходите с пониманием, что по умолчанию компиляция будет молча откатываться к обычному поведению React. Если у вас есть критичные участки, где компоненты должны компилироваться (и тем самым быть нормально мемоизированы), настройте ESLint-правило и пусть сборка падает. А дальше принимайте осознанные решения, каким компонентам нужна компиляция, а каким нет.

Ограничения временные. Изменение того, как вы строите UI, — надолго.

Тихие сбои React Compiler и как их исправить - 10

Если хотите расти дальше «магии хуков» и понимать, где на самом деле рождаются лишние ререндеры, полезно прокачать базу фронтенда системно. На курсе 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

Источник

Rambler's Top100