Назад
Назад к заметкам
· 5 мин чтения

React Profiler: практический гайд по поиску узких мест

Быстрый разбор React Profiler: как включить, читать Flamegraph и фиксировать метрики в CI. Примеры кода, чек-лист и советы по мемоизации.

reactperformanceoptimizationprofiler

Введение

  1. Список в React-приложении начал тормозить, а консоль молчит скорее всего, происходит лишний перерасчет компонентов.
  2. Ручная проверка (console.time, логирование рендеров) медленна и не всегда выявляет причину.
  3. При тысячи строк в списке даже небольшое избыточное обновление приводит к заметному фризу.
  4. React Profiler встроенный в React DevTools инструмент, показывающий, какие компоненты отрисовались, сколько времени занял каждый рендер и сколько раз он был вызван.
  5. Мы пройдем от установки DevTools до измерения производительности на примере виртуализированного списка с фильтрацией.
  6. После чтения вы сможете быстро определить “тормозящие” части UI и применить оптимизацию без лишних гипотез.
  7. Главный момент не собрать метрики, а интерпретировать их и превратить в изменения кода.
  8. В примерах используется React Profiler вместе с useMemo, React.memo и useCallback.
  9. Каждый совет подкреплен небольшим, готовым к копированию кодом.
  10. Если вы знакомы с профайлером Chrome, часть информации будет вам известна, но мы подробно разберем нюансы именно в React-контексте.
  11. В конце чек-лист из пяти пунктов, чтобы ничего не упустить при профилировании.

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-профайлинга:

  1. Добавьте тест-файл с оберткой <Profiler>.
  2. Запустите в pipeline (Jest, Vitest).
  3. Выведите метрики в артефакт.
  4. Настройте правило failIfSlow (пример в package.json).
  5. При изменении кода проверяйте, не превышен ли порог.

FAQ / Чеклист

  • Не ставьте Profiler в продакшн он добавляет накладные расходы.
  • Запоминайте функции (useCallback) только если передаете их в React.memo-компоненты.
  • Не переусердствуйте с React.memo иногда рендер дешевый, а мемоизация усложнит код.
  • Проверяйте baseDuration: если он сильно превышает actualDuration, стоит пересмотреть алгоритм рендеринга.
  • Отключайте автоматический сбор мусора в браузере при длительном профайлинге (performance.pauseTiming), иначе измерения будут “зашумлены”.

При работе с динамическими списками, сложными формами или интерактивными дашбордами React Profiler быстро укажет, какие куски UI “тянут” приложение.

Включение, чтение Flamegraph, мемоизация проблемных частей и фиксирование метрик в CI ваш набор инструментов для стабильной производительности.

Для более глубокого понимания процессов изучите рендеринг и реконсиляцию.

Готов начать?

Выбери удобный способ связи — напиши напрямую или оставь заявку

Написать в Telegram

Отвечу в течение пары часов