- Основные понятия React
- Нативные хуки React
- Фазы рендеринга в React
- Жизненный цикл компонента
- Точка входа в React-приложение
- Реконсиляция
- Рендеры
- Fiber и useTransition
- Поверхностное сравнение
- Инструменты оптимизации
React — это библиотека JavaScript для создания пользовательских интерфейсов. Основные концепции:
- Компоненты — независимые и повторно используемые блоки UI, которые логически разделяют интерфейс
- JSX — синтаксический сахар, позволяющий писать HTML-подобный код в JavaScript
- Виртуальный DOM — легковесное представление реального DOM, которое позволяет React эффективно обновлять интерфейс
- Состояние (state) — данные, которые могут изменяться в результате взаимодействия пользователя или других факторов
- Свойства (props) — данные, передаваемые в компонент извне, доступные только для чтения
- Побочные эффекты — действия, выходящие за пределы рендеринга компонента (API-запросы, подписки, таймеры)
Хуки — это специальные функции, позволяющие использовать состояние и другие возможности React без написания классов.
- useState — позволяет добавить состояние в функциональный компонент
- useEffect — позволяет выполнять побочные эффекты в функциональных компонентах
- useContext — позволяет подписываться на изменения контекста
- useReducer — альтернатива useState для сложной логики управления состоянием
- useCallback — мемоизирует функцию между рендерами
- useMemo — мемоизирует результат вычислений между рендерами
- useRef — позволяет получить доступ к DOM-элементу или хранить изменяемое значение
- useImperativeHandle — позволяет настраивать экземпляр, передаваемый родительским компонентам
- useLayoutEffect — синхронная версия useEffect, выполняется после измерения и расположения
- useDebugValue — позволяет отображать метку для пользовательских хуков в React DevTools
- useTransition — позволяет пометить обновления как "неблокирующие", сохраняя интерфейс отзывчивым
- useDeferredValue — позволяет отложить обновление части UI при изменении данных
- useId — генерирует уникальный ID для доступности
Рендеринг в React — это процесс обновления интерфейса, состоящий из трех основных этапов: триггера (инициация), рендеринга (вычисление изменений в Virtual DOM) и фиксации (применение изменений к реальному DOM). Этот процесс разделен на две основные фазы: фаза рендеринга (render phase, чистая) и фаза фиксации (commit phase, DOM-операции).
- Что происходит: React вызывает компоненты, вычисляет изменения, сравнивая текущее дерево с предыдущим (алгоритм Diffing).
- Особенности: Эта фаза может быть приостановлена, прервана или перезапущена React. В этой фазе компоненты должны быть "чистыми" (без побочных эффектов).
- Результат: Создание нового виртуального дерева (Fiber tree) и формирование списка необходимых изменений.
- Что происходит: React применяет вычисленные изменения к реальному DOM-дереву (appendChild, update, remove).
- Особенности: Эта фаза выполняется синхронно и не прерывается, чтобы гарантировать правильность отображения.
- Результат: Обновленный пользовательский интерфейс.
- Триггер: Событие, инициирующее рендер (первоначальный рендер или изменение стейта/пропсов).
- После фиксации (Post-commit): После того как DOM обновлен, React вызывает «побочные эффекты» (например, хук useEffect или методы жизненного цикла componentDidMount/componentDidUpdate).
- Virtual DOM: React не трогает реальный DOM, если результат рендеринга такой же, как в прошлый раз.
- Ререндеринг: Происходит, когда компонент получает новые props или меняется его state.
- Оптимизация: React выполняет минимально необходимые операции в DOM.
Жизненный цикл компонента React — это последовательность стадий, через которые проходит компонент: монтирование (создание и вставка в DOM), обновление (изменение props/state) и размонтирование (удаление из DOM). В классовых компонентах используются методы (componentDidMount, componentDidUpdate, componentWillUnmount), а в современных функциональных компонентах — хук useEffect.
- Монтирование (Mounting): Компонент создается и добавляется в DOM. Методы: constructor, render, componentDidMount (используется для API-запросов).
- Обновление (Updating): Вызывается при изменении props или state. Методы: render, componentDidUpdate (обновление данных после изменения).
- Размонтирование (Unmounting): Компонент удаляется из DOM. Метод: componentWillUnmount (очистка таймеров, подписок).
- Монтирование: useEffect(() => { ... }, []) — пустой массив зависимостей означает выполнение один раз при создании.
- Обновление: useEffect(() => { ... }, [data]) — выполняется, когда изменяется переменная data.
- Размонтирование: useEffect(() => { return () => { ... } }, []) — возврат функции очистки.
Понимание этих стадий помогает оптимизировать производительность и управлять побочными эффектами.
Для начала, давайте разберемся, что является пусковым механизмом для машины React. Другими словами, найдем точку входа. В случае с React это не сложно.
Без примеров, все так, обойтись не получится. Взглянем на типичный React-код для Web-приложения.
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<div>My React App</div>);Этот код использует пакет react-dom/client для создания Root-контейнера и, далее, рендера DIV-элемента в этот контейнер. Именно здесь и находится тот самый пусковой механизм React, точнее, здесь их сразу два: создание контейнера посредством createRoot, и запуск процесса рендеринга в контейнере. Но обо всем по порядку.
Как мы знаем из открытых источников, прежде чем изменения попадут в DOM, React сначала производит все необходимые модификации в так называемом виртуальном дереве. После чего, уже это виртуальное дерево "попадает" в реальный DOM. Процесс согласования виртуального дерева с реальным DOM и называется реконсиляцией.
Дополнительную сложность процессу создает тот факт, что сегодня существуют разные платформы, где итоговый UI может быть выведен (на экран или, например, в строку или файл). В частности, сам React предусматривает рендеринг в Web, серверный рендеринг (SSR), рендеринг на мобильных устройствах (React Native) и др.
В этой связи, команда React выделила отдельный пакет react-reconcile в качестве некоего абстрактного API. Он работает в двух режимах:
- Mutation mode - для платформ, позволяющих мутировать итоговое дерево (т.е. имеют методы, схожие с appendChild/removeChild)
- Persistent mode - для платформ с иммутабельными деревьями. В этом случае, на каждое изменение, все дерево клонируется целиком, производятся модификации, а затем все дерево полностью заменяется на модифицированное
Сам этот пакет не осуществляет конечную привязку к DOM, а только обеспечивает всю необходимую механику по подготовке и манипуляции элементами. Сама же непосредственная привязка к DOM осуществляется средствами внешнего провайдера, реализующего API пакета react-reconciler. Реализации провайдером заключается в выставлении конкретных флагов-настроек и описании методов-колбэков, таких как createInstance, appendChild, removeChild и др.. Такой подход позволяет создавать разные провайдеры для разных случаев и платформ.
Провайдеры, реализующие API пакета react-reconciler, условно, называются рендеры. Теоретически, пакет react-reconciler задумывался как полноценный API, но, с момента его создания в 2017-м работы по нему ведутся довольно активно, периодически случаются существенные изменения, поэтому, формально, пакет считается unstable, использовать его напрямую в своих проектах стоит с осторожностью.
Сам же React предлагает несколько реализаций рендеров "из коробки":
- React DOM - этот рендер мы уже видели в примере выше. Он обеспечивает привязку к DOM-дереву браузера
- React Native - этот рендер обеспечивает нативный рендеринг на мобильных платформах
- React ART - позволяет рисовать векторную графику средствами React. Фактически, является реактивной оболочкой для библиотеки ART.
Прежде чем двинуться дальше, важно познакомиться с базовой сущностью движка React.
Fiber - это внутренний объект React, представляющий задачу ("работу"), которую движок запланировал к выполнению или уже выполнил.
React Fiber — это низкоуровневый алгоритм движка (реконсилер), разбивающий рендеринг на части для отзывчивости. useTransition — это хук, использующий Fiber для пометки обновлений состояния как «менее важных», позволяя держать интерфейс отзывчивым (например, при фильтрации больших списков). Fiber — движок, useTransition — инструмент управления приоритетами.
- Суть: Fiber — это переписанный алгоритм согласования (reconciliation), работающий с Fiber-узлами (структура данных), а useTransition — API-хук.
- Уровень: Fiber работает «под капотом» (автоматически), а useTransition используется разработчиком явно для оптимизации.
- Функция: Fiber обеспечивает возможность приостанавливать, прерывать и возобновлять рендеринг. useTransition явно разделяет обновления на срочные (input) и фоновые (результаты поиска).
- Применение: Fiber работает всегда в React 16+. useTransition применяется для конкретных задач производительности в React 18+.
const [isPending, startTransition] = useTransition();
// ...
// Срочное обновление (ввод)
setInputValue(e.target.value);
// Фоновое обновление (фильтрация)
startTransition(() => {
setSearchQuery(e.target.value);
});useTransition помогает избежать зависания интерфейса при тяжелых рендерах, позволяя пользователю взаимодействовать с элементами.
Shallow (поверхностное) сравнение в React — это оптимизационный алгоритм, проверяющий изменения пропов или стейта путем сравнения примитивов по значению, а объектов/массивов — только по ссылке (не углубляясь во вложенные поля). Это позволяет быстро определить необходимость ререндера, избегая дорогостоящего перебора глубоких структур.
- Примитивы (Number, String, Boolean): Сравниваются по значению (a===b).
- Объекты/Массивы: Сравниваются ссылки. Если объект создан заново (новый {}), сравнение вернет false, даже если поля идентичны.
- Где применяется: Используется в React.memo для пропов, в массивах зависимостей useEffect и useMemo, а также при обновлении стейта.
- Цель: Оптимизация производительности: предотвращение перерисовки компонентов, если данные не изменились.
Для корректной работы поверхностного сравнения необходимо использовать принцип неизменяемости (immutability) — создавать новые объекты/массивы при изменении, а не мутировать старые.
React.memo — это функция высшего порядка (HOC - Higher Order Component), предназначенная для оптимизации производительности функциональных компонентов в React. Она мемоизирует (кеширует) результат рендеринга компонента: если пропсы не изменились, React пропускает перерендер компонента, используя сохраненный результат.
- Предотвращение лишних рендеров: Полезен, когда компонент часто перерисовывается с теми же пропсами.
- Поверхностное сравнение: React.memo сравнивает текущие и предыдущие пропсы, используя shallow comparison (поверхностное сравнение, Object.is).
- Применение:
const MemoizedComponent = React.memo(MyComponent);. - Особенности: Если пропсы — объекты или функции, они должны быть мемоизированы с помощью useMemo или useCallback, иначе memo будет считать их новыми.
- Собственный компаратор: Вторым аргументом можно передать функцию, которая принимает prevProps и nextProps и возвращает true, если пропсы равны, и false — если нет.
React.memo не предотвращает рендер, если меняется собственное состояние компонента (useState) или контекст (useContext).
useMemo — это React-хук для оптимизации производительности, который кэширует (мемоизирует) результат вычислений между рендерами. Он пересчитывает значение только при изменении указанных зависимостей, предотвращая ресурсоемкие операции при каждом перерендеринге компонента.
- Синтаксис:
const cachedValue = useMemo(calculateValue, dependencies). - Применение: Используется для кэширования тяжелых вычислений, а не для побочных эффектов.
- Отличие от useCallback: useMemo кэширует результат вызова функции, в то время как useCallback кэширует саму функцию.
- Отличие от React.memo: React.memo оптимизирует весь компонент, предотвращая перерисовку, если пропсы не изменились, тогда как useMemo оптимизирует конкретные вычисления внутри компонента.
const sortedList = useMemo(() => {
return heavySortingFunction(data); // Выполняется только при изменении data
}, [data]);- При необходимости избежать выполнения «тяжелых» функций при каждом рендере.
- При передаче мемоизированных значений в дочерние компоненты, обернутые в React.memo.
Вероятно, имелся в виду хук useCallback в React, который используется для мемоизации (кэширования) определений функций между рендерами. Он предотвращает пересоздание функции, оптимизируя производительность компонентов, особенно при передаче колбэков в дочерние компоненты, предотвращая лишние перерисовки.
- Синтаксис:
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);. - Когда использовать:
- При передаче функций в мемоизированные компоненты (React.memo).
- Когда функция используется как зависимость в других хуках (например, useEffect).
- Отличие от useMemo: useCallback кэширует саму функцию (ее определение), а useMemo кэширует результат вычисления функции.
import { useCallback } from 'react';
const handleSubmit = useCallback((data) => {
console.log(data);
}, [/* зависимости */]);Осторожность: Не следует использовать useCallback везде подряд. Это имеет смысл только при реальных проблемах с производительностью, так как сам хук также требует ресурсов на выполнение.
| Инструмент | Тип | Что делает | Цель |
|---|---|---|---|
| React.memo | HOC | Мемоизирует компонент на основе пропсов | Избежать рендера, если props не изменились |
| useMemo | Hook | Кэширует результат функции | Избежать тяжелых вычислений |
| useCallback | Hook | Кэширует саму функцию | Избежать пересоздания функции (для пропсов) |
| useTransition | Hook | Делает обновление состояния неблокирующим | Сделать UI отзывчивым при тяжелых рендерах |
- Применение: Оборачивает функциональный компонент.
- Как работает: Сравнивает prevProps и nextProps. Если они равны, компонент не перерисовывается.
- Пример:
const MyComponent = React.memo(function MyComponent(props) { ... });
- Применение: Используется внутри компонента для кэширования результатов, например, при сортировке огромного массива.
- Как работает: Пересчитывает значение только при изменении зависимостей.
- Пример:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- Применение: Используется для передачи функций-колбэков в мемоизированные дочерние компоненты (в связке с React.memo).
- Как работает: Сохраняет ссылку на функцию между рендерами, пока зависимости не изменятся.
- Пример:
const memoizedCallback = useCallback(() => { doSomething(a); }, [a]);
- Применение: Используется, когда нужно обновить состояние, но это вызывает «зависание» UI (например, фильтрация большого списка при вводе).
- Как работает: Позволяет пометить обновление как «низкоприоритетное» (transition). Интерфейс остается отзывчивым, пока выполняется тяжелая операция.
- Пример:
const [isPending, startTransition] = useTransition();
- Используйте React.memo для компонентов, которые часто рендерятся с теми же пропсами.
- Используйте useMemo, если расчеты занимают много времени.
- Используйте useCallback, если функция передается как проп в memo компоненты.
- Используйте useTransition для действий, требующих тяжелого обновления UI (поиск, фильтрация).
Я использую useCallback почти только тогда, когда мне нужна функция, которая должна выполняться внутри useEffect, и её нельзя объявить внутри него.
useMemo, с другой стороны, я использую для тяжёлых вычислений, которые я не хочу запускать на каждом рендере, а только когда его результат будет отличаться (то есть, какие-то данные, которые он использует, отличаются от предыдущего рендера).
"Тяжёлым" можно назвать функции, у которых временная сложность больше O(1) или O(n*log(n)). Например, вложенные циклы. Или любые функции типа reduce, filter и т.д. В этом посте это лучше всего объяснено https://www.developerway.com/posts/how-to-use-memo-use-callback
Ещё добавлю, что я использую useCallback, когда передаю коллбэки как пропсы дочерним компонентам.