Наталья Кайда 08 апреля 2025

🧙‍♂️⚛️🚀 Как ускорить React на 300%: реальный кейс

Виртуальный DOM React – мощный инструмент, но не волшебная палочка. Если не следить за рендерингом компонентов, можно легко превратить быстрое приложение в неповоротливого монстра. Многие сталкиваются с этим, но не всегда понимают причину. Эта статья поможет выявить и устранить узкие места в производительности React-приложений, используя реальные кейсы, практические примеры и эффективные инструменты для оптимизации.
🧙‍♂️⚛️🚀 Как ускорить React на 300%: реальный кейс

В прошлом году я сделал e-commerce приложение, которое загружалось за 1,5 секунды. Спустя полгода, после добавления пары новых функций – динамических фильтров товаров и чата – время загрузки выросло до 8 секунд. Пользователи перестали оплачивать заказы, заказчик пришел в ярость. Что же стало причиной такого замедления? Неконтролируемые повторные рендеры и монолитный код.

React с его виртуальным DOM – не волшебная палочка. Как любой механизм, он требует регулярной оптимизации. В этой статье разберем:

  • Скрытые проблемы рендеринга в React (и как их избежать).
  • Инструменты для аудита производительности.
  • Оптимизации в коде, которые ускорили приложение на 300% (с примерами «до» и «после»).

Как React рендерит компоненты

При работе с виртуальным DOM React отслеживает состояние приложения. Когда оно изменяется, он сравнивает новый виртуальный DOM со старым и обновляет измененные части. При этом часто React заодно обновляет то, что не нужно.

Ненужные повторные рендеры

Рассмотрим этот компонент:

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

return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<StaticComponent /> {/* Renders every time count changes! */}
</div>
);
}

function StaticComponent() {
console.log("I'm rendering unnecessarily ");
return <div>I never change!</div>;
}
    

Что здесь происходит? Каждый раз, когда нажимается кнопка Increment, компонент повторно рендерится, хотя он вообще не зависит от count. У меня был случай, когда я несколько часов оптимизировал дашборд, а потом понял, что статический футер рендерился 100 раз в секунду!

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

React DevTools

Как выявить лишние рендеры? Поможет плагин React DevTools для браузера Chrome:

  • Открываем DevTools → вкладка Profiler → Start Recording.
  • Взаимодействуем с приложением (кликаем по кнопкам, переключаем страницы).
  • Останавливаем запись и анализируем отчет.

Отчет будет выглядеть примерно так:

        ▲ Flamegraph Snapshot (After 3 clicks)
├─ App (Root) [Duration: 2.5ms]
│ ├─ button [Duration: 0.2ms]
│ └─ StaticComponent [Duration: 1.8ms]
│ └─ console.log call
├─ App (Root) [Duration: 2.3ms]
│ ├─ button [Duration: 0.1ms]
│ └─ StaticComponent [Duration: 1.7ms]
├─ App (Root) [Duration: 2.6ms]
│ ├─ button [Duration: 0.2ms]
│ └─ StaticComponent [Duration: 1.9ms]
    

Этот отчет показывает, что StaticComponent рендерился 3 раза без всякой на то необходимости!

📌Как найти проблемные места? Ищите оранжевые/желтые полосы в профайлере –они указывают на медленные рендеры. В нашем случае StaticComponent засветился, как новогодняя елка.

Как логировать ненужные рендеры

Для логирования ненужных ререндеров достаточно установить библиотеку why-did-you-render:

        npm install @welldone-software/why-did-you-render
    

Затем нужно добавить этот код в index.js или App.js, чтобы в консоли отображались предупреждения о компонентах, рендерящихся без необходимости:

        import React from 'react';

if (process.env.NODE_ENV !== 'production') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true, // Shames all components into behaving
});
}
    

Как-то раз эта библиотека помогла мне обнаружить, что компонент Header постоянно ререндерился из-за объекта userPreferences, который обновлялся при каждом рендере. Исправить проблему помогла мемоизация (но об этом позже).

***
Что такое Frontend Basic от Proglib Academy?
Курс Frontend Basic от Proglib Academy поможет вам за 2 месяца освоить верстку на HTML/CSS, программирование на JavaScript и создать свой первый интернет-магазин на React, а бессрочный доступ к 26 видеоурокам и обратная связь от опытных преподавателей обеспечат комфортное обучение в удобном для вас темпе.

Оснoвные способы оптимизации

React.memo – баунсер компонентов

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

❌ До (проблема):

Этот компонент рендерится при каждом обновлении родителя, даже если user остался тем же:

        <UserProfile user={user} />  {/* Лишний ререндер! */}

    

✅ После (исправление):

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

        const UserProfile = React.memo(({ user }) => {
return <div>{user.name}</div>;
});
    

⚠️Важный нюанс!

React.memo использует поверхностное сравнение, и в случае с объектами или массивами может не заметить лишний рендер:

        // Не сработает, если `user` - это новый объект с теми же данными
<UserProfile user={{ name: 'Alice' }} />

// Работает с примитивами
<UserProfile userId={42} />
    

Для решения проблемы нужно передать кастомную функцию, которая определяет, изменился ли пропс:

        const UserProfile = React.memo(
({ user }) => <div>{user.name}</div>,
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
    

Когда использовать React.memo:

  • Компоненты с тяжелым UI (таблицы, графики).
  • Часто обновляемые родительские компоненты (новостная лента, фид соцсети и т. п.)

useCallback и useMemo

Эти два хука помогают оптимизировать производительность, предотвращая ненужные ререндеры и дорогостоящие вычисления.

useCallback для предотвращения лишних ререндеров из-за функций

Функции в JavaScript создаются заново при каждом рендере. Это может вызвать ненужные перерисовки дочерних компонентов.

Пример проблемы – кнопка перерисовывается при каждом рендере, потому что onClickкаждый раз получает новую функцию:

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

  // Новая функция создается при каждом рендере
  const handleClick = () => setCount(count + 1);

  return <Button onClick={handleClick} />;
}

const Button = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Click me</button>;
});

    

Решение useCallback запоминает функцию и не создает ее заново, если зависимости не изменились:

        const handleClick = useCallback(() => {
  setCount(prev => prev + 1); // Используем prev, чтобы избежать зависимостей
}, []); // Пустой массив => функция создается один раз
    

useMemo для оптимизации ресурсоемких вычислений

Если у вас есть огромный список (10 000+ элементов), и вы фильтруете его на каждое нажатие клавиши, без useMemo не обойтись.

❌ Без useMemo фильтрация выполняется при каждом рендере:

        const filteredList = hugeList.filter(item => item.includes(searchTerm));

    

✅ Используем useMemo для кэширования результата:

        const filteredList = useMemo(() => {
  return hugeList.filter(item => item.includes(searchTerm));
}, [hugeList, searchTerm]); // Пересчет только при изменении зависимостей

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

Отложенная и фоновая загрузка

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

Используем React.lazy для динамической подгрузки:

        const ProductCarousel = React.lazy(() => import('./ProductCarousel'));

function HomePage() {
  return (
    <div>
      <HeroBanner />
      <Suspense fallback={<Spinner />}>
        <ProductCarousel />  {/* Загружается только при рендере */}
      </Suspense>
    </div>
  );
}

    

Можно предварительно загрузить компонент в фоновом режиме, когда пользователь показывает низкую активность:

        const ProductCarousel = React.lazy(() => import(
  /* webpackPrefetch: true */ './ProductCarousel'
));

    

Небольшой минус – пользователь может на мгновение увидеть спиннер при медленном соединении (например, 3G).

Дополнительная оптимизация: Context API и работа с большими списками

Context API – незаметный убийца производительности

Фатальная ошибка – хранить в AppContext и данные пользователя, и тему оформления, и уведомления. При изменении темы, например, будут перерисовываться все компоненты, использующие контекст, даже если они используют только данные пользователя. Правильный подход – разделить контексты и передавать их отдельно:

        <UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <App />
  </ThemeContext.Provider>
</UserContext.Provider>

    

Изменение theme заставит ререндериться только компоненты, использующие ThemeContext.

Оптимизация больших списков

При рендеринге списка из 10 000 элементов браузер создаeт 10 000 DOM-узлов. Это загружает процессор, замедляет прокрутку и может привести к зависанию приложения на слабых гаджетах. Решить проблему поможет библиотека react-window, которая рендерит только видимые элементы, а не весь список:

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

const Row = ({ index, style }) => (
  <div style={style}>Item {data[index]}</div>
);

function BigList() {
  return (
    <List
      height={400}       // Высота области списка
      itemCount={10000}  // Всего 10 000 элементов
      itemSize={50}      // Высота одного элемента
      width={300}        // Ширина списка
    >
      {Row}
    </List>
  );
}

    

Результат: количество DOM-узлов сокращается с 10 000 до максимум 20 (в зоне видимости), CPU не перегружается, лаги исчезают, прокрутка становится плавной.

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

Платформа для бронирования путешествий медленно загружала результаты поиска. Основными проблемами были:

  • 4-секундная задержка при загрузке результатов.
  • Лаги при вводе текста в поиске из-за постоянных перерисовок.

Описанные выше способы диагностики помогли выяснить причины:

  • Компонент SearchResults не был мемоизирован, поэтому перерисовывался при каждом нажатии клавиши.
  • API-запросы отправлялись на каждый символ, перегружая сервер.

Решение:

1. Дебаунс (исключение лишних API-запросов). Добавили задержку 500 мс, чтобы API-запрос отправлялся только когда пользователь перестает печатать:

        const SearchInput = () => {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500); // Ждем 500 мс перед запросом

  useEffect(() => {
    fetchResults(debouncedQuery); // Запрос только после паузы в вводе
  }, [debouncedQuery]);
};

    

Результат:

  • Исчезли лишние запросы при наборе текста, снизилась нагрузка на API.
  • У пользователей появилось ощущение плавного ввода без лагов.

2. Мемоизация результатов (useMemo). Поскольку результаты поиска обрабатывались заново при каждом рендере, необходимо было использовать useMemo для кеширования данных:

        const results = useMemo(() => processRawData(rawData), [rawData]);

    

Результат:

  • Если значение rawData не изменилось, обработка не выполняется заново.
  • Снизилась нагрузка на CPU.
  • Страница перестала тормозить.

Общий итог оптимизации – время загрузки снизилось с 4 с до 1,2 с, лаги при вводе текста исчезли, прокрутка стала плавной.

React.js – мощная библиотека для создания пользовательских интерфейсов, но многие начинающие разработчики не используют его на полную силу. В этой статье разберем 5 полезных хаков, которые помогут вам писать более чистый, производительный и удобный в поддержке код.

Частые проблемы с производительностью React-приложений

Почему компонент продолжает тормозить даже с учетом React.memo?

React.memo не избавляет от всех подводных камней, которые могут вызвать лишние рендеры. Проверьте эти моменты:

1. Передача новых объектов/массивов в пропсы (они создаются заново на каждом рендере):

        <MyComponent style={{ color: 'red' }} /> // Новый объект при каждом рендере!
    

Используйте useMemo или выносите объекты за пределы компонента.

2. Потребители контекста (useContext) могут обновляться при каждом изменении. Не забудьте разделить контексты (UserContext, ThemeContext и т. д.), чтобы обновлялась только нужная часть UI.

Стоит ли усложнять код ради отложенной загрузки?

  • Да, если компонент не виден сразу (располагается ниже первого экрана). Это уменьшает размер первого загружаемого бандла.
  • Нет, если компонент важен для первого рендера (шапка, главное меню). В этом случае отложенная загрузка замедлит отображение страницы.

Как убедить команду, что пора оптимизировать скорость?

Продемонстрируйте им Lighthouse-отчеты перед и после оптимизации. Исследование Deloitte показывает, что ускорение сайта всего на 0,1 с повышает конверсии на 8,4-10,1%.

Подведем итоги

Оптимизация React-приложений – это не разовое действие, а процесс. Главное – не пытаться улучшить код вслепую:

  • Сначала профилирование, потом оптимизация. Не гадайте, где находятся узкие места в вашем приложении. Используйте конкретные данные для выявления реальных проблем производительности.
  • Используйте React DevTools. Это отличный инструмент для поиска фактических узких мест в вашем приложении, который даст вам точную информацию о том, что нужно оптимизировать.
  • Не оборачивайте все в useMemo на всякий случай. Это может вызвать больше проблем, чем решит.
  • Примите несовершенство. Точечно оптимизированное приложение, которое работает на 90% быстрее с небольшими компромиссами, лучше гипотетического идеального приложения, которое никогда не будет выпущено.

Комментарии

 
 

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

Подпишись

на push-уведомления