Наталья Кайда 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% быстрее с небольшими компромиссами, лучше гипотетического идеального приложения, которое никогда не будет выпущено.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Senior Java Developer
от 350000 RUB до 400000 RUB
Java Engineer
по итогам собеседования

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