Как предотвратить лишние ре-рендеры в React: полное руководство

16 марта 2026
Автор

Олег Марков

Предотвращение лишних ре-рендеров

React автоматически ре-рендерит компонент при изменении его state или пропсов, а также при ре-рендере родителя. Понимание когда и почему происходят ре-рендеры — ключ к оптимизации.

Почему происходят ре-рендеры

Причины ре-рендера:
1. Изменилось состояние компонента (setState)
2. Изменились пропсы компонента
3. Ре-рендерился родительский компонент
4. Изменился контекст (useContext)
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+{count}</button>
      {/* Child ре-рендерится при КАЖДОМ нажатии, хотя его пропсы не меняются */}
      <Child name="Алиса" />
    </div>
  );
}

function Child({ name }: { name: string }) {
  console.log('Child ре-рендер'); // Выводится каждый раз!
  return <div>Привет, {name}</div>;
}

React.memo — остановить ре-рендер от родителя

React.memo запоминает результат рендера и пропускает повторный рендер если пропсы не изменились:

import { memo, useState } from 'react';

// ✅ Child ре-рендерится только при изменении name
const Child = memo(function Child({ name }: { name: string }) {
  console.log('Child ре-рендер');
  return <div>Привет, {name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+{count}</button>
      <Child name="Алиса" /> {/* Не ре-рендерится при изменении count */}
    </div>
  );
}

Почему React.memo не всегда помогает

Проблема возникает когда пропс — это объект или функция, создающиеся заново при каждом рендере родителя:

function Parent() {
  const [count, setCount] = useState(0);

  // ❌ Новый объект при каждом рендере — Child всегда ре-рендерится!
  const config = { theme: 'dark', size: 'lg' };

  // ❌ Новая функция при каждом рендере — Child всегда ре-рендерится!
  const handleClick = () => console.log('click');

  return <MemoizedChild config={config} onClick={handleClick} />;
}

useCallback — стабилизация функций

import { useCallback, memo, useState } from 'react';

const Button = memo(function Button({
  onClick,
  label
}: {
  onClick: () => void;
  label: string;
}) {
  console.log('Button ре-рендер:', label);
  return <button onClick={onClick}>{label}</button>;
});

function TodoList() {
  const [todos, setTodos] = useState<string[]>([]);
  const [input, setInput] = useState('');

  // ✅ Стабильная функция — Button не ре-рендерится без нужды
  const handleAdd = useCallback(() => {
    setTodos(prev => [...prev, input]);
    setInput('');
  }, [input]); // Обновляется только при изменении input

  // ✅ Функция с updater — без зависимостей
  const handleClear = useCallback(() => {
    setTodos([]);
  }, []); // Создаётся один раз

  return (
    <>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <Button onClick={handleAdd} label="Добавить" />
      <Button onClick={handleClear} label="Очистить" />
      <ul>{todos.map((t, i) => <li key={i}>{t}</li>)}</ul>
    </>
  );
}

useMemo — стабилизация объектов и значений

import { useMemo, memo } from 'react';

interface ChartProps {
  config: { theme: string; showGrid: boolean };
  data: number[];
}

const Chart = memo(function Chart({ config, data }: ChartProps) {
  console.log('Chart ре-рендер');
  return <div>График: {JSON.stringify(config)}</div>;
});

function Dashboard() {
  const [theme, setTheme] = useState('dark');
  const [count, setCount] = useState(0);

  // ✅ Стабильный объект — Chart не ре-рендерится при изменении count
  const chartConfig = useMemo(
    () => ({ theme, showGrid: true }),
    [theme] // Пересоздаётся только при изменении theme
  );

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Счётчик: {count}</button>
      <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
        Тема: {theme}
      </button>
      <Chart config={chartConfig} data={[1, 2, 3]} />
    </>
  );
}

Структура состояния для предотвращения ре-рендеров

Разнесение состояния (State Splitting)

// ❌ Плохо — всё в одном компоненте, любое изменение ре-рендерит весь список
function App() {
  const [items, setItems] = useState<Item[]>([]);
  const [filter, setFilter] = useState('');
  const [selectedId, setSelectedId] = useState<number | null>(null);

  return (
    <>
      <FilterInput value={filter} onChange={setFilter} />
      {/* Весь список ре-рендерится при изменении selectedId */}
      <ItemList items={items} filter={filter} selectedId={selectedId} onSelect={setSelectedId} />
    </>
  );
}

// ✅ Хорошо — состояние выбора изолировано в отдельном компоненте
function ItemList({ items, filter }: { items: Item[]; filter: string }) {
  const [selectedId, setSelectedId] = useState<number | null>(null);

  const filtered = useMemo(
    () => items.filter(i => i.name.includes(filter)),
    [items, filter]
  );

  return (
    <ul>
      {filtered.map(item => (
        <Item
          key={item.id}
          item={item}
          isSelected={item.id === selectedId}
          onSelect={setSelectedId}
        />
      ))}
    </ul>
  );
}

Вынесение состояния вниз (State Down)

// ❌ Плохо — состояние формы в родителе вызывает ре-рендер всего дерева
function Page() {
  const [inputValue, setInputValue] = useState(''); // Меняется при каждом нажатии клавиши

  return (
    <div>
      <ExpensiveComponent />   {/* Ре-рендерится при каждом нажатии клавиши! */}
      <AnotherExpensiveComponent />
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
    </div>
  );
}

// ✅ Хорошо — состояние формы вынесено в отдельный компонент
function SearchInput() {
  const [inputValue, setInputValue] = useState(''); // Изолированное состояние
  return (
    <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
  );
}

function Page() {
  return (
    <div>
      <ExpensiveComponent />  {/* Не ре-рендерится при вводе в поле */}
      <AnotherExpensiveComponent />
      <SearchInput />         {/* Только этот компонент обновляется */}
    </div>
  );
}

Паттерн: children для изоляции

// ✅ Хорошо — children не ре-рендерится при изменении состояния родителя
function ColorPicker({ children }: { children: React.ReactNode }) {
  const [color, setColor] = useState('blue');

  return (
    <div style={{ background: color }}>
      <button onClick={() => setColor('red')}>Красный</button>
      <button onClick={() => setColor('blue')}>Синий</button>
      {/* children не ре-рендерится при изменении color! */}
      {children}
    </div>
  );
}

function App() {
  return (
    <ColorPicker>
      <ExpensiveComponent />  {/* Не ре-рендерится при смене цвета */}
    </ColorPicker>
  );
}

Контекст и ре-рендеры

Каждый компонент, использующий useContext, ре-рендерится при изменении значения контекста:

// ❌ Плохо — любое изменение темы ре-рендерит всё приложение через один контекст
const AppContext = createContext({ theme: 'light', user: null, count: 0 });

// ✅ Хорошо — разделить контексты по частоте изменений
const ThemeContext = createContext<'light' | 'dark'>('light');
const UserContext = createContext<User | null>(null);
const CountContext = createContext(0);

// ✅ Хорошо — использовать контекст-селектор паттерн
// Компонент ре-рендерится только если нужное значение изменилось
function ThemeButton() {
  const theme = useContext(ThemeContext); // Только тема, не весь контекст
  return <button className={`btn-${theme}`}>Кнопка</button>;
}

Отладка ненужных ре-рендеров

// Хук для визуализации ре-рендеров в development
function useRenderCount(componentName: string) {
  const renderCount = useRef(0);
  renderCount.current++;

  if (process.env.NODE_ENV === 'development') {
    console.log(`${componentName} ре-рендер #${renderCount.current}`);
  }
}

function MyComponent() {
  useRenderCount('MyComponent');
  return <div>Контент</div>;
}
// Проверка что изменило пропсы (для отладки)
function useWhyDidYouUpdate(name: string, props: Record<string, unknown>) {
  const prevProps = useRef<Record<string, unknown>>({});

  useEffect(() => {
    const changedProps = Object.entries(props).reduce((acc, [key, value]) => {
      if (prevProps.current[key] !== value) {
        acc[key] = { before: prevProps.current[key], after: value };
      }
      return acc;
    }, {} as Record<string, unknown>);

    if (Object.keys(changedProps).length > 0) {
      console.log('[why-did-you-update]', name, changedProps);
    }

    prevProps.current = props;
  });
}

Чеклист для предотвращения лишних ре-рендеров

Проблема Решение
Дочерний компонент ре-рендерится с родителем React.memo
Функция-пропс создаётся заново useCallback
Объект/массив-пропс создаётся заново useMemo
Состояние не влияет на часть дерева Вынести состояние вниз (State Down)
Все дочерние ре-рендерятся при изменении Использовать children паттерн
Контекст вызывает массовые ре-рендеры Разделить контексты, изолировать
Тяжёлые вычисления в рендере useMemo для кэширования

Краткое резюме

Инструмент Что предотвращает
React.memo Ре-рендер от родителя при неизменных пропсах
useCallback Создание новой функции при каждом рендере
useMemo Создание нового объекта/вычисление при каждом рендере
State splitting Ре-рендер несвязанных частей UI
children паттерн Ре-рендер children при изменении состояния родителя
Разделение контекста Массовые ре-рендеры от одного контекста

Дополнительные материалы

Стрелочка влевоУправление состоянием в React через ContextuseMemo vs useCallback: подробное руководство по мемоизации в ReactСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с данными
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
StrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOM
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий