Производительность React‑приложения редко ломается из‑за одной «тяжёлой» функции. На практике проблемы накапливаются постепенно: компонент начинает ререндериться чаще, чем нужно, состояние оказывается поднято слишком высоко, в useEffect появляется лишняя работа, а список из нескольких тысяч элементов начинает заметно влиять на интерфейс.
При этом ради оптимизация не всегда правильно использовать useMemo и React.memo. Такой подход даже ухудшить ситуацию, если реализовывать его без понимания причины проблемы.
Сначала нужно понять, что именно мы оптимизируем
Сам по себе ререндер не является проблемой, это обычная часть работы React. Когда меняется состояние компонента, React должен заново вычислить его UI и определить, какие изменения необходимо применить к DOM.
Проблема начинается тогда, когда приложение выполняет эту работу: слишком часто, для слишком большого количества компонентов, при изменениях, которые фактически не затрагивают большую часть дерева, в неподходящий момент, например во время активного ввода пользователя.
Нужно бороться с рендерами, которые не несут полезной работы. Вместо того чтобы механически добавлять React.memo, сначала стоит ответить на три вопроса:
Что вызывает обновление?
Какие компоненты действительно должны обновиться?
Сколько работы выполняется во время этого обновления?
Откуда вообще берутся лишние ререндеры
Рассмотрим простой компонент:
const UserList = ({ users }: Props) => { const [search, setSearch] = useState(''); const filteredUsers = users.filter(user => user.name.toLowerCase().includes(search.toLowerCase()) ); return ( <> <input value={search} onChange={e => setSearch(e.target.value)} /> {filteredUsers.map(user => ( <UserCard key={user.id} user={user} /> ))} </> ); };
При каждом изменении search компонент UserList ререндерится, но вместе с ним React должен пройти по всему списку пользователей. Если пользователей 20 — проблем нет. Но если 20 000, то ситуация уже другая.
При этом изменение строки поиска действительно должно влиять на список. Значит, сам факт ререндера UserList здесь не является ошибкой, проблема может находиться глубже.
Например, если UserCard содержит сложную разметку, вычисления или несколько дочерних компонентов, каждый ввод символа начинает заставлять React заново работать с огромным количеством элементов.
React.memo не всегда решает все проблемы
Один из самых известных способов оптимизации:
const UserCard = memo(({ user }: Props) => { return ( <div> <strong>{user.name}</strong> <span>{user.email}</span> </div> ); });
Теперь UserCard не будет повторно рендериться, если его props не изменились.
Но если родитель передаёт новый объект, то для React это новый объект на каждом рендере, даже если значения внутри объекта одинаковые и memo тут не спасёт:
<UserCard user={{ id: user.id, name: user.name, email: user.email, }} />
То же самое касается функций, на каждом рендере создаётся новая:
<UserCard user={user} onDelete={() => handleDelete(user.id)} />
Если UserCard мемоизирован, он всё равно может обновляться из‑за нового onDelete.
В таких случаях можно стабилизировать ссылку:
const handleDelete = useCallback((id: string) => { deleteUser(id); }, []);
А затем:
<UserCard user={user} onDelete={handleDelete} />
Но действительно ли нужна эта оптимизация?
Мемоизация имеет свою стоимость
useMemo, useCallback и React.memo не являются бесплатными.
Они добавляют:
дополнительную логику сравнения;
хранение предыдущих значений;
усложнение кода;
новые зависимости, которые нужно поддерживать;
риск ошибок при неправильной работе с dependency array.
Поэтому для простой операции сложения практически бессмысленна такая конструкция:
const value = useMemo(() => { return a + b; }, [a, b]);
React должен потратить ресурсы на работу с useMemo, хотя само вычисление практически ничего не стоит.
А если фильтрация действительно дорогая и выполняется на большом наборе данных, то такой случай уже может иметь смысл:
const filteredUsers = useMemo(() => { return users.filter(user => matchesSearch(user, search) ); }, [users, search]);
Главный критерий здесь не размер кода, а стоимость вычисления.
UseEffect не должен использоваться как универсальный механизм синхронизации
В больших React‑приложениях можно встретить цепочки:
useEffect(() => { setSomething(...); }, [a]); useEffect(() => { setSomethingElse(...); }, [something]); useEffect(() => { setData(...); }, [somethingElse]);
Такая архитектура легко превращается в цепочку обновлений:
a => effect => state => render => effect => state => render
Вместе они создают дополнительную работу и усложняют понимание жизненного цикла компонента.
При работе с useEffect нужно задать вопрос: действительно ли здесь нужно синхронизировать React state с внешней системой? Если нет, то это обычное вычисление, которое должно выполняться непосредственно во время render.
Самая недооценённая проблема — количество DOM‑элементов
Предположим, у нас есть таблица:
users.map(user => ( <UserRow key={user.id} user={user} /> ))
Для 50 — 500 строк это абсолютно нормальный подход. Но если приложение отображает 10 000 строк, то React создает огромное количество элементов, которые пользователь физически не может увидеть одновременно.
Здесь оптимизация ререндеров уже не решает основную проблему. Даже идеально мемоизированный компонент не изменит тот факт, что DOM содержит тысячи элементов. Нам нужно уменьшить количество одновременно существующих элементов.
Для этого используется виртуализация.
Виртуализация списков
Идея виртуализации достаточно проста.
Вместо: 10 000 элементов на 10 000 DOM‑узлов мы создаём только элементы, которые сейчас находятся в области просмотра: 10 000 элементов данных на примерно 20–50 DOM‑узлов. При прокрутке существующие элементы переиспользуются и получают новые данные.
Например, в iLocks Online мы используем виртуализацию для случаев, когда на этаже много номеров:

При этом пользователю кажется, что он работает с обычным длинным списком.
Но при работе с виртуализацей важно учитывать нюансы
Переменная высота элементов
Если строки имеют одинаковую высоту, то расчёты относительно простые. Но если высота зависит от содержимого, то виртуализатору приходится дополнительно измерять элементы и пересчитывать их положение.
Поиск и скролл к элементу
Если пользователь нажимает: «Перейти к пользователю № 8342», то виртуализатор должен уметь вычислить позицию элемента и прокрутить контейнер к нему, даже если сам DOM‑элемент еще не существует.
Debounce для поиска — оптимизация не React, а пользовательского сценария
Ещё один распространённый кейс:
<input value={search} onChange={e => setSearch(e.target.value)} />
Если при каждом символе выполняется запрос, мы получаем пять запросов.
Если поиск работает по локальному массиву из нескольких тысяч элементов, ситуация аналогичная: фильтрация запускается после каждого изменения. Для таких сценариев полезен debounce.
Например:
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
Теперь тяжёлая операция запускается только после небольшой паузы пользователя.
Это особенно важно для:
серверного поиска;
сложной фильтрации;
запросов autocomplete;
больших локальных коллекций.
При этом debounce не должен использоваться везде подряд. Для обычного текстового поля, где фильтрация занимает доли миллисекунды, он может только ухудшить ощущение быстроты отклика.
Сначала измеряем, потом оптимизируем
Лучше не оптимизировать код по ощущениям, для точных измерений есть React DevTools Profiler. В нем можно посмотреть: какие компоненты рендерились, сколько времени занял render, какие обновления были дорогими, какие части дерева участвуют в обновлении.
Если же без профилирования начать добавлять useMemo во все компоненты, можно получить более сложный код без заметного прироста производительности.
Что делать с большими таблицами
Таблицы — один из наиболее сложных случаев. Допустим у нас есть таблица на 50 колонок × 5000 строк = 250 000 ячеек. Даже если каждая ячейка очень простая, то это уже серьёзный объём работы. Есть несколько вариантов решения:
если пользователю не нужно видеть все 5000 строк одновременно, то пагинация — самое простое решение.
если необходим непрерывный scrolling — виртуализация.
если отдельные строки сложные — дополнительно мемоизация.
Вместо заключения
Производительность React‑приложения редко определяется одним хуком или библиотекой. На небольшом проекте можно практически не думать о ререндерах: React спокойно справится с десятками или сотнями компонентов.
Проблемы появляются, когда приложение растёт. Сотни компонентов превращаются в тысячи, простые списки — в таблицы на десятки тысяч элементов. Локальное изменение состояния начинает затрагивать большую часть интерфейса.
Главное — не оптимизировать приложение ради самой оптимизации.