c/dev@DevOpserMay 28, 2026 at 12:38 PM

Почему React-компонент перерендеривается чаще, чем нужно

ChatGPT Image 28 мая 2026 г., 05_48_25.png

Наверное, многие через это проходили: ставишь в компонент console.log, кликаешь по кнопке — и видишь, что компонент рендерится снова. Потом ещё раз. И ещё.

TypeScript
function Card() {
  console.log('Card render');

  return <div>Карточка</div>;
}

Сначала может показаться, что React делает что-то лишнее. Особенно если дочерний компонент перерендерился, хотя его данные вроде бы вообще не менялись.

Но сам по себе ререндер — не всегда проблема. В React это обычная часть работы приложения. Вопрос не в том, сколько раз компонент вызвался, а в том, стал ли из-за этого интерфейс заметно тормозить.

Ререндер — это повторный вызов функции компонента. React снова получает JSX, сравнивает его с предыдущим результатом и решает, нужно ли реально менять DOM. Поэтому важно не путать ререндер компонента и изменение DOM. Компонент может вызваться заново, но на странице визуально ничего не изменится.

Посмотрим на простой пример:

TypeScript
import { useState } from 'react';

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

  console.log('App render');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>

      <UserCard name="Johnny" />
    </div>
  );
}

function UserCard({ name }: { name: string }) {
  console.log('UserCard render');

  return <div>Пользователь: {name}</div>;
}

При клике на кнопку изменится count, поэтому app перерендерится. Вместе с ним React снова вызовет и UserCard, хотя name остался тем же самым.

В консоли будет что-то вроде:

TypeScript
App render
UserCard render

На маленьком компоненте это нормально. Такой ререндер почти ничего не стоит. Но если внутри дочернего компонента большой список, сложная карточка, редактор текста, графики или тяжёлая логика, тогда уже есть смысл смотреть внимательнее.

Чтобы не вызывать компонент повторно, если его пропс не изменились, можно использовать memo:

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

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

  console.log('App render');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>

      <UserCard name="Johnny" />
    </div>
  );
}

const UserCard = memo(function UserCard({ name }: { name: string }) {
  console.log('UserCard render');

  return <div>Пользователь: {name}</div>;
});

Теперь при изменении count родительский компонент обновится, а UseCard React сможет пропустить, потому что его пропс не поменялись.

Но с memo есть частый нюанс. Он сравнивает пропс поверхностно. Если передать в компонент новую функцию, новый объект или новый массив, React будет считать, что пропсы изменились.

Например:

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

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

  const handleOpen = () => {
    console.log('Открыть профиль');
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>

      <UserCard name="Johnny" onOpen={handleOpen} />
    </div>
  );
}

const UserCard = memo(function UserCard({
  name,
  onOpen,
}: {
  name: string;
  onOpen: () => void;
}) {
  console.log('UserCard render');

  return (
    <div>
      <div>Пользователь: {name}</div>
      <button onClick={onOpen}>Открыть</button>
    </div>
  );
});

Здесь UserCard всё равно будет ререндериться при клике. Причина в том, что hadnleOpen создаётся заново при каждом рендере App.

Для нас функция выглядит такой же, но для JavaScript это уже другая ссылка.

В таких случаях можно использовать useCallback:

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

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

  const handleOpen = useCallback(() => {
    console.log('Открыть профиль');
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>

      <UserCard name="Johnny" onOpen={handleOpen} />
    </div>
  );
}

const UserCard = memo(function UserCard({
  name,
  onOpen,
}: {
  name: string;
  onOpen: () => void;
}) {
  console.log('UserCard render');

  return (
    <div>
      <div>Пользователь: {name}</div>
      <button onClick={onOpen}>Открыть</button>
    </div>
  );
});

useCallback не делает функцию быстрее. Он просто сохраняет ссылку на неё между рендерами, пока не изменились зависимости.

Похожая история бывает с объектами и массивами:

TypeScript
const user = {
  name: 'Johnny',
  role: 'moderator',
};

Если такой объект создаётся внутри компонента, то при каждом ререндере он будет новым объектом. Даже если внутри те же самые значения.

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

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

  const user = {
    name: 'Johnny',
    role: 'moderator',
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>

      <UserCard user={user} />
    </div>
  );
}

const UserCard = memo(function UserCard({
  user,
}: {
  user: { name: string; role: string };
}) {
  console.log('UserCard render');

  return (
    <div>
      {user.name} — {user.role}
    </div>
  );
});

Даже с memo компонент будет обновляться, потому что user каждый раз получает новую ссылку.

Можно завернуть создание объекта в useMemo:

TypeScript
import { memo, useMemo, useState } from 'react';

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

  const user = useMemo(() => {
    return {
      name: 'Johnny',
      role: 'moderator',
    };
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>

      <UserCard user={user} />
    </div>
  );
}

const UserCard = memo(function UserCard({
  user,
}: {
  user: { name: string; role: string };
}) {
  console.log('UserCard render');

  return (
    <div>
      {user.name} — {user.role}
    </div>
  );
});

Хотя чаще useMemo полезен не ради маленьких объектов, а ради вычислений, которые действительно могут быть дорогими:

TypeScript
const filteredPosts = useMemo(() => {
  return posts.filter((post) =>
    post.title.toLowerCase().includes(search.toLowerCase())
  );
}, [posts, search]);

Так фильтрация будет запускаться только тогда, когда изменились posts или search.

Но здесь важно не увлечься. Memo, useCallback и useMemo не нужно расставлять везде подряд. Иногда они делают код сложнее, а реального выигрыша не дают.

Обычно я бы смотрел на оптимизацию тогда, когда есть заметная проблема: список долго появляется, ввод в поле начинает лагать, карточки тяжело отрисовываются, компонент часто получает большие массивы или внутри есть дорогие вычисления.

А если компонент маленький, вроде такого:

TypeScript
function Badge({ text }: { text: string }) {
  return <span>{text}</span>;
}

то оптимизировать его чаще всего вообще не имеет смысла. Да, он может перерендериться. Но это настолько дешёвая операция, что дополнительная оптимизация может быть просто костылями в коде.

Ещё один момент: в dev-режиме с React.Strcitmode некоторые вызовы могут происходить дважды. Это сделано специально, чтобы проще находить побочные эффекты. Поэтому если вы видите лишний console.log в разработке, это не всегда значит, что на проде будет то же самое.

Для себя можно держать простое правило: сначала пишем понятный код, потом смотрим на реальные тормоза, потом измеряем и только после этого оптимизируем точечно.

console.log полезен, чтобы заметить ререндеры. Но окончательно лучше проверять через React DevTools Profiler: там видно, какие компоненты действительно занимают время.

А вы обычно оптимизируете компоненты заранее или сначала ждёте, когда вылезут реальные лаги?

3
10

Discussion

0 comments

Log in to write, edit, and rate comments

G
0 characters
Loading comments...

We use cookies

Cookies help keep you signed in, save interface settings, and improve how the site works. Read the Cookie Policy.