Каскад ре-рендеров React: когда один клик перерисовывает всё дерево
Что происходит
В React обновление состояния перерисовывает компонент и, по умолчанию, всех его детей. Если состояние живёт высоко в дереве (контекст, стор уровня страницы), одно нажатие клавиши или клик может перерисовать сотни компонентов — большинство из которых показывают ровно то же, что и раньше.
Один рендер дёшев; тысячи рендеров в одном обработчике — нет. Эта работа попадает в фазу processing time у INP.
Как распознать
- Профайлер React DevTools показывает широкие flame-графы: компоненты перерисовываются без видимого изменения их props.
- Ввод в controlled input лагает; каждое нажатие клавиши перерисовывает большие списки или таблицы.
- Processing time доминирует в INP, а стек в DevTools Performance полон внутренностей React.
Как починить
- Опустите состояние вниз: храните его в самом маленьком компоненте, которому оно нужно, а не в компоненте уровня страницы или контексте.
- Оберните дорогие поддеревья в React.memo и стабилизируйте передаваемые им props через useCallback/useMemo — memo бесполезен, если на каждый рендер создаётся новый колбэк.
- Помечайте несрочные обновления (фильтрация списка при вводе) через useTransition, чтобы срочный рендер (сам input) отрисовался первым.
Проблема
const Page = () => {
const [query, setQuery] = useState("")
// every keystroke re-renders Page → Header, Sidebar, BigTable, Footer
return <>
<SearchInput value={query} onChange={setQuery} />
<BigTable rows={filterRows(rows, query)} />
</>
}
Фикс
const BigTable = React.memo(({ rows }) => /* ... */)
const Page = () => {
const [query, setQuery] = useState("")
const [isPending, startTransition] = useTransition()
const rows = useMemo(() => filterRows(allRows, query), [query])
const onChange = (q) =>
startTransition(() => setQuery(q)) // input paints first, table follows
return <>
<SearchInput onChange={onChange} />
<BigTable rows={rows} />
</>
}
Просканировать свой сайт бесплатно →