React performance profiling: как искать узкие места без гадания
Разбираем React performance profiling на практике: Profiler, browser Performance, commit time, лишние ререндеры, тяжелые вычисления и системный поиск узких мест.
- Введение
- Что именно считается узким местом в React
- Архитектура диагностики: как не перепутать React-проблему с браузерной
- Контекст задачи
- Схема компонентов и поток данных
- Границы ответственности
- Точка отказа и деградация
- С чего начинать profiling на практике
- 1. Зафиксируйте симптом
- 2. Выберите метрику
- 3. Воспроизведите в правильной сборке
- 4. Снимите минимальный, но чистый профиль
- React DevTools Profiler: как читать профиль без самообмана
- На что обратить внимание в первую очередь
- Что означает commit
- Flamegraph и ranked view
- Код-пример 1: лишние ререндеры из-за нестабильных props
- Код-пример 2: дорогой render из-за вычисления в компоненте
- Сравнение инструментов profiling
- Production pitfalls: где profiling ломают чаще всего
- Ошибка 1. Профилировать без чётко определённого сценария
- Ошибка 2. Делать выводы по dev-сборке как по production
- Ошибка 3. Сразу добавлять memoization everywhere
- Ошибка 4. Игнорировать широкий context
- Ошибка 5. Не отделять render от layout/paint
- Разбор производительности: как принимать решение по результатам профиля
- 1. Это стоимость одного обновления или частота обновлений
- 2. Где компромисс между CPU, памятью и сложностью кода
- 3. Можно ли сократить область обновления архитектурно
- 4. Улучшение видно пользователю или только инженеру
- Практики, которые реально помогают
- 1. Профилируйте действие, а не страницу целиком
- 2. Держите рядом React Profiler и browser Performance
- 3. Проверяйте changed props/hooks, а не только время
- 4. Сначала сокращайте работу, потом кешируйте
- 5. Валидируйте эффект в production build
- Частые ошибки
- Как отвечать на интервью
- FAQ
- React Profiler показывает дорогой компонент. Значит, проблема в нем?
- Когда лучше использовать browser Performance, а не только React DevTools?
- Как понять, что useMemo действительно нужен?
- Почему один и тот же экран может быть быстрым локально, но медленным у пользователей?
- Можно ли профилировать React-приложение только через Lighthouse?
- Итоги
Введение
React performance profiling обычно начинают слишком поздно. Сначала команда замечает, что ввод в поле фильтра стал неотзывчивым, список дёргается при прокрутке, модалка открывается с задержкой, а потом в коде появляется хаотичный набор React.memo, useMemo и кастомных сравнений props.
Проблема в том, что оптимизация без профиля почти всегда лечит не причину, а симптом. React-приложение может тормозить из-за лишних ререндеров, тяжелого commit, синхронного вычисления в render, проблем layout/paint или просто из-за слишком медленного запроса. Для базовой модели, когда компонент вообще перерисовывается, полезно ознакомиться с разбор механики ререндера в React.
В этой статье разберем, как искать узкие места системно: что измерять, где заканчивается зона ответственности React DevTools Profiler, как читать commit и flamegraph, когда мемоизация оправдана, а когда вы только усложняете код.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что именно считается узким местом в React
Если говорить инженерно, узкое место - это не просто "что-то медленно". Это участок, который стабильно ограничивает отклик экрана при конкретном сценарии: печать в поле, раскрытие дерева, переключение таба, drag-and-drop, массовый апдейт таблицы.
У React-приложения обычно есть пять классов проблем:
- Слишком частые ререндеры большого поддерева.
- Дорогой render из-за тяжелых вычислений.
- Дорогой commit: layout, DOM-мутации, эффекты после коммита.
- Блокировка main thread вне React: сериализация, парсинг, сторонний скрипт.
- Сетевой или серверный лаг, который ошибочно списали на React.
Из-за этого фраза "React тормозит" почти бесполезна. Нужно уточнять: тормозит ввод, первый показ экрана, переключение фильтра, гидратация после SSR или виртуализированный список после сортировки. Без этого вы профилируете приложение в целом, а не конкретный дефект.
Архитектура диагностики: как не перепутать React-проблему с браузерной
Контекст задачи
У React нет эксклюзивного доступа к производительности страницы. Один и тот же лаг может быть вызван разными слоями:
- React пересчитывает слишком большое дерево.
- браузер тратит время на layout и paint;
useEffectзапускает каскад запросов;- таблица сортирует 20 000 строк синхронно;
- аналитика блокирует поток длинным обработчиком.
Поэтому диагностику нужно строить как конвейер, а не как один запуск Profiler.
Схема компонентов и поток данных
Для типичного экрана с фильтрами и списком поток выглядит так:
- Пользователь печатает в input.
- Обновляется локальный state формы.
- Родитель перерисовывает панель фильтров и список.
- Селекторы или вычисления пересчитывают данные.
- React делает commit.
- Браузер выполняет layout, paint и compositing.
- Эффекты после коммита могут запускать синхронизацию URL, аналитику или fetch.
Узкое место может находиться на любом шаге. Если вы смотрите только на React Profiler, можно пропустить дорогой layout. Если смотрите только на вкладку Performance, можно не увидеть каскад ререндеров по props/context, поэтому следует использовать оба инструмента.
Границы ответственности
React DevTools Profilerотвечает на вопрос: какие компоненты обновились и сколько времени React потратил на render/commit.- Browser
Performanceотвечает на вопрос: чем был занят main thread целиком. - Network объясняет, не упирается ли экран в медленный запрос.
- Бизнес-метрика или UX-метрика отвечает, стоит ли вообще оптимизировать этот сценарий.
Точка отказа и деградация
Частая точка отказа в команде - смотреть только на "красный" компонент во flamegraph и пытаться оптимизировать его с помощью мемоизации в отрыве от контекста. Но дорогой leaf-компонент часто лишь симптом. Источник каскада может лежать выше: нестабильные props, широкий context, смена key, пересоздание массива, новый callback на каждом вводе. Именно поэтому полезно понимать, как работает движок рендеринга React Fiber.
С чего начинать profiling на практике
Нормальный процесс начинается не с инструмента, а со сценария.
1. Зафиксируйте симптом
Плохо: "страница иногда лагает".
Хорошо: "при вводе в поле поиска после 8-10 символов задержка между нажатием клавиши и обновлением списка доходит до 180-220 мс на MacBook Air M1 в Chrome".
Чем точнее сценарий, тем проще сравнивать профили до и после правки.
2. Выберите метрику
Для React-интерфейсов обычно полезны:
- время отклика на ввод;
- длительность commit;
- число обновившихся компонентов на одно действие;
- длина long task в main thread;
- p95 для конкретного пользовательского действия.
Если метрики нет, вы не докажете, что оптимизация помогла.
3. Воспроизведите в правильной сборке
Для поиска структуры ререндеров удобна dev-сборка и React DevTools Profiler. Для финального подтверждения пользы нужен production build, потому что dev-режим и Strict Mode добавляют шум. Это особенно важно, если вы уже встречали дубли эффектов и ререндеров в разработке; отдельный разбор этого поведения есть в статье про двойной рендер в dev режиме.
4. Снимите минимальный, но чистый профиль
Не открывайте сразу десять вкладок и не пытайтесь записать пятиминутную сессию. Нужен короткий сценарий: старт записи, одно проблемное действие, стоп. Иначе профиль зарастает шумом.
React DevTools Profiler: как читать профиль без самообмана
React DevTools удобен тем, что показывает проблему на уровне дерева компонентов. Подробный практический разбор самого инструмента есть в отдельной статье про React DevTools, а здесь сосредоточимся на профилировании.
На что обратить внимание в первую очередь
После записи профиля не надо сразу искать самый "красивый" flamegraph. Сначала ответьте на три вопроса:
- Какое действие пользователя вы профилировали?
- Сколько commit произошло на одно действие?
- Какие крупные поддеревья обновились без явной пользы для UI?
Если на одно нажатие клавиши приходится 6-8 commit, это уже сигнал. Причина может быть в локальном state, URL sync, context provider, эффекте с setState или сторонней библиотеке формы.
Что означает commit
Commit - это момент, когда React применяет изменения после render-фазы. Слишком дорогой commit опасен тем, что пользователь пользователь ощущает задержку. Если render дешевый, а commit дорогой, ищите:
- массовые DOM-мутации;
- сложный layout после изменения размеров;
- дорогие эффекты после коммита;
- пересчет стилей;
- animation/layout thrashing.
Flamegraph и ranked view
Flamegraph полезен, когда нужно увидеть форму каскада: от какого родителя пошла волна обновлений. Ranked полезен, когда нужен список самых дорогих компонентов по времени. Использовать только один режим неудобно:
Flamegraphхорошо показывает структуру.Rankedхорошо показывает цену.
Сильная привычка: сначала найти дорогой commit, потом посмотреть, какие компоненты были дорогими, а затем отдельно проверить, были ли они действительно обязаны обновляться.
Код-пример 1: лишние ререндеры из-за нестабильных props
Ниже типичная история: список большой, элемент обернут в React.memo, но это не помогает.
type RowProps = {
item: Item;
onSelect: (id: string) => void;
style: React.CSSProperties;
};
const Row = React.memo(function Row({ item, onSelect, style }: RowProps) {
return (
<div style={style} onClick={() => onSelect(item.id)}>
{item.title}
</div>
);
});
function Results({ items }: { items: Item[] }) {
return (
<>
{items.map((item) => (
<Row
key={item.id}
item={item}
onSelect={(id) => analytics.track("select", { id })}
style={{ padding: 8 }}
/>
))}
</>
);
}
Проблема здесь не в Row, а в родителе. На каждом рендере создаются новый callback и новый объект style, поэтому React.memo не может стабильно отсечь обновления. В профиле вы увидите, что почти все строки "обязаны" обновляться по изменившимся props, хотя визуально список тот же.
Исправление выглядит так:
const rowStyle = { padding: 8 };
function Results({ items }: { items: Item[] }) {
const handleSelect = (id: string) => {
analytics.track("select", { id });
};
return (
<>
{items.map((item) => (
<Row
key={item.id}
item={item}
onSelect={handleSelect}
style={rowStyle}
/>
))}
</>
);
}
Но даже это не универсальная рекомендация. Если список короткий и лагов нет, такой рефакторинг может быть преждевременным. Подробно компромиссы мемоизации разобраны в статье про React.memo, useMemo и useCallback.
Код-пример 2: дорогой render из-за вычисления в компоненте
Иногда профиль показывает не каскад, а один объективно тяжелый render.
function SearchResults({
items,
query,
}: {
items: Product[];
query: string;
}) {
const visibleItems = items
.filter((item) => item.title.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => score(b, query) - score(a, query));
return <ResultsList items={visibleItems} />;
}
Если items содержит тысячи записей, то filter + sort + score на каждое нажатие клавиши быстро превысят лимит времени, отведённый на обработку кадра. Здесь узкое место уже не в количестве ререндеров, а в цене одного рендера.
Более устойчивый вариант:
function SearchResults({
items,
query,
}: {
items: Product[];
query: string;
}) {
const deferredQuery = useDeferredValue(query);
const visibleItems = useMemo(() => {
const normalized = deferredQuery.trim().toLowerCase();
return items
.filter((item) => item.title.toLowerCase().includes(normalized))
.sort((a, b) => score(b, normalized) - score(a, normalized));
}, [items, deferredQuery]);
return <ResultsList items={visibleItems} />;
}
Здесь важно не само наличие useMemo, а аргументация:
- вычисление действительно дорогое;
- результат нужен повторно;
- входные зависимости понятны;
- отложенный query улучшает отзывчивость ввода.
Если список совсем большой, следующим шагом будет не новая мемоизация, а виртуализация или перенос вычисления на другой слой.
Сравнение инструментов profiling
| Инструмент | Что показывает хорошо | Что не показывает | Когда использовать |
|---|---|---|---|
| React DevTools Profiler | Ререндеры, commit, дерево обновлений, изменившиеся props/hooks | Layout, paint, сеть, сторонние long tasks | Когда подозреваете проблему внутри React-дерева |
| Browser Performance | Main thread целиком, layout, paint, scripting, long tasks | Причины React-каскада на уровне props/context | Когда UI лагает, а источник неочевиден |
| Network | Медленные запросы, waterfall, блокирующие ответы | Стоимость render и commit | Когда экран зависит от fetch или SSR/RSC данных |
| User Timing / custom marks | Свою бизнес-метрику и границы сценария | Автоматический анализ React-компонентов | Когда нужен замер конкретного действия пользователя |
| Логи и продуктовые метрики | Масштаб, p95/p99, частота проблемы | Локальная первопричина в дереве компонентов | Когда надо понять приоритет и влияние на UX |
Эта таблица полезна и для интервью, и для реальной команды. Слабый ответ звучит как "я открою Profiler". Сильный - "я сначала отделю React-профиль от браузерного профиля и проверю, не упирается ли сценарий в сеть или layout".
Production pitfalls: где profiling ломают чаще всего
Ошибка 1. Профилировать без чётко определённого сценария
Симптом: у команды много скриншотов flamegraph, но нет ответа, что именно тормозит.
Последствие: случайные правки без подтвержденной пользы.
Как заметить заранее: разные инженеры снимают разные профили и спорят о разных узких местах.
Ошибка 2. Делать выводы по dev-сборке как по production
Симптом: "в DevTools commit 40 мс, значит в проде тоже плохо".
Последствие: ложные приоритеты и борьба с noise от dev-режима.
Как заметить заранее: профиль не сравнивали с production build на том же сценарии.
Ошибка 3. Сразу добавлять memoization everywhere
Симптом: код обрастает React.memo, useMemo, useCallback, а лаг остается.
Последствие: поддержка усложняется, чтение кода ухудшается, эффект минимальный.
Как заметить заранее: никто не может назвать конкретный commit, который должен был улучшиться.
Ошибка 4. Игнорировать широкий context
Симптом: изменение маленького флага перерисовывает большой экран.
Последствие: каскад обновлений, который не лечится локальным memo.
Как заметить заранее: в профиле обновляются десятки дочерних компонентов после апдейта provider.
Ошибка 5. Не отделять render от layout/paint
Симптом: React commit выглядит терпимо, а интерфейс все равно дергается.
Последствие: команда оптимизирует JSX, хотя проблема в браузерной фазе.
Как заметить заранее: во вкладке Performance длинные блоки идут после commit и связаны с layout/style recalculation.
Разбор производительности: как принимать решение по результатам профиля
После снятия профиля надо не просто сказать "нашли медленный компонент", а принять решение. Я обычно проверяю четыре вопроса.
1. Это стоимость одного обновления или частота обновлений
Если один render стоит 12-15 мс, но происходит редко, проблема может быть не критична. Если render стоит 2 мс, но запускается 80 раз на одно действие, источник уже в каскаде обновлений.
2. Где компромисс между CPU, памятью и сложностью кода
Мемоизация снижает CPU-стоимость, но добавляет:
- кэш и удержание памяти;
- более хрупкие зависимости;
- риск stale-логики;
- рост когнитивной сложности.
Поэтому useMemo оправдан не по эстетике, а по профилю.
3. Можно ли сократить область обновления архитектурно
Часто лучший фикс - не локальная микрооптимизация, а изменение границы состояния:
- перенести state ближе к потребителю;
- разделить context;
- разбить экран на меньшие поддеревья;
- виртуализировать длинный список;
- отложить обновление второстепенного UI с помощью concurrent-механизмов.
Если нужна более широкая картина по системной оптимизации, ее удобно читать рядом с разбором React optimization для middle-интервью и объяснением concurrent rendering.
4. Улучшение видно пользователю или только инженеру
Это самый трезвый вопрос. Сократить commit с 5 мс до 3 мс приятно, но бизнесу и пользователю это может быть безразлично. А вот убрать ввод с лагом 180 мс до 40 мс - уже реальное улучшение UX.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Практики, которые реально помогают
1. Профилируйте действие, а не страницу целиком
Открытие модалки, печать в поле, drag элемента, раскрытие аккордеона - у каждого действия свой профиль. Усредненный "профиль страницы" слишком шумный.
2. Держите рядом React Profiler и browser Performance
Первый отвечает за React-дерево, второй - за весь поток. Вместе они быстро отсекают ложные гипотезы.
3. Проверяйте changed props/hooks, а не только время
Дорогой компонент - это полпроблемы. Важно понять, почему он обновился. Если причина - новый объект, новый callback или широкий provider, лечить надо источник.
4. Сначала сокращайте работу, потом кешируйте
Удалить лишний ререндер почти всегда лучше, чем кешировать его последствия. Сначала уменьшайте область обновления и число операций, потом рассматривайте memoization.
5. Валидируйте эффект в production build
Оптимизация считается завершенной только после повторного профиля на том же сценарии и в той сборке, которая ближе к реальному пользователю.
Частые ошибки
- Считать, что самый дорогой компонент в
Rankedавтоматически и есть первопричина. - Путать дорогой render с дорогим commit и лечить не ту фазу.
- Оптимизировать короткий список на 20 элементов так же агрессивно, как таблицу на 10 000 строк.
- Сравнивать профили, снятые на разных сценариях и в разном окружении.
- Оставлять в коде мемоизацию, которая ничего не изменила по метрике.
- Игнорировать
key, context и структуру state, концентрируясь только наuseMemo.
Как отвечать на интервью
Сильный ответ по теме React performance profiling звучит примерно так:
- Я начинаю с воспроизводимого пользовательского сценария и целевой метрики.
- Снимаю React Profiler, чтобы увидеть commit и дерево ререндеров.
- Если источник неочевиден, иду во вкладку Performance и проверяю layout, paint, long tasks и сеть.
- Отделяю частоту обновлений от стоимости одного обновления.
- Только после этого выбираю решение: сузить state, разбить context, виртуализировать список, убрать лишние props или добавить точечную мемоизацию.
- Подтверждаю результат повторным профилем и объясняю компромисс по сложности кода.
Для middle/senior уровня полезно отдельно проговорить, что profiling - это не поиск "волшебного хука", а проверка гипотез на данных. Такой ответ обычно сильнее, чем перечисление React.memo, useMemo, useCallback по памяти.
Отработайте React performance-вопросы на живых кейсах
Тренируем диагностику ререндеров, Profiler, memoization и архитектурные trade-off так, как это спрашивают на реальных технических интервью.
FAQ
React Profiler показывает дорогой компонент. Значит, проблема в нем?
Не всегда. Он может быть дорогим по времени, но обновляться из-за родителя, context provider или нестабильных props. Нужно проверить причину обновления, а не только стоимость компонента.
Когда лучше использовать browser Performance, а не только React DevTools?
Когда есть ощущение общей вязкости интерфейса, layout shift, долгий paint, долгие JS task или подозрение на сторонние скрипты. React Profiler не покрывает весь main thread.
Как понять, что useMemo действительно нужен?
Должно выполняться сразу несколько условий: вычисление дорогое, вызывается часто, входы предсказуемы, а профиль до и после показывает измеримую пользу. Если одного из пунктов нет, мемоизация часто преждевременна.
Почему один и тот же экран может быть быстрым локально, но медленным у пользователей?
Потому что у пользователей другой CPU, вкладки, расширения, объем данных, сеть и конкуренция за main thread. Локальный профиль нужен, но его надо сверять с продуктовой телеметрией и production-сценариями.
Можно ли профилировать React-приложение только через Lighthouse?
Нет. Lighthouse полезен для общих аудитов, но он не заменяет сценарное профилирование конкретного пользовательского действия и не объясняет структуру ререндеров внутри React-дерева.
Итоги
React performance profiling полезен только тогда, когда вы ищете не "медленный компонент вообще", а узкое место в конкретном сценарии. Для этого нужна последовательность: воспроизвести симптом, выбрать метрику, снять React-профиль, сверить его с браузерным профилем и только потом оптимизировать.
Практический результат такого подхода простой: меньше случайной мемоизации, меньше спорных правок "на глаз", быстрее локализация проблем и более понятные инженерные решения. Если профилирование сделано правильно, оптимизация становится не ритуалом, а обычной диагностикой.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Автор
Lexicon Team
Читайте также
frontend
System design для frontend разработчика: как мыслить системно, а не только компонентами
Практический разбор system design для frontend разработчика: границы ответственности, данные, производительность, ошибки и сильный ответ на интервью.
frontend
React anti-patterns: 15 ошибок разработчиков, которые ломают поддержку и производительность
Разбираем React anti-patterns: 15 типичных ошибок в state, рендерах, эффектах и архитектуре. С примерами кода, trade-off и ответами для собеседования.
frontend
React архитектура больших приложений: как не утонуть в слоях, состояниях и фичах
Практический разбор React архитектуры больших приложений: слои, feature-модули, state management, performance, ошибки и критерии выбора.