Рендеринг и реконсиляция в 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” и улучшается отклик интерфейса.