В 2024 году команда React готовит множество нововведений, приуроченных к выходу React 19.

Одним из таких нововведений является React Сompiler — новый JavaScript-компилятор для оптимизации вычислений. Главной целью разработчиков была оптимизация и автоматизация мемоизации в React-приложениях. Если раньше фронтендерам приходилось использовать такие хуки, как useCallback и useMemo, то вскоре React сам возьмёт на себя ответственность за мемоизацию, чтобы избежать повторных вычислений при каждом рендеринге.

Не будем затягивать со вступлением и сразу перейдём к пересказу.

Принцип работы

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

В настоящее время React требует ручного управления оптимизацией рендеров с использованием API useMemo(), useCallback() и memo. Новый компилятор React возьмёт на себя управление этими вещами, автоматически определяя, как и когда изменять состояние и обновлять пользовательский интерфейс.

Это означает, что разработчикам больше не нужно вручную использовать useMemo(), useCallback() и memo, так как React Compiler сам позаботится об оптимизации. Таким образом, необходимость в использовании useMemo(), useCallback() и memo значительно уменьшится, освобождая разработчиков от ручного управления оптимизациями.

Мемoизация в React до React Compiler: Как это было?

До появления React Compiler, мемоизация в React требовала от разработчиков ручного управления оптимизацией вычислений. Давайте вместе разберёмся, как это происходило с использованием хуков useMemo и useCallback.

Когда вы работали над вашими React-приложениями, необходимо было использовать useMemo для мемоизации значений. Представьте, что у вас есть сложный вычислительный процесс, который вы не хотите запускать при каждом рендере компонентов. В таких случаях вы использовали useMemo, чтобы указать React: «Запомни это значение и не пересчитывай его, если зависимости не изменились».

Пример использования:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

В этом примере функция computeExpensiveValue выполняется только тогда, когда a или b изменяются. В остальных случаях React просто возвращает кэшированное значение, что значительно экономит ресурсы.

Для мемоизации функций же использовался useCallback. Представьте, что у вас есть функция, которую вы не хотите пересоздавать при каждом рендере. Вы использовали useCallback, чтобы держать её в памяти, пока зависимости остаются неизменными.

Пример использования:

const memoizedCallback = useCallback(() => {   doSomething(a, b); }, [a, b]);

Эта стратегия означала, что функция doSomething пересоздавалась только тогда, когда a или b изменялись. Это помогало избежать ненужных рендеров компонентов, зависящих от этой функции.

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

Основные улучшения React Сompiler (он же React Forget)

  1. Оптимизация состояний и побочных эффектов

    • React Forget автоматически оптимизирует обработку состояний и побочных эффектов в компонентах. Компилятор анализирует код и определяет, какие части могут быть мемоизированы или оптимизированы для предотвращения лишних перерисовок.

  2. Автоматическое управление мемоизацией

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

  3. Умное определение зависимостей

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

Установка

По словам самой команды React:
"React Compiler — это новый экспериментальный компилятор, исходный код которого мы открыли, чтобы заранее получить отзывы от сообщества. Он все еще имеет недочёты и еще не полностью готов к продакшену."
Однако, у нас есть возможность опробовать компилятор в условиях экспериментального использования уже сейчас. Для этого необходимо воспользоваться React'ом версии 19 RC.
Перед установкой самого компилятора, советую сначала проверить, совместима ли ваша кодовая база с помощью команды:

npx react-compiler-healthcheck@latest

Этот скрипт:

  • Проверит, сколько компонентов можно успешно оптимизировать;

  • Проверит использование <StrictMode>: включение и соблюдение этого параметра означает более высокую вероятность соблюдения правил React;

  • Проверит использование несовместимых библиотек и т.д.

    В качестве примера:

Successfully compiled 8 out of 9 components.                                   
StrictMode usage not found.                                                    
Found no usage of incompatible libraries.

Помимо этого, React Сompiler также поддерживает плагин eslint. Его можно использовать независимо от компилятора, то есть вы можете использовать плагин eslint, даже если вы не используете сам компилятор.

npm install eslint-plugin-react-compiler

Затем добавьте его в свою конфигурацию eslint:

module.exports = {  
	plugins: [  
		'eslint-plugin-react-compiler',  
	],  
	
	rules: {  
		'react-compiler/react-compiler': "error",  
	},  
}

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

Существующие проекты

Компилятор предназначен для компиляции функциональных компонентов и хуков, соответствующих правилам React. Он также может обрабатывать код, который нарушает эти правила, освобождая (пропуская) эти компоненты или перехватчики.
"По этой причине, чтобы успешно использовать компилятор в существующих проектах, мы рекомендуем сначала запустить его в небольшом каталоге кода вашего продукта. Вы можете сделать это, настроив компилятор для запуска только в определенном наборе каталогов" - Сообщает команда React.

const ReactCompilerConfig = {  
	sources: (filename) => {  
		return filename.indexOf('src/path/to/dir') !== -1;  
	},  
};

В редких случаях вы также можете настроить компилятор для работы в режиме «аннотации», используя compilationMode: "annotation". Это означает, что компилятор будет компилировать только компоненты и перехватчики, аннотированные директивой "use memo".

const ReactCompilerConfig = {  
	compilationMode: "annotation",  
};  

// src/app.jsx  
export default function App() {  
	"use memo";  
	// ...  
}

Этот режим аннотации является временным и предназначен для помощи ранним пользователям, поэтому разработчики не планируют использовать"use memo" директиву в долгосрочной перспективе.

Новые проекты

В качестве установки на новые проекты, React Сompiler будет иметь широкий выбор вариантов установки и настройки:

Babel

npm install babel-plugin-react-compiler

После установки плагина просто добавьте его в свою Babel конфигурацию :

// babel.config.js  
const ReactCompilerConfig = { /* ... */ };  

module.exports = function () {  
	return {  
		plugins: [  
			['babel-plugin-react-compiler', ReactCompilerConfig], 
			// ...  
		],  
	};  
};

Vite

Для использования в связке с Vite внесите изменения в vite.config:

// vite.config.js  
const ReactCompilerConfig = { /* ... */ };  
  
export default defineConfig(() => {  
	return {  
		plugins: [  
			react({  
				babel: {  
				plugins: [  
					["babel-plugin-react-compiler", ReactCompilerConfig],  
				],  
				},  
			}),  
		],  
		// ...  
	};  
});

Next.js

В случае с Next.js мы имеем экспериментальную конфигурацию для включения компилятора React. Это автоматически гарантирует, что Babel настроен с использованием babel-plugin-react-compiler.

Для начала стоит установить Next.js canary версию с включенным React 19 Release Candidate, затем поставьте сам плагин:

npm install next@canary babel-plugin-react-compiler

После чего перейдите к редактированию next.config:

// next.config.js  
/** @type {import('next').NextConfig} */  
const nextConfig = {  
	experimental: {  
		reactCompiler: true,  
	},  
};  

module.exports = nextConfig;

Webpack

Для работы с Webpack потребуется создать собственный лоадер для компилятора
Выглядеть это должно примерно так:

const ReactCompilerConfig = { /* ... */ };  

const BabelPluginReactCompiler = require('babel-plugin-react-compiler');  

function reactCompilerLoader(sourceCode, sourceMap) {  
	// ...  
	
	const result = transformSync(sourceCode, { 
	
	// ...  
	
	plugins: [  
		[BabelPluginReactCompiler, ReactCompilerConfig],  
	],  
	
	// ...  
	});  
  
	if (result === null) {  
		this.callback(  
			Error(`Failed to transform "${options.filename}"`)  
		);  
		return;  
	}  
	this.callback(  
		null,  
		result.code  
		result.map === null ? undefined : result.map  
		);  
}  

module.exports = reactCompilerLoader;

После установки вы можете узнать, какие из ваших компонентов были оптимизированы. Для этого React Devtools (v5.0+) имеет встроенную поддержку React Compiler и отображает значок «Memo ✨» рядом с компонентами, оптимизированными компилятором.
Чтобы отменить оптимизацию компонентов, вы можете использовать директиву "use no memo", которая позволяет вам отказаться от компиляции компонентов и хуков компилятором React на некоторое время.

Что дальше?

Таким образом, React Compiler действительно направлен на автоматизацию мемоизации, значительно облегчая жизнь разработчикам. Сейчас нам остаётся лишь ждать полноценного выпуска компилятора. Тем не менее, важно понимать, что в некоторых случаях ручное управление всё ещё может быть полезным.

Хотя дата релиза постоянно переносится, я надеюсь на скорейший выпуск стабильной версии. Стоит также учитывать, что с выпуском стабильной версии некоторые аспекты установки и настройки компилятора могут измениться. В этом случае, рекомендую ознакомиться с официальной документацией React.

P.S. Для меня опыт написания статей является новым. Поддавшись уговорам одного своего приятеля, я всё же решил попробовать себя на этом поприще. Если у вас есть предложения по улучшению качества данной статьи, прошу оставить свой отзыв в комментариях.

Комментарии (39)


  1. rzcoder
    15.06.2024 11:33

    Учитывая, что с внешними стейт менеджерами работать не умеет, довольно бесполезная в реальной жизни штука. Так же большой вопрос: что там с дебагом, сурсмапы завезут? А если с тайпскриптом?


    1. 0x6b73ca
      15.06.2024 11:33

      Поставьте бету и узнайте


  1. nin-jin
    15.06.2024 11:33
    +4

    Учитывая, что useMemo и useCallback и так не решали проблему лишних ререндеров, то этот мега-компилятор-оптимизатор мало что изменит. Пара примеров:

    const b = c.map( Boolean )
    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    const a = false
    const b = Date.now()
    const memoizedCallback = useCallback(() => a ? doSomething(a, b) : null, [a, b]);


    1. iscareal
      15.06.2024 11:33

      А что не так? В обоих случаях на каждом рендере b - новое значение. Поэтому useCallback и useMemo будут заново пересчитываться


      1. Balek
        15.06.2024 11:33

        Проблема в том, что инструмент должен подстраиваться под разработчика, а не разработчик под инструмент. Правильный вопрос, не "а что не так", а "почему реакт не может соптимизировать этот код, чтобы не делать бессмысленную работу в рантайме"?


      1. nin-jin
        15.06.2024 11:33
        +3

        В первом случае значения хоть и разные, но эквивалентные, а во втором значение переменной ни на что не влияет.


    1. Yozi
      15.06.2024 11:33

      Новый компилятор автоматически исправит ошибку в первом примере, обернув b useMemo


      1. nin-jin
        15.06.2024 11:33

        Это ничем не поможет. Там каждый раз генерируется новый массив с тем же содержимым. Там, конечно, filter должен быть, а не map, но суть это не меняет.


        1. Yozi
          15.06.2024 11:33

          const b = useMemo(()=> c.map( Boolean ), [c]);
          const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

          В этом случае b хоть и новый массив, но он будет стабильным пока стабильно "c"


          1. nin-jin
            15.06.2024 11:33
            +1

            Но "с" не стабильно, в отличие от "б".


            1. Alexandroppolus
              15.06.2024 11:33

              В общем случае, если "с" поменялось, то и memoizedValue должно поменяться.

              Разумеется, если в "с" воткнуть значение [...c, false], то всё должно остаться как есть (с учётом поправки про filter), но тут уж на уровне библиотеки не отследить..


              1. nin-jin
                15.06.2024 11:33
                +1

                Сам мап тут не дорогой, проблема дальше.


                1. Alexandroppolus
                  15.06.2024 11:33

                  Это понятно. Конкретно здесь достаточно сравнения shallow для массива "b", но это выход за пределы фундаментальных основ Реакта, где все сравнения только по ссылке. В более общем кейсе не хватит и полного глубокого сравнения. Новый компилятор, насколько я понял, будет автоматически навешивать мемоизацию, но действовать в рамках основ.


                  1. nin-jin
                    15.06.2024 11:33
                    +2

                    Так о том и речь, что основы кривые. Горбатого могила исправит, а не компилятор.


  1. Revertis
    15.06.2024 11:33
    +3

    Обязвтельно тюремный жаргон в заголовке использовать?


    1. CatalystoEyes Автор
      15.06.2024 11:33

      Простите, но во время создания статьи я не увидел ничего тюремного в слове 'пояснение'. Возможно, мы с вами по-разному интерпретируем этот термин.


      1. Revertis
        15.06.2024 11:33
        +1

        Смотрите на "поясняю за".


      1. artptr86
        15.06.2024 11:33

        «Пояснить за» — это «блатной» жаргон


        1. Revertis
          15.06.2024 11:33

          Так ли он сильно отличается от тюремного? И зачем он на Хабре?


          1. artptr86
            15.06.2024 11:33
            +2

            Ничем не отличается, и на Хабре такое не нужно


  1. isumix
    15.06.2024 11:33
    +1

    Что "многопоточность" в 18 версии, что "компиляция" в 19 версии - решения призванные закостылить фундаментальные недостатки Реакта.

    А именно:

    • Не разделение логики компонентов на содание и обновление

    • Автоматический, "умный" рендеринг/обновление

    По этим причинам я решил развивать версию реакта без этих недостатоков. Всем кому интересно, прошу поучаствовать https://github.com/fusorjs/dom.


    1. artptr86
      15.06.2024 11:33

      Fine-grained control over DOM updates

      вручную каждый раз дёргать update() — это не fine-grained control, а тупиковый путь


      1. isumix
        15.06.2024 11:33

        В Реакте вы каждый раз вручную дергаете setState для обновления...


        1. artptr86
          15.06.2024 11:33

          Да, но если бы, например, вместо кортежа [value, setValue] возвращался объект с геттером и сеттером, вы бы явного setValue не увидели бы. Собственно, примерно это происходит, например, в SolidJS, Vue или $mol. Вам бы стоило изучить prior art перед изобретением собственного велосипеда.


          1. isumix
            15.06.2024 11:33

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

            Хуков, сигналов, жизненного цикла - не определено. Для стейта можно использовать обычные переменные, так как содание/обновление компонентов разделено. Для жизненного цикла, создание и обновление это обычные функции JavaScript, остается монтирование в ДОМ, которое по умолчанию взято из Custom Elements. Это всё можно изменить под свои нужды.

            Обновление происходит только там и тогда, гды мы этого хотим. Опять же можно подключить систему автоматического diff-а и обновления как в Реакте, но зачем? Я делал небольшое сравнение по многословности, и компоненты Фьюзора выглядят "легче".


            1. artptr86
              15.06.2024 11:33

              Понимаете, в вашем случае вы скрываете под «гибкостью» отсутствие полезных возможностей. В результате непонятно, почему тогда не использовать Custom elements, которые из коробки предоставляют жизненный цикл. Минимализм у вас получается самоцелью, которая при этом достигается не минимализацией кода, а выбрасыванием фич. В результате, что вы даёте разработчикам? Только JSX-рендер компонентов? Тогда почему бы им не выбрать минималистичные Preact или SolidJS, которые могут предоставить больше фич?

              Требование совершать update вручную тут же обязывает программиста либо дёргать update на каждый чих, либо вручную делать какую-то систему проверки изменений. В простейшем случае это будет аналог useMemo, в который придётся завернуть все компоненты.

              Такой пример: В приложении в отдельных местах могут выводиться даты. При этом пользователь может вызвать диалог настроек и поменять текущую таймзону. В вашем случае нужно будет делать глобальный update или каждый потребитель таймзоны должен будет самостоятельно подписываться на неё и отслеживать изменения?


              1. isumix
                15.06.2024 11:33

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

                • Фич не меньше, с Фьюзором можно осуществить те же задачи что и с Реактом, Преактом и Солидом. Нет тех фич которыя появились для закостыливаня вышеотмеченных проблем (хуки, многопоточность, компиляция...)

                • Нативные Кастомные Элементы не позволяют декларативно создавать и обновлять ДОМ. Фьюзор позволяет это делать используя их нативный жизненный цикл.

                • Проверка изменений уже встроена. Например если вы дернете app.update() в корне вашего приложения, то проверка побежит по всем его нодам и обновит ДОМ, но только в том случае если значение изменилось (Как в Реакте). Но самое главнояе что вы можете дернуть апдейт не в корне, а в любом произвольном месте, обновить одну только дату, и в этом случае дорогостоящий DIFF не будет бежать по всему дереву. В этом гибкость.


                1. artptr86
                  15.06.2024 11:33

                  то проверка побежит по всем его нодам и обновит ДОМ, но только в том случае если значение изменилось

                  Так, отлично, Тогда что же будет проверяться и как будет производиться сравнение? Все переданные пропсы будут сравниваться через ===? И как у вас предлагается передавать глобальное состояние типа локали или таймзоны?

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

                  Вообще-то речь шла о таймзоне, а не дате. Ну да ладно. Кто же будет дёргать апдейт? Родительский компонент или код самого компонента?

                  дорогостоящий DIFF не будет бежать по всему дереву

                  Дереву стейта (как в ангуляре) или виртуальному DOMу?


                  1. isumix
                    15.06.2024 11:33

                    • Только динамические (функции) пропсы и чайлды сравниваются с предыдущими значениями и при разнице обновляется ДОМ в нужном месте. Через Object.is. Статические при создании добавляются в ДОМ и не меняются.

                    • Глобальное состояние таймзоны претендует на глобальную переменную.

                    • Апдейт может делать как сам компонент, для своих элементов, как видно из примера "CountingButton" и "SVG Analog Clock". Так и внешний код, например: глобальный стейт из TodoMVC демки, или history для роутинга из демки Tutorial...

                    • Дереву приложения, приложение это ваши функции - компоненты, которые используют примитивы Фьюзора, которые в свою очередь организованы в дерево. Как в Реакте. Я бы не стал это называть виртуальным домом, так как примитивы Фьюзора знают только те места ДОМ, которые нужно обновлять. И делают это они одной операцией, без предварительного запроса дом.


                    1. artptr86
                      15.06.2024 11:33

                      В общем, похоже, вам остаётся добавить реактивные примитивы, и получится аналог SolidJS


                      1. isumix
                        15.06.2024 11:33

                        Вы можете поключить и использовать сигналы если хотите. На сколько я помню их вынесли из солида. Но накручивание лишней логики на простые операции не принесет особых бенефитов. А производительность и читабельность могут пострадать ИМХО.


                      1. artptr86
                        15.06.2024 11:33

                        А много ли реальных приложений с логикой уровня click counter или всё же в них логика значительно более сложная, что оправдывает реактивность?


                      1. isumix
                        15.06.2024 11:33

                        Поэтому я подготовил два полноценных приложения: TODO и Tutorial в котором имплементированы основные кейсы использования. Если чего-то не будет хватать, то буду добавлять.


                      1. artptr86
                        15.06.2024 11:33

                        TODO в 2024? Серьёзно? Пример, который даже не содержит асинхронных операций?


                      1. isumix
                        15.06.2024 11:33

                        Асинхронные есть в туториале, например загрузка данных с сервера.


  1. Ismail5002
    15.06.2024 11:33

    Скоро и самого разработчика будут оборачивать компиляторам ,чтобы правильно код писал


  1. reistr
    15.06.2024 11:33

    Так а разве мемоизировать все что можно имеет смысл? Или компилятор будет судить что дешевле - мемоизация или перевычисление?


    1. CatalystoEyes Автор
      15.06.2024 11:33

      Честно признаться, сам процесс этой оптимизации мне также не до конца понятен. Но я всё же склоняюсь к предположению, что компилятор сам определится, что будет выгоднее с точки зрения оптимизации. Пока стоит ждать полноценного релиза, чтобы понять, стоит ли игра свеч вообще.


  1. Lirrr
    15.06.2024 11:33

    Есть же уже отличная статья от Нади Макаревич, которая показывает несостоятельность компайлера. Из 10 проблемных мест он заметил только 2. https://www.developerway.com/posts/i-tried-react-compiler