По умолчанию, когда компонент перерисовывается, все его дочерние компоненты тоже перерисовываются — даже если их пропсы не изменились. Это нормальное поведение React: лучше перерисовать лишний раз, чем пропустить нужное обновление.
Однако при сложных вычислениях или больших деревьях компонентов это может замедлить приложение. Тогда на помощь приходят инструменты мемоизации.
React.memo — HOC (компонент высшего порядка), который оборачивает компонент и пропускает его ре-рендер, если пропсы не изменились:
// Без memo: ChildComponent перерисовывается при каждом ре-рендере Parent
function ChildComponent({ name }) {
console.log('ChildComponent render')
return <div>{name}</div>
}
// С memo: ChildComponent перерисовывается только если name изменился
const ChildComponent = React.memo(function ChildComponent({ name }) {
console.log('ChildComponent render')
return <div>{name}</div>
})Сравнение пропсов происходит через поверхностное сравнение (shallow equal) — как Object.is для каждого пропса.
Здесь кроется главная ловушка: объекты и функции в JavaScript сравниваются по ссылке:
function Parent() {
const [count, setCount] = useState(0)
// ПРОБЛЕМА: новый объект создаётся на каждом рендере!
const style = { color: 'red' } // {} !== {} всегда false
// ПРОБЛЕМА: новая функция на каждом рендере!
const handleClick = () => console.log('click')
// Child получит новые style и handleClick — и перерисуется, даже с memo!
return <Child style={style} onClick={handleClick} />
}useMemo кешируует результат функции между рендерами:
function ProductList({ products, filterText }) {
// БЕЗ useMemo: фильтрация запускается при КАЖДОМ рендере
const filtered = products.filter(p => p.name.includes(filterText))
// С useMemo: фильтрация запускается только при изменении зависимостей
const filteredMemo = useMemo(
() => products.filter(p => p.name.includes(filterText)),
[products, filterText] // зависимости — как у useEffect
)
return filteredMemo.map(p => <Product key={p.id} {...p} />)
}useMemo также стабилизирует ссылку на объект, что помогает с memo:
const style = useMemo(() => ({ color: 'red' }), [])
// style — одна и та же ссылка между рендерами!useCallback — это useMemo для функций:
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// БЕЗ useCallback: новая функция при каждом рендере
const handleClick = () => console.log(count)
// С useCallback: функция пересоздаётся только при изменении count
const handleClickMemo = useCallback(
() => console.log(count),
[count]
)
// Child с React.memo не будет ре-рендериться при изменении text
return <Child onClick={handleClickMemo} />
}Мемоизация сама по себе имеет стоимость: React должен сравнивать зависимости на каждом рендере и хранить кеш. Это накладные расходы!
Не мемоизируйте:
useMemo(() => count * 2, [count]) — лишнее)memo-обёрнутые компонентыМемоизируйте только когда:
useEffect или дочерний memo-компонентПравило: сначала напишите без мемоизации, замеряйте, потом оптимизируйте.
Демонстрация мемоизации через Map-кеш, замер производительности дорогостоящих вычислений, проблема референциального равенства
// Мемоизация — это кеширование результата вычислений.
// Реализуем её вручную, чтобы понять как работают useMemo и useCallback.
// --- Проблема: дорогостоящее вычисление без кеша ---
function isPrime(n) {
if (n < 2) return false
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false
}
return true
}
function filterPrimes(numbers) {
// Имитируем дорогостоящее вычисление
return numbers.filter(isPrime)
}
// Генерируем тестовые данные
const bigArray = Array.from({ length: 10000 }, (_, i) => i + 2)
console.log('=== Без мемоизации ===')
const start1 = performance.now()
const result1 = filterPrimes(bigArray)
const time1 = (performance.now() - start1).toFixed(2)
console.log(`Простых чисел: ${result1.length}, время: ${time1}мс`)
// Симулируем "повторный рендер" — вычисление запускается снова!
const start2 = performance.now()
const result2 = filterPrimes(bigArray)
const time2 = (performance.now() - start2).toFixed(2)
console.log(`Повторный рендер: ${time2}мс (те же данные, но вычисление повторяется!)`)
// --- Реализация useMemo через Map-кеш ---
function createMemoCache() {
const cache = new Map()
return function memoize(computeFn, deps) {
// Ключ кеша — строковое представление зависимостей
const key = JSON.stringify(deps)
if (cache.has(key)) {
console.log(` [cache HIT] ключ: ${key.substring(0, 40)}...`)
return cache.get(key)
}
console.log(` [cache MISS] вычисляем...`)
const result = computeFn()
cache.set(key, result)
return result
}
}
const useMemo = createMemoCache()
console.log('
=== С мемоизацией (useMemo) ===')
// "Первый рендер"
const startMemo1 = performance.now()
const memoResult1 = useMemo(
() => filterPrimes(bigArray),
[bigArray.length, bigArray[0]] // зависимости
)
console.log(`Первый вызов: ${(performance.now() - startMemo1).toFixed(2)}мс`)
// "Второй рендер" с теми же зависимостями
const startMemo2 = performance.now()
const memoResult2 = useMemo(
() => filterPrimes(bigArray),
[bigArray.length, bigArray[0]] // зависимости не изменились
)
console.log(`Второй вызов (из кеша): ${(performance.now() - startMemo2).toFixed(2)}мс`)
// --- Проблема референциального равенства ---
console.log('
=== Референциальное равенство ===')
// Примитивы сравниваются по значению
console.log('5 === 5:', 5 === 5) // true
console.log('"a" === "a":', 'a' === 'a') // true
// Объекты — по ссылке
const obj1 = { color: 'red' }
const obj2 = { color: 'red' }
console.log('obj1 === obj2:', obj1 === obj2) // false! Разные объекты!
// Функции — тоже по ссылке
const fn1 = () => console.log('click')
const fn2 = () => console.log('click')
console.log('fn1 === fn2:', fn1 === fn2) // false!
// React.memo сравнивает пропсы через Object.is (как ===)
// Если компонент получает { style: {color:'red'} } каждый рендер —
// это ВСЕГДА новый объект, memo не поможет!
console.log('
Вывод: передавай в memo-компоненты только:')
console.log('- примитивы (string, number, boolean)')
console.log('- стабилизированные через useMemo объекты')
console.log('- стабилизированные через useCallback функции')
// React-код для справки:
// const style = useMemo(() => ({ color: 'red' }), [])
// const handleClick = useCallback(() => doSomething(id), [id])
// const filtered = useMemo(() => filterPrimes(numbers), [numbers])По умолчанию, когда компонент перерисовывается, все его дочерние компоненты тоже перерисовываются — даже если их пропсы не изменились. Это нормальное поведение React: лучше перерисовать лишний раз, чем пропустить нужное обновление.
Однако при сложных вычислениях или больших деревьях компонентов это может замедлить приложение. Тогда на помощь приходят инструменты мемоизации.
React.memo — HOC (компонент высшего порядка), который оборачивает компонент и пропускает его ре-рендер, если пропсы не изменились:
// Без memo: ChildComponent перерисовывается при каждом ре-рендере Parent
function ChildComponent({ name }) {
console.log('ChildComponent render')
return <div>{name}</div>
}
// С memo: ChildComponent перерисовывается только если name изменился
const ChildComponent = React.memo(function ChildComponent({ name }) {
console.log('ChildComponent render')
return <div>{name}</div>
})Сравнение пропсов происходит через поверхностное сравнение (shallow equal) — как Object.is для каждого пропса.
Здесь кроется главная ловушка: объекты и функции в JavaScript сравниваются по ссылке:
function Parent() {
const [count, setCount] = useState(0)
// ПРОБЛЕМА: новый объект создаётся на каждом рендере!
const style = { color: 'red' } // {} !== {} всегда false
// ПРОБЛЕМА: новая функция на каждом рендере!
const handleClick = () => console.log('click')
// Child получит новые style и handleClick — и перерисуется, даже с memo!
return <Child style={style} onClick={handleClick} />
}useMemo кешируует результат функции между рендерами:
function ProductList({ products, filterText }) {
// БЕЗ useMemo: фильтрация запускается при КАЖДОМ рендере
const filtered = products.filter(p => p.name.includes(filterText))
// С useMemo: фильтрация запускается только при изменении зависимостей
const filteredMemo = useMemo(
() => products.filter(p => p.name.includes(filterText)),
[products, filterText] // зависимости — как у useEffect
)
return filteredMemo.map(p => <Product key={p.id} {...p} />)
}useMemo также стабилизирует ссылку на объект, что помогает с memo:
const style = useMemo(() => ({ color: 'red' }), [])
// style — одна и та же ссылка между рендерами!useCallback — это useMemo для функций:
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// БЕЗ useCallback: новая функция при каждом рендере
const handleClick = () => console.log(count)
// С useCallback: функция пересоздаётся только при изменении count
const handleClickMemo = useCallback(
() => console.log(count),
[count]
)
// Child с React.memo не будет ре-рендериться при изменении text
return <Child onClick={handleClickMemo} />
}Мемоизация сама по себе имеет стоимость: React должен сравнивать зависимости на каждом рендере и хранить кеш. Это накладные расходы!
Не мемоизируйте:
useMemo(() => count * 2, [count]) — лишнее)memo-обёрнутые компонентыМемоизируйте только когда:
useEffect или дочерний memo-компонентПравило: сначала напишите без мемоизации, замеряйте, потом оптимизируйте.
Демонстрация мемоизации через Map-кеш, замер производительности дорогостоящих вычислений, проблема референциального равенства
// Мемоизация — это кеширование результата вычислений.
// Реализуем её вручную, чтобы понять как работают useMemo и useCallback.
// --- Проблема: дорогостоящее вычисление без кеша ---
function isPrime(n) {
if (n < 2) return false
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false
}
return true
}
function filterPrimes(numbers) {
// Имитируем дорогостоящее вычисление
return numbers.filter(isPrime)
}
// Генерируем тестовые данные
const bigArray = Array.from({ length: 10000 }, (_, i) => i + 2)
console.log('=== Без мемоизации ===')
const start1 = performance.now()
const result1 = filterPrimes(bigArray)
const time1 = (performance.now() - start1).toFixed(2)
console.log(`Простых чисел: ${result1.length}, время: ${time1}мс`)
// Симулируем "повторный рендер" — вычисление запускается снова!
const start2 = performance.now()
const result2 = filterPrimes(bigArray)
const time2 = (performance.now() - start2).toFixed(2)
console.log(`Повторный рендер: ${time2}мс (те же данные, но вычисление повторяется!)`)
// --- Реализация useMemo через Map-кеш ---
function createMemoCache() {
const cache = new Map()
return function memoize(computeFn, deps) {
// Ключ кеша — строковое представление зависимостей
const key = JSON.stringify(deps)
if (cache.has(key)) {
console.log(` [cache HIT] ключ: ${key.substring(0, 40)}...`)
return cache.get(key)
}
console.log(` [cache MISS] вычисляем...`)
const result = computeFn()
cache.set(key, result)
return result
}
}
const useMemo = createMemoCache()
console.log('
=== С мемоизацией (useMemo) ===')
// "Первый рендер"
const startMemo1 = performance.now()
const memoResult1 = useMemo(
() => filterPrimes(bigArray),
[bigArray.length, bigArray[0]] // зависимости
)
console.log(`Первый вызов: ${(performance.now() - startMemo1).toFixed(2)}мс`)
// "Второй рендер" с теми же зависимостями
const startMemo2 = performance.now()
const memoResult2 = useMemo(
() => filterPrimes(bigArray),
[bigArray.length, bigArray[0]] // зависимости не изменились
)
console.log(`Второй вызов (из кеша): ${(performance.now() - startMemo2).toFixed(2)}мс`)
// --- Проблема референциального равенства ---
console.log('
=== Референциальное равенство ===')
// Примитивы сравниваются по значению
console.log('5 === 5:', 5 === 5) // true
console.log('"a" === "a":', 'a' === 'a') // true
// Объекты — по ссылке
const obj1 = { color: 'red' }
const obj2 = { color: 'red' }
console.log('obj1 === obj2:', obj1 === obj2) // false! Разные объекты!
// Функции — тоже по ссылке
const fn1 = () => console.log('click')
const fn2 = () => console.log('click')
console.log('fn1 === fn2:', fn1 === fn2) // false!
// React.memo сравнивает пропсы через Object.is (как ===)
// Если компонент получает { style: {color:'red'} } каждый рендер —
// это ВСЕГДА новый объект, memo не поможет!
console.log('
Вывод: передавай в memo-компоненты только:')
console.log('- примитивы (string, number, boolean)')
console.log('- стабилизированные через useMemo объекты')
console.log('- стабилизированные через useCallback функции')
// React-код для справки:
// const style = useMemo(() => ({ color: 'red' }), [])
// const handleClick = useCallback(() => doSomething(id), [id])
// const filtered = useMemo(() => filterPrimes(numbers), [numbers])Создай компонент App с двумя состояниями: список чисел (numbers) и строка поиска (search). Используй useMemo для вычисления отфильтрованных чисел — перефильтровывай только при изменении numbers или search, не при каждом рендере. Добавь кнопку "Добавить число" которая добавляет случайное число в список.
useMemo(() => { ... }, [numbers, search]) — первый аргумент функция, второй массив зависимостей. Открой консоль — "Вычисление filtered..." должно появляться только при изменении numbers или search. addNumber: setNumbers(prev => [...prev, newNum]).