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

Исправлял ли проблему слишком нагруженного рендеринга страницы

2.0 Middle🔥 201 комментариев
#React

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

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

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

Оптимизация нагруженного рендеринга: проблемы и решения

Вопрос о производительности рендеринга — классический для собеседования. Это показывает практический опыт и понимание работы браузера.

Как диагностировать проблему

Chrome DevTools Performance:

// 1. Открыть Chrome DevTools (F12)
// 2. Вкладка Performance
// 3. Нажать Record
// 4. Взаимодействовать с страницей
// 5. Остановить Record

// Смотреть на:
// - Frames per second (FPS) — должно быть 60 FPS
// - Long tasks (жёлтые/красные блоки) — более 50ms
// - Layout (reflow) и Paint (repaint)

React DevTools Profiler:

// Вкладка Profiler в React DevTools
// Показывает:
// - Какие компоненты рендерятся
// - Сколько времени занимает рендеринг
// - Почему компонент перерендерился

Lighthouse:

// Chrome DevTools -> Lighthouse
// Оценивает Performance, Accessibility, Best Practices, SEO
// Даёт конкретные рекомендации

Типичные проблемы и решения

Проблема 1: Бесконечные ре-рендеры

Код с проблемой:

function DataList() {
  const [items, setItems] = useState([]);

  // ПРОБЛЕМА: useEffect вызывает setItems -> useEffect запускается снова
  useEffect(() => {
    setItems([1, 2, 3]); // Каждый раз новый массив
  }, [items]); // items в зависимостях!

  return <div>{items.map(item => <div key={item}>{item}</div>)}</div>;
}

Решение:

function DataList() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems([1, 2, 3]);
  }, []); // Пустые зависимости — запускается один раз

  return <div>{items.map(item => <div key={item}>{item}</div>)}</div>;
}

Проблема 2: Ненужные ре-рендеры детей

Код с проблемой:

function Parent() {
  const [name, setName] = useState('John');
  const [count, setCount] = useState(0);

  return (
    <>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <p>Name: {name}</p>

      {/* ПРОБЛЕМА: ExpensiveList перерендерится каждый раз,
          когда меняется name, хотя она не зависит от name */}
      <ExpensiveList count={count} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  );
}

function ExpensiveList({ count }: { count: number }) {
  console.log('ExpensiveList rendered'); // Логируется каждый раз!

  // Дорогие вычисления
  const items = Array.from({ length: 10000 }, (_, i) => i);

  return (
    <div>
      Count: {count}
      {items.map(item => (
        <div key={item}>{item}</div>
      ))}
    </div>
  );
}

Решение: React.memo

const ExpensiveList = React.memo(function ExpensiveList({ count }) {
  console.log('ExpensiveList rendered'); // Логируется только при изменении count

  const items = Array.from({ length: 10000 }, (_, i) => i);

  return (
    <div>
      Count: {count}
      {items.map(item => (
        <div key={item}>{item}</div>
      ))}
    </div>
  );
});

// Теперь ExpensiveList перерендеривается только если count меняется

Проблема 3: Большие списки без виртуализации

Код с проблемой:

function UsersList({ users }: { users: User[] }) {
  return (
    <div>
      {/* ПРОБЛЕМА: 10000 элементов — DOM станет огромным */}
      {users.map(user => (
        <div key={user.id} style={{ padding: '10px' }}>
          <p>{user.name}</p>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  );
}

// 10000 пользователей = 10000 DIV элементов в DOM = МЕДЛЕННО

Решение: Виртуализация (react-window)

import { FixedSizeList as List } from 'react-window';

function UsersList({ users }: { users: User[] }) {
  const Row = ({ index, style }: { index: number; style: any }) => (
    <div style={style}>
      <p>{users[index].name}</p>
      <p>{users[index].email}</p>
    </div>
  );

  return (
    <List
      height={600}
      itemCount={users.length}
      itemSize={100}
      width="100%"
    >
      {Row}
    </List>
  );
}

// Теперь в DOM только видимые элементы (например, 10-15)
// Остальные добавляются/удаляются по мере скролла

Проблема 4: Медленные операции в рендеринге

Код с проблемой:

function SearchResults({ query }: { query: string }) {
  // ПРОБЛЕМА: Полнотекстовый поиск в рендеринге
  const results = database.search(query);

  // ПРОБЛЕМА: Тяжелые вычисления
  const sortedResults = results
    .map(result => ({
      ...result,
      score: calculateComplexScore(result),
    }))
    .sort((a, b) => b.score - a.score);

  return (
    <div>
      {sortedResults.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

Решение: useMemo

function SearchResults({ query }: { query: string }) {
  const results = useMemo(() => {
    return database.search(query);
  }, [query]);

  const sortedResults = useMemo(() => {
    return results
      .map(result => ({
        ...result,
        score: calculateComplexScore(result),
      }))
      .sort((a, b) => b.score - a.score);
  }, [results]);

  return (
    <div>
      {sortedResults.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

Проблема 5: Слишком много событий (debounce/throttle)

Код с проблемой:

function SearchInput() {
  const [query, setQuery] = useState('');

  // ПРОБЛЕМА: При каждом символе запрос на сервер
  // Пользователь печатает "prepbro" -> 7 запросов!
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    fetch(`/api/search?q=${e.target.value}`); // 7 запросов
  };

  return <input onChange={handleChange} />;
}

Решение: Debounce

import { useDebouncedCallback } from 'use-debounce';

function SearchInput() {
  const [query, setQuery] = useState('');

  // Запрос на сервер только после 500ms без печати
  const debouncedSearch = useDebouncedCallback((value: string) => {
    fetch(`/api/search?q=${value}`);
  }, 500);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    debouncedSearch(e.target.value); // 1 запрос вместо 7
  };

  return <input onChange={handleChange} value={query} />;
}

Проблема 6: Блокирующий JavaScript

Код с проблемой:

function HeavyComponent() {
  // Синхронный расчёт 1 миллиона чисел
  const sum = Array.from({ length: 1_000_000 }, (_, i) => i).reduce((a, b) => a + b);

  return <div>Sum: {sum}</div>; // Блокирует UI на несколько секунд
}

Решение: Web Worker или useTransition

function HeavyComponent() {
  const [sum, setSum] = useState<number | null>(null);
  const [isPending, startTransition] = useTransition();

  useEffect(() => {
    startTransition(() => {
      // React поместит это в фоновый приоритет
      const result = Array.from({ length: 1_000_000 }, (_, i) => i).reduce((a, b) => a + b);
      setSum(result);
    });
  }, []);

  return (
    <div>
      {isPending && <p>Calculating...</p>}
      {sum !== null && <p>Sum: {sum}</p>}
    </div>
  );
}

Пример полного ответа на собеседовании

Да, я встречался с такой проблемой в своём предыдущем проекте. У нас была таблица со списком заказов из 5000 элементов. Когда пользователь прокручивал, FPS падала до 15-20.

Я сначала профилировал в Chrome DevTools и обнаружил два основных узких места:

  1. Каждый элемент таблицы был дорогим в рендеринге — содержал вложенные компоненты и сложные вычисления
  2. При прокрутке все 5000 элементов держались в DOM

Решение было двухуровневое:

  • Обернул TableRow в React.memo, так как пропсы не менялись
  • Внедрил виртуализацию с помощью react-window — в DOM остаются только видимые строки

После этого FPS вернулась к 55-60. Также заметил, что при поиске по таблице срабатывал дебаунс для фильтрации, так как без него было слишком много запросов на сервер.

Теперь я всегда начинаю с профилирования — не гадаю, где узкое место, а измеряю.

Инструменты для профилирования

  1. Chrome DevTools Performance — встроенный
  2. React DevTools Profiler — для React приложений
  3. Lighthouse — общая оценка производительности
  4. Web Vitals — ключевые метрики (LCP, FID, CLS)
  5. Bundle Analyzer — размер бандла

Заключение

Проблемы с рендерингом решаются через:

  1. Диагностику (Chrome DevTools, React Profiler)
  2. Идентификацию узких мест (что медленное?)
  3. Применение правильного решения:
    • React.memo для дорогих компонентов
    • useMemo для дорогих расчётов
    • useCallback для стабильных функций
    • Виртуализация для больших списков
    • Debounce/throttle для частых событий
    • Web Workers для тяжелых вычислений
Исправлял ли проблему слишком нагруженного рендеринга страницы | PrepBro