useMemo, useCallback и React.memo на простых примерах
Разбираем зачем нужны эти три инструмента оптимизации и как они работают вместе. Минимум теории, максимум кода.
Проблема лишних рендеров
По умолчанию, когда родительский компонент обновляется — его дети тоже рендерятся. Даже если пропсы не поменялись.
function Child({ value }) {
console.log("render child")
return <div>{value}</div>
}
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child value="static text" />
</>
)
}
- Каждый клик вызывает
setCount
. Parent
ререндерится — иChild
тоже.- Но
value
уChild
не менялся.
React.memo
Чтобы Child
не рендерился зря, оборачиваем его в React.memo
.
const Child = React.memo(function Child({ value }) {
console.log("render child")
return <div>{value}</div>
})
Теперь Child
обновится только если value
реально изменится.
Проблема с функциями
JS считает каждую функцию новой. Даже если тело то же самое.
function Parent() {
const [count, setCount] = useState(0)
const handleClick = () => console.log("clicked")
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} />
</>
)
}
- Каждый рендер создаёт новый
handleClick
. - Для
Child
это "новые пропсы". - React.memo думает: пропсы изменились → ререндер.
useCallback
Фиксим с useCallback
, чтобы ссылка на функцию оставалась той же.
const handleClick = useCallback(() => {
console.log("clicked")
}, [])
Теперь:
- пока зависимости не изменились → ссылка та же
React.memo
видит старыйonClick
→ не ререндерит ребёнка
Проблема с тяжёлыми вычислениями
Допустим у нас список чисел и мы считаем сумму.
function Numbers({ items }) {
const total = items.reduce((a, b) => a + b, 0)
return <div>Sum: {total}</div>
}
Даже если items
не менялся, при каждом рендере будет заново пересчёт.
useMemo
С useMemo
пересчёт будет только если изменился items
.
const total = useMemo(() => {
console.log("heavy calc...")
return items.reduce((a, b) => a + b, 0)
}, [items])
Теперь:
items
не меняется → берём закэшированное значениеitems
изменился → пересчитываем
Всё вместе
Обычно схема такая:
React.memo
на ребёнкеuseCallback
для функцийuseMemo
для вычислений
const Child = React.memo(function Child({ value, onClick }) {
console.log("render child")
return <button onClick={onClick}>{value}</button>
})
function Parent({ items }) {
const total = useMemo(() => items.reduce((a, b) => a + b, 0), [items])
const handleClick = useCallback(() => console.log("clicked"), [])
return <Child value={total} onClick={handleClick} />
}
Когда использовать
- если есть заметные тормоза от вычислений →
useMemo
- если пропсы-функции ломают
React.memo
→useCallback
- если дорогой в рендере компонент не должен перерисовываться без причины →
React.memo
Итог
- React.memo — не рендерит ребёнка, если пропсы те же
- useCallback — сохраняет одну и ту же ссылку на функцию
- useMemo — сохраняет результат вычисления
Эти штуки нужны точечно, не "на всякий случай". Сначала пиши код просто. Потом смотри профайлер и добавляй оптимизации там, где реально видно проблему.