React Profiler: практический гайд по поиску узких мест
Быстрый разбор React Profiler: как включить, читать Flamegraph и фиксировать метрики в CI. Примеры кода, чек-лист и советы по мемоизации.
Введение
- Список в React-приложении начал тормозить, а консоль молчит скорее всего, происходит лишний перерасчет компонентов.
- Ручная проверка (
console.time
, логирование рендеров) медленна и не всегда выявляет причину. - При тысячи строк в списке даже небольшое избыточное обновление приводит к заметному фризу.
- React Profiler встроенный в React DevTools инструмент, показывающий, какие компоненты отрисовались, сколько времени занял каждый рендер и сколько раз он был вызван.
- Мы пройдем от установки DevTools до измерения производительности на примере виртуализированного списка с фильтрацией.
- После чтения вы сможете быстро определить “тормозящие” части UI и применить оптимизацию без лишних гипотез.
- Главный момент не собрать метрики, а интерпретировать их и превратить в изменения кода.
- В примерах используется React Profiler вместе с
useMemo
,React.memo
иuseCallback
. - Каждый совет подкреплен небольшим, готовым к копированию кодом.
- Если вы знакомы с профайлером Chrome, часть информации будет вам известна, но мы подробно разберем нюансы именно в React-контексте.
- В конце чек-лист из пяти пунктов, чтобы ничего не упустить при профилировании.
1. Как включить React Profiler в DevTools
Откройте DevTools → вкладка Components → включите Profiler. Если вкладка отсутствует, обновите расширение React DevTools.
После включения появится кнопка “Record”. Нажмите ее, выполните действие (например, переключите фильтр) и нажмите “Stop”. Появится график с таймлайном рендеров.
Таблица навигации по профайлеру:
Элемент | Что показывается | Как использовать |
---|---|---|
Flamegraph | Иерархия рендеров в виде горящего графика | Быстро увидеть “трудные” компоненты |
Ranked list | Список компонентов, отсортированных по времени | Найти топ-5 самых дорогих рендеров |
Commit details | Подробный список изменений в конкретном коммите | Понять, какие пропсы/стейт изменились |
2. Чтение Flamegraph: что реально тормозит
Flamegraph набор горизонтальных полос. Чем шире полоса, тем дольше рендер. Обратите внимание на самый широкий столбец это ваш bottleneck.
function Item({ data }) {
console.log("render Item"); // быстро увидеть лишние рендеры
return <div>{data.title}</div>;
}
export default React.memo(Item); // мемоизируем, если пропсы не меняются
Если Item
попал в топ-3 по времени, проверьте, действительно ли его пропсы меняются каждый рендер. Частая ошибка передача новых объектов/массивов без мемоизации.
Мини-список типичных виновников:
- Инлайн-функции в JSX (
onClick={() => …}
). - Новые массивы/объекты в props (
style={{…}}
). - Отсутствие
React.memo
у чистых компонентов.
3. Как измерить отдельный компонент
Компонент можно обернуть в <Profiler>
с колбэком onRender
.
import { Profiler } from "react";
function onRenderCallback(
id, // "ListItem"
phase, // "mount" | "update"
actualDuration, // время рендера в ms
baseDuration, // теоретическое время без оптимизаций
startTime,
commitTime,
interactions
) {
console.log(`[${id}] ${phase} ${actualDuration.toFixed(2)}ms`);
}
function ListItem({ data }) {
return (
<Profiler id="ListItem" onRender={onRenderCallback}>
<Item data={data} />
</Profiler>
);
}
Теперь каждый рендер ListItem
будет логировать свои метрики удобно, когда Flamegraph слишком громоздок.
Таблица сравнения подходов:
Подход | Преимущества | Недостатки |
---|---|---|
Flamegraph (DevTools) | Визуальная карта всей UI | Сложнее искать одиночный компонент |
<Profiler> колбэк | Точные цифры, можно отправлять в аналитику | Требует изменения кода |
4. Частый ловушечный паттерн: передача новых функций
function Parent({ items }) {
// ❌ каждый рендер создает новую функцию
const handleClick = (id) => console.log(id);
return items.map((item) => (
<Child key={item.id} onClick={() => handleClick(item.id)} />
));
}
Child
получает новую функцию каждый раз, даже если item
не изменился. Решение useCallback
.
function Parent({ items }) {
const handleClick = useCallback((id) => console.log(id), []);
return items.map((item) => (
<Child key={item.id} onClick={handleClick} id={item.id} />
));
}
const Child = React.memo(({ onClick, id }) => (
<button onClick={() => onClick(id)}>Click {id}</button>
));
Теперь Child
рендерится только при изменении id
.
Краткий чек-лист для функций в props:
- ✅ Выделяйте обработчики в
useCallback
. - ✅ Передавайте
id
отдельно, а не через замыкание. - ✅ Оборачивайте “чистый” компонент в
React.memo
.
5. Мемоизация данных: useMemo
vs. пересоздание массивов
function List({ rawData }) {
// ❌ каждый рендер создает новый массив
const filtered = rawData.filter((item) => item.active);
return filtered.map((item) => <Item key={item.id} {...item} />);
}
Если rawData
часто приходит без изменения, filtered
будет пересоздаваться каждый раз, вызывая лишние рендеры. useMemo
решает проблему.
function List({ rawData }) {
const filtered = useMemo(
() => rawData.filter((item) => item.active),
[rawData]
);
return filtered.map((item) => <Item key={item.id} {...item} />);
}
Профайлер покажет, что List
теперь работает быстрее, а Item
реже.
Таблица типовых мест для memo:
Что мемоизировать | Как мемоизировать | Когда НЕ нужно |
---|---|---|
Вычисленные массивы/объекты | useMemo | Если данные меняются каждый рендер |
Callback-функции | useCallback | Если функция не передается потомкам |
Дорогостоящие расчеты | useMemo | Если расчет легкий |
6. Интеграция профилирования в CI/CD
Для больших проектов удобно фиксировать метрики рендеров в тестах. React 18 позволяет запускать Profiler в headless-режиме через react-test-renderer
.
import React from "react";
import TestRenderer from "react-test-renderer";
import { Profiler } from "react";
function onRender(id, phase, actualDuration) {
console.log(`CI ${id} ${phase}: ${actualDuration}`);
}
test("profile MyComponent", () => {
const component = TestRenderer.create(
<Profiler id="MyComponent" onRender={onRender}>
<MyComponent />
</Profiler>
);
// вызвать setState и т.д.
});
CI-скрипт может сравнивать текущий actualDuration
с базовой метрикой и падать, если регресс > 10 %.
Список шагов для CI-профайлинга:
- Добавьте тест-файл с оберткой
<Profiler>
. - Запустите в pipeline (Jest, Vitest).
- Выведите метрики в артефакт.
- Настройте правило
failIfSlow
(пример вpackage.json
). - При изменении кода проверяйте, не превышен ли порог.
FAQ / Чеклист
- Не ставьте
Profiler
в продакшн он добавляет накладные расходы. - Запоминайте функции (
useCallback
) только если передаете их вReact.memo
-компоненты. - Не переусердствуйте с
React.memo
иногда рендер дешевый, а мемоизация усложнит код. - Проверяйте
baseDuration
: если он сильно превышаетactualDuration
, стоит пересмотреть алгоритм рендеринга. - Отключайте автоматический сбор мусора в браузере при длительном профайлинге (
performance.pauseTiming
), иначе измерения будут “зашумлены”.
При работе с динамическими списками, сложными формами или интерактивными дашбордами React Profiler быстро укажет, какие куски UI “тянут” приложение.
Включение, чтение Flamegraph, мемоизация проблемных частей и фиксирование метрик в CI ваш набор инструментов для стабильной производительности.
Для более глубокого понимания процессов изучите рендеринг и реконсиляцию.