Рендеринг и реконсиляция в React
Как управлять рендерингом и реконсиляцией в React: виртуальный DOM, memo, useCallback, виртуализация и профилирование для больших списков.
Введение
Если ваш список в UI тормозит, а консоль полна предупреждений о лишних рендерах, значит, вы столкнулись с проблемой Рендеринг и реконсиляция в React. На первый взгляд все выглядит просто: передаете новые props React пересчитывает дерево. На деле каждый лишний проход потерянные миллисекунды и ухудшенный UX.
Разберем реальный кейс: массив из 10 000 записей, который пользователь может фильтровать и сортировать. Показаны инструменты, контролирующие процесс, и те, что лишь создают иллюзию оптимизации.
Фокус конкретные шаги, которые можно внедрить за полчаса и увидеть разницу.
- Как работает виртуальный DOM и почему его “diff” иногда падает в тупик.
- Правильное использование
React.memo
иuseCallback
. - Практика: разбиваем большой список на “ленивые” части с
react-window
. - Сравнение локального и глобального хранилищ (Redux, Zustand).
- Измерение реального времени рендера через
Profiler
. - Чеклист типичных подводных камней.
1. Как работает виртуальный DOM
Виртуальный DOM легкая копия реального дерева узлов. При изменении состояния React создает новое дерево, сравнивает его с предыдущим и генерирует минимальный набор операций.
Почему сравнение может быть дорогим?
- Каждый элемент сравнивается по типу и ключу.
- Если у списка нет стабильных
key
, React считает каждый элемент новым. - Глубокие вложения увеличивают количество проверок.
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
function List() {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Таблица сравнения: ключи vs. отсутствие ключей
Ситуация | Операции DOM | Время рендера (мс) |
---|---|---|
С key (stable) | 10 000 → 0 | ~12 |
Без key | 10 000 → 10 000 | ~45 |
Вывод: стабильно уникальные key
ваш первый щит от лишних обновлений. Это центральный элемент в теме Рендеринг и реконсиляция в React.
2. Мемоизация компонентов
React.memo
хранит предыдущие пропсы и пропускает рендер, если они не изменились. В комбинации с useCallback
можно “заморозить” функции, которые передаются вниз по дереву. Детальный разбор этих инструментов смотрите в статье React.memo vs useMemo.
const Item = React.memo(({ name, onClick }) => {
console.log("render", name);
return <li onClick={onClick}>{name}</li>;
});
export default function List({ data }) {
const handleClick = React.useCallback((id) => {
console.log("clicked", id);
}, []);
return (
<ul>
{data.map((item) => (
<Item
key={item.id}
name={item.name}
onClick={() => handleClick(item.id)}
/>
))}
</ul>
);
}
Типичные ловушки:
- Не оборачивать стрелочную функцию в
useCallback
каждый рендер создает новую функцию,React.memo
не работает. - Переиспользовать объект-пропс без
useMemo
объект считается новым каждый раз.
Эти приемы напрямую влияют на Рендеринг и реконсиляцию в React, уменьшая количество лишних проходов.
3. Ленивый рендер списков
Когда список содержит десятки тысяч элементов, даже с key
и memo
рендер остается тяжелым. Здесь помогает виртуализация списков отрисовываем лишь видимую часть.
import { FixedSizeList as List } from "react-window";
const Row = ({ index, style }) => <div style={style}>Row {index}</div>;
export default function VirtualizedList() {
return (
<List height={400} itemCount={10000} itemSize={35} width={300}>
{Row}
</List>
);
}
Плюсы виртуализации:
- Память: одновременно в DOM только 12-15 узлов.
- Перерасчет происходит только при скролле.
Чек-лист:
itemCount > 5000
включайтеreact-window
.- Высота строки должна быть фиксирована; иначе используйте
VariableSizeList
. - Не помещайте
position: absolute
-элементы внутри строки они ломают расчет.
4. Где хранить состояние
Частая ошибка держать весь массив в глобальном хранилище (Redux) и каждый раз передавать его в компоненты. Это заставляет каждый подписчик получать новые ссылки, даже если данные не изменились.
Плохой подход:
// store.js
export const itemsSlice = createSlice({
name: "items",
initialState: [],
reducers: { setItems: (state, action) => action.payload },
});
// Component.jsx
const items = useSelector((state) => state.items);
return <LargeList data={items} />; // каждый dispatch = новый массив
Лучшее решение локальное состояние + селекторы:
function FilterableList() {
const [filter, setFilter] = React.useState("");
const data = React.useMemo(() => {
return allItems.filter((i) => i.name.includes(filter));
}, [filter]);
return <VirtualizedList data={data} />;
}
Преимущества локального хранилища:
- Перерисовка только при изменении фильтра.
- Нет лишних подписок на глобальный store.
- Память используется экономнее.
Эти рекомендации уменьшают нагрузку на Рендеринг и реконсиляцию в React.
5. Профилирование рендера
React 18 имеет встроенный <Profiler>
оберните интересующий участок и получите детали о времени “commit” и “render”.
import { Profiler } from "react";
function onRender(id, phase, actualDuration, baseDuration) {
console.log({ id, phase, actualDuration, baseDuration });
}
export default function App() {
return (
<Profiler id="LargeList" onRender={onRender}>
<VirtualizedList />
</Profiler>
);
}
Ключевые метрики:
actualDuration
реальное время рендера.baseDuration
оценка без оптимизаций.
Сравнение показывает, насколько эффективно работают memo- и виртуализационные приемы, т.е. как они влияют на Рендеринг и реконсиляцию в React.
6. Практический рецепт “минимального рендера”
Соберите все в один шаблон, который можно скопировать в любой проект.
- Установите уникальный
key
. - Оберните элементы в
React.memo
. - Передавайте функции через
useCallback
. - Если список > 5000 элементов подключите
react-window
. - Храните фильтры локально, а не в глобальном store.
- Проверьте время через
<Profiler>
и фиксируйте регрессии.
Таблица внедрения:
Шаг | Что делаем | Почему | Библиотека |
---|---|---|---|
1 | key | Предотвратить полное пересоздание элементов | |
2 | React.memo | Сократить повторные рендеры | |
3 | useCallback | Стабильные функции-пропсы | |
4 | Виртуализация | Обрезать DOM-дерево | react-window |
5 | Локальный useMemo | Минимизировать пересчеты | |
6 | <Profiler> | Видеть реальную нагрузку | React |
FAQ / чеклист
- Не забывайте
key
. Даже сmemo
без ключа список будет полностью пересоздаваться. - Оборачивайте в
useCallback
только те функции, которые действительно передаются вниз. Иначе добавляется лишний слой. - Виртуализируйте только при необходимости. Излишняя виртуализация усложняет код и может ухудшить SEO.
- Не храните отфильтрованный массив в Redux. Каждый
dispatch
создает новую ссылку и приводит к массовому ререндеру. - Проверяйте
<Profiler>
после изменений. Без измерений невозможно понять, помогли ли оптимизации.
Любой проект, где UI оперирует большими массивами (таблицы, ленты, чат-истории), выигрывает от описанных техник. После их применения заметно снижается количество “Too many re-renders” и улучшается отклик интерфейса.