← Назад к вопросам

Как устранить лишние перерендеры?

2.0 Middle🔥 251 комментариев
#React#Оптимизация и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

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

Диагностика: найти лишние перерендеры

Способ 1: React DevTools Profiler

  1. Открыть DevTools -> Profiler вкладка
  2. Нажать кнопку записи
  3. Выполнить действие на сайте
  4. Остановить запись
  5. Посмотреть, какие компоненты рендерились и когда

**Способ 2: console.log

function MyComponent({ prop }) {
  console.log("MyComponent рендерится", prop);
  return <div>{prop}</div>;
}

Когда видишь много логов — значит перерендеров много.

Проблема 1: Функция создается каждый раз

Проблема:

function Parent() {
  const handleClick = () => console.log("Клик"); // Новая функция каждый раз!
  
  return (
    <Child onClick={handleClick} />
  );
}

const Child = memo(({ onClick }) => {
  console.log("Child рендерится");
  return <button onClick={onClick}>Клик</button>;
});

// Каждый раз Parent перерендеривается -> новая handleClick -> memo видит новую функцию -> Child перерендеривается

Решение: useCallback

function Parent() {
  const handleClick = useCallback(() => {
    console.log("Клик");
  }, []); // Функция создается один раз
  
  return <Child onClick={handleClick} />;
}

Проблема 2: Объект создается каждый раз

Проблема:

function Parent() {
  const user = { name: "Иван", age: 30 }; // Новый объект каждый раз!
  
  return <UserCard user={user} />;
}

const UserCard = memo(({ user }) => {
  console.log("UserCard рендерится");
  return <div>{user.name}</div>;
});

// Хотя user.name не изменился, объект user — новый -> memo видит изменение -> перерендер

**Решение: useMemo

function Parent() {
  const user = useMemo(() => (
    { name: "Иван", age: 30 }
  ), []); // Объект создается один раз
  
  return <UserCard user={user} />;
}

Проблема 3: State в неправильном месте

Проблема:

function App() {
  const [query, setQuery] = useState("");
  const [filters, setFilters] = useState({});
  const [results, setResults] = useState([]);
  
  // Когда меняется query -> App перерендеривается -> SearchBox, FilterPanel, ResultsList — все перерендеривются
  // Но FilterPanel и ResultsList не используют query!
  
  return (
    <div>
      <SearchBox value={query} onChange={setQuery} />
      <FilterPanel filters={filters} onChange={setFilters} />
      <ResultsList results={results} />
    </div>
  );
}

Решение: поднять state ближе к использованию

function SearchSection() {
  const [query, setQuery] = useState(""); // State здесь, не в App
  return <SearchBox value={query} onChange={setQuery} />;
}

function FilterSection() {
  const [filters, setFilters] = useState({});
  return <FilterPanel filters={filters} onChange={setFilters} />;
}

function ResultsSection({ results }) {
  return <ResultsList results={results} />;
}

function App() {
  const [results, setResults] = useState([]);
  
  return (
    <div>
      <SearchSection /> {/* Перерендеривается только когда query меняется */}
      <FilterSection /> {/* Независимо от query */}
      <ResultsSection results={results} /> {/* Независимо от query */}
    </div>
  );
}

Проблема 4: Меморизация не работает

Проблема:

const UserCard = memo(({ user }) => {
  console.log("UserCard рендерится");
  return <div>{user.name}</div>;
});

function Parent() {
  // user.name не меняется, но объект user новый
  const user = { name: "Иван" };
  
  return <UserCard user={user} />; // memo НЕ поможет!
}

Решение:

function Parent() {
  const user = useMemo(() => ({ name: "Иван" }), []);
  return <UserCard user={user} />; // Теперь memo сработает
}

ИЛИ сделать компонент более гибким:

const UserCard = memo(({ name }) => { // Принимаем примитив вместо объекта
  console.log("UserCard рендерится");
  return <div>{name}</div>;
});

function Parent() {
  const name = "Иван"; // Примитив, меморизация не нужна
  return <UserCard name={name} />;
}

Проблема 5: Context запускает перерендеры

Проблема:

const ThemeContext = createContext();

function App() {
  const [theme, setTheme] = useState("light");
  const [user, setUser] = useState(null);
  
  // Когда user меняется -> App перерендеривается -> value новое -> все потребители Context перерендеривались
  return (
    <ThemeContext.Provider value={{ theme, setTheme, user, setUser }}>
      <Page />
    </ThemeContext.Provider>
  );
}

function Header() {
  const { theme } = useContext(ThemeContext); // Используем только theme
  return <div className={theme}>...</div>;
}

// Когда user меняется -> Header перерендеривается (хотя theme не поменялся)

Решение 1: разделить Contexts

const ThemeContext = createContext();
const UserContext = createContext();

function App() {
  const [theme, setTheme] = useState("light");
  const [user, setUser] = useState(null);
  
  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Теперь Header зависит только от ThemeContext
// Когда user меняется -> Header НЕ перерендеривается

**Решение 2: useMemo для value

function App() {
  const [theme, setTheme] = useState("light");
  const [user, setUser] = useState(null);
  
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
  const userValue = useMemo(() => ({ user, setUser }), [user]);
  
  return (
    <ThemeContext.Provider value={themeValue}>
      <UserContext.Provider value={userValue}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

Проблема 6: Списки без правильных ключей

Проблема:

// ПЛОХО: индекс как ключ
list.map((item, index) => <Item key={index} item={item} />)

// Если удалить первый элемент:
// list[0] теперь другой элемент
// Item с key=0 будет показывать новый элемент (вместо перезагрузки)
// Это может привести к странным багам и лишним перерендерам

Решение:

// ХОРОШО: уникальный ID
list.map(item => <Item key={item.id} item={item} />)

Проблема 7: Логика в render функции

Проблема:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  // Эта логика выполняется при КАЖДОМ рендере!
  const fetchUser = async () => {
    const res = await fetch(`/api/users/${userId}`);
    setUser(await res.json());
  };
  
  return <div>{user?.name}</div>;
}

**Решение: useEffect

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  // Выполняется только когда userId меняется
  useEffect(() => {
    const fetchUser = async () => {
      const res = await fetch(`/api/users/${userId}`);
      setUser(await res.json());
    };
    fetchUser();
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

Инструмент: why-did-you-render

import whyDidYouRender from '@welldone-software/why-did-you-render';

if (process.env.NODE_ENV === 'development') {
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

Теперь в консоли видишь точную причину перерендера каждого компонента.

Чеклист оптимизации

  1. Профилировать — найти реальную проблему
  2. useCallback для колбэков — если они переданы в memo() компоненты
  3. useMemo для объектов/массивов — если они переданы в memo() компоненты
  4. Разделить state — не держать всё в одном месте
  5. Разделить Contexts — если у тебя много значений
  6. Правильные ключи в списках — всегда уникальные ID
  7. Логика в useEffect — не в render функции
  8. Избегать inline объектов/функций в props

Пример оптимизированного компонента

const OptimizedChild = memo(({ items, onAdd }) => {
  console.log("OptimizedChild рендерится");
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
      <button onClick={onAdd}>Добавить</button>
    </ul>
  );
});

function Parent() {
  const [items, setItems] = useState([]);
  const [count, setCount] = useState(0);
  
  // items меморизирован
  const memoItems = useMemo(() => items, [items]);
  
  // onAdd меморизирован
  const onAdd = useCallback(() => {
    setItems(prev => [...prev, { id: Date.now(), name: "Новый" }]);
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Счетчик: {count}</button>
      <OptimizedChild items={memoItems} onAdd={onAdd} />
      {/* Когда count меняется -> Parent перерендеривается -> OptimizedChild НЕ перерендеривается */}
    </div>
  );
}

Главное — профилировать сначала, оптимизировать потом. Большинство приложений работают достаточно быстро без излишней оптимизации.