ГлавнаяБлогПочему React тормозит: состояние в неправильном месте
Алгоритмы

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

Узнайте, как неправильное размещение состояния в React вызывает лаги при вводе. Простое решение без useMemo — переместите useState ниже. Попробуйте прямо сейчас!

Al
Редакция Algolitalgolit.ru
8 мин чтения7 июля 2026 г.

Одно поле поиска сломало весь мой React-дашборд. Каждое нажатие клавиши запаздывало почти на 300 мс. Моя первая мысль — мемоизировать всё: useMemo, useCallback, обернуть половину приложения в React.memo. Это была неправильная мысль.

Приложение было не огромным: около 150 компонентов — боковая панель, шапка, таблица, графики, фильтры. Но когда я печатал в поиске, ввод отставал на 300 мс. React DevTools показали, что одно нажатие клавиши перерисовывало весь экран: поиск, таблицу, шапку, боковую панель, графики и даже подвал.

Типичная ошибка с состоянием в React

Вот упрощённая версия того, что было:

import { useState } from 'react';

export default function Dashboard() {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div className="layout">
      <Header />
      <Sidebar />
      <main>
        <SearchBar value={searchQuery} onChange={setSearchQuery} />
        <DataGrid query={searchQuery} />
        <HeavyCharts />
        <Footer />
      </main>
    </div>
  );
}

На первый взгляд не страшно. Состояние поиска принадлежит странице, верно? Это была ловушка. Состояние жило в Dashboard, поэтому каждое изменение поиска обновляло Dashboard. React рендерит компонент и всех его детей, если не сработает оптимизация. Так каждое нажатие клавиши заставляло весь дашборд участвовать в рендере.

Профилировщик до исправления показывал 142.7 мс на нажатие. Для плавных 60 FPS нужно около 16 мс на кадр — мой поиск тратил почти 9 кадров на рендер. Неудивительно, что было медленно.

Python уже учил меня этому правилу

В Python вы быстро учитесь не выносить переменные в более широкую область видимости, чем нужно. Если переменная используется только в одной функции, держите её внутри. Если сделать её модульной «на всякий случай», получите путаницу. Состояние React имеет тот же запах. useState внутри Dashboard — не буквально глобальная переменная Python, но ведёт себя как значение с избыточным охватом.

Я задал простой вопрос: кому на самом деле нужен searchQuery? Ответ: только SearchBar и DataGrid.

Исправление: перемещение состояния вниз

Я переместил состояние в компонент, который содержит только поиск и таблицу:

import { useState } from 'react';

function SearchableDataGrid() {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <>
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <DataGrid query={searchQuery} />
    </>
  );
}

export default function Dashboard() {
  return (
    <div className="layout">
      <Header />
      <Sidebar />
      <main>
        <SearchableDataGrid />
        <HeavyCharts />
        <Footer />
      </main>
    </div>
  );
}

Вот и всё изменение. Dashboard снова стал просто разметкой. SearchableDataGrid стал владельцем шумного состояния. Теперь нажатие клавиши рендерит только SearchableDataGrid, SearchBar и DataGrid. Шапка, боковая панель, графики и подвал не перерисовываются.

Время рендера упало с 142.7 мс до 4.1 мс на нажатие. Цифры с моей машины, но суть ясна: работа по рендеру перестала распространяться по всей странице.

Почему мемоизация была неправильным первым шагом

React.memo, useMemo и useCallback — хорошие инструменты, но я тянулся к ним слишком рано. Мемоизация помогает пропустить работу, если входные данные не изменились, но добавляет сложность. Чистое исправление — вообще не запускать эти компоненты.

Теперь я отлаживаю производительность React в таком порядке:

  1. Не слишком ли высоко поднято состояние?
  2. Выполняется ли дорогая работа во время рендера?
  3. Можно ли отложить или разбить дорогую работу?
  4. Это наконец настоящая проблема для мемоизации?

Этот порядок спасает от украшения плохой структуры умными хуками.

Когда этот трюк не помогает

Если сама таблица дорогая, перемещение состояния не спасёт. Например, если DataGrid фильтрует 10 000 строк на каждое нажатие, поможет useDeferredValue:

import { useDeferredValue, useState } from 'react';

function SearchableDataGrid() {
  const [searchQuery, setSearchQuery] = useState('');
  const deferredQuery = useDeferredValue(searchQuery);

  return (
    <>
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <DataGrid query={deferredQuery} />
    </>
  );
}

Это сохраняет отзывчивость ввода, пока React обновляет тяжёлое представление с низким приоритетом.

Бывают случаи, когда состояние действительно должно жить выше. Если шапке нужно показывать «23 совпадения», а боковая панель должна синхронизироваться с тем же запросом, перемещение состояния вниз может усложнить поток данных. Тогда стоит поднять его обратно, использовать контекст для небольшой общей области или крошечное внешнее хранилище.

И да, иногда React.memo — правильный инструмент:

import React from 'react';

const HeavyCharts = React.memo(function HeavyCharts() {
  // Дорогой рендер графиков, не зависящий от searchQuery.
  return <Charts />;
});

Но пусть это не будет вашим первым рефлексом.

Маленький чеклист, который я использую сейчас

Когда React-взаимодействие кажется медленным, я открываю Profiler до изменения кода. Записываю взаимодействие один раз. Затем спрашиваю:

  • Что изменилось?
  • Какие компоненты отрендерились?
  • Какие из этих рендеров были на самом деле нужны?
  • Медленная часть — лишний рендер или дорогая работа внутри компонента, который действительно должен был рендериться?

Если вся страница загорается после одного нажатия, ищу состояние, сидящее слишком высоко в дереве. Если загорается только нужный компонент, но он всё равно работает 100 мс, смотрю внутрь него.

Что меня раздражало

Исправление не было сложным. Не новая библиотека, не хитрый хук. Это была скучная ошибка области видимости. Я относился к строке поиска как к состоянию уровня страницы, потому что так было удобно. React сделал именно то, что я попросил, а я час винил рендеринг.

Версия урока на Python: если переменная принадлежит функции, не кладите её в модуль. Версия на React: если состояние принадлежит двум компонентам, не заставляйте всю страницу нести его.

Была ли у вас такая ошибка производительности, когда исправление оказалось крошечным? Особенно интересны случаи, когда профилировщик заставлял чувствовать себя лично задетым.

#производительность React#состояние#рендеринг#мемоизация#оптимизация
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →