Если вы занимаетесь разработкой средних или крупных приложений, то наверняка слышали про хуки useMemo
и useCallback
. И, возможно, сталкивались с ситуацией, когда код вашего приложения напоминал «множество неясных useMemo
и useCallback
, которые усугубляли читаемость и усложняли отладку». Эти хуки иногда могут разрастись по вашему коду, словно не поддающиеся контролю сорняки, и вы начнете добавлять их без разбора — только потому, что они повсюду, и все вокруг их используют.
Вы можете прямо сейчас убрать большинство useMemo
и useCallback
из вашего приложения, и оно продолжит работать корректно, и, возможно, даже станет немного быстрее. Проблема заключается в том, что их использование оправдано только в нескольких конкретных случаях, но, к сожалению, мы часто применяем их в ситуациях, где они совсем не нужны.
Давайте начнем с основ. Сначала вспомним про хуки useState
и useEffect
.
useState
— это хук, который отвечает за состояние компонента в функциональных компонентах.
useEffect
— хук, позволяющий выполнять побочные эффекты в функциональных компонентах. Побочные эффекты могут быть связаны с загрузкой данных, изменением состояния компонента, обновлением DOM
и т.д. Для этого в массиве зависимостей указываются параметры для повторного вызова хука.
const [state, setState] = useState() // храним состояние компонента
useEffect(() => {
setState('some')
}, [dependency]) // массив зависимостей
Также хотелось бы упомянуть о props
. props
(сокращение от properties) в React
— это объект, содержащий свойства компонента, переданные ему из родительского компонента. props
позволяют передавать данные от родительского компонента к дочернему компоненту и использовать эти данные для отображения различных значений и состояний компонента.
В мире React
компоненты — это, по сути, функции. И вот тут возникает небольшая проблема: каждый раз, когда состояние компонента обновляется, его внутренний код выполняется заново. Давайте рассмотрим пример с родительским компонентом, использующим useState
и useEffect
:
Каждый раз, когда состояние state
обновляется или срабатывает useEffect
, все дочерние компоненты этого родителя ререндерятся. Это означает, что код внутри дочерних компонентов снова выполняется, и, если у них есть сложные вычисления, это может негативно сказаться на пользовательском опыте из-за замедления работы приложения. Разрешить эту проблему поможет мемоизация.
Мемоизация — это процесс сохранения результатов выполнения функции с определенными аргументами и возврата уже сохраненного результата при повторном вызове функции с теми же аргументами. В React
мемоизацию можно использовать с помощью функции React.memo
для оптимизации компонентов.
Скорее всего нет, особенно если ваши компоненты не содержат сложной логики, ваши компоненты принимают одни и те же данные при рендере, и на UI нет визуальных багов.
Но если вы видите баги или у вас чувствительные данные (постоянно меняющийся курс с биржи), то мы можем измерить время, затраченное на его рендеринг. Для этого нужно включить тротлинг CPU
и вставить код для записи времени в начало и конец функции renderItems()
:
const MyComponent = ({ data }) => {
const renderItems = () => {
const startTime = performance.now() // Записываем время начала выполнения
// Выполняем сложные вычисления для каждого элемента списка
const items = data.map((item) => {
// Сложные вычисления
const result = performComplexCalculation(item)
return result
})
const endTime = performance.now() // Записываем время завершения выполнения
return items
}
return renderItems()
}
После измерения времени выполнения, мы можем определить, занимает ли рендеринг этого компонента значительное количество времени. Например, если время выполнения составляет более 50 миллисекунд, это может сигнализировать о необходимости оптимизации. Важно всегда проводить тесты производительности на слабых компьютерах, так как у ваших клиентов оборудование разной мощности. В таком случае рассмотрите возможность применения мемоизации с использованием useMemo
или React.memo
для улучшения производительности компонента.
useMemo
и useCallback
при вызове:
useMemo
useMemo
React
использует свои внутренние механизмы сравненияОтсюда можно сделать следующие выводы:
useMemo
. Это будет быстрее.useMemo
, тем больше времени потребуется на проверку изменений этих зависимостей и увеличению цены мемоизации.useMemo
и useCallback
при вызове:
React.memo
— это функция высшего порядка (higher-order component, HOC), которая оборачивает компонент и проверяет, изменились ли его props
. Если props
остались неизменными, то React
вернет ранее созданный визуальный вывод компонента, минуя его рендеринг, что может существенно повысить производительность приложения. Однако React
все равно может его ререндерить. Ссылка на документациюuseMemo
— это хук, который принимает функцию и массив зависимостей, и возвращает мемоизированное значение этой функции. Он позволяет избежать повторного выполнения сложных вычислений для одних и тех же props
. Ссылка на документациюuseCallback
— это хук, который принимает функцию и массив зависимостей, и возвращает мемоизированную версию этой функции. Он полезен, когда нам нужно передавать колбэк-функцию в дочерние компоненты, чтобы она не пересоздавалась каждый раз при рендеринге родительского компонента. Ссылка на документациюОднако, использование этих инструментов требует разумного подхода, злоупотребление ими может привести к избыточной сложности и ухудшению производительности приложения. Вот несколько ключевых рекомендаций по их использованию:
React.memo
, useMemo
и useCallback
бездумно.Использование этих инструментов не бесплатно с точки зрения производительности, помните про цену мемоизации. Поэтому применяйте их только там, где они действительно необходимы, например, для оптимизации дорогостоящих вычислений.
const MyComponent = ({a, b, ...numbers}) => {
// не правильное использование useMemo
// const value = useMemo(()=> a + b, [a,b])
// useMemo избыточен
// const validValue = a + b
// правильное использование useMemo для массивов
const value = useMemo(() => numbers.reduce((a,c) => a + c, 0),[numbers])
return {value}
}
В данном примере происходит мемоизация операции сложения. Кажется, что здесь явно можно просто вычислить сумму и ее вывести, так как React
очень быстро обрабатывает рендеры
React.memo
.React.memo
работает только с простыми типами данных и значением ссылки. Есть сценарии, когда использование React.memo
может быть неэффективным или даже не подходить вообще:
props
: для того, чтобы мемоизация работала, надо оборачивать компонент вReact.memo
, а это может даже увеличить накладные расходы из-за проверок на изменения.props
представляют собой сложные объекты, и вы изменяете их часто, React.memo
будет выполнять глубокое сравнение объектов, что может стать затратным с точки зрения производительности. В таких случаях имеет смысл использовать другие методы оптимизации, такие как useCallback
для колбэков или мемоизация данных.useEffect
), использование React.memo
может повлиять на поведение этих эффектов. Это связано с тем, что мемоизация компонента может привести к тому, что он не будет монтироваться или размонтироваться в ожидаемый момент.props
: eсли ваши props
включают динамически создаваемые функции, такие как колбэки, использование React.memo
может сделать его неэффективным, так как каждый раз будет создаваться новая функция, и компонент будет рендериться заново.
В этом примере при смене цвета всегда будет рендер MemoSquare
, так как React.memo
не запоминает всю историю props
, например, как это делает функция memoize
в lodash
. Однако, если мы будем увеличивать счетчик, кликая на кнопку, то MemoSquare
не будет перерендериваться, так как значение color остается неизменным.
Взгляните на предыдущий пример. В качестве props
у Square
примитив, потому при смене любого state
, который не связан с Square
, не будет происходить рендера.
Изменим предыдущий пример, вместо примитива добавим объект:
import React, { useState } from 'react'
function App() {
return (
<>
<MyComponent />
</>
)
}
const MyComponent = () => {
const [color, setColor] = useState('red')
const [counter, setCounter] = useState(0)
return (
<>
<button
onClick={() => setColor((prev) => (prev === 'red' ? 'blue' : 'red'))}
>
Change color
</button>
<button onClick={() => setCounter((prev) => prev + 1)}>
Increment result . Result is {counter}
</button>
<MemoSquare params={{ color }} />
</>
)
}
const Square = ({ params }) => {
console.log('Square RENDER')
return (
<div style={{ width: 50, height: 50, backgroundColor: params.color }} />
)
}
const MemoSquare = React.memo(Square)
export default App
const Square = ({ params }) => {
return (
<div style={{ width: 50, height: 50, backgroundColor: params.color }} />
)
}
const MemoSquare = React.memo(Square)
const MyComponent = ({ a, b }) => {
const [color, setColor] = useState('red')
// пример с мемоизацией props
const params = useMemo(()=>({color}), [color])
return (
<>
<button onClick={() => setColor((prev) => prev === 'red' ? 'blue' : 'red')}>
Change color
<button />
// у MemoSquare все равно будут рендеры , так как params это объект
// который всегда будет получать новую ссылку
<MemoSquare params={params}/>
</>
)
}
Помните, что React.memo
сравнивает предыдущие значения по ссылкам. Если функции, массивы или объекты могут измениться, мемоизируйте их с помощью useCallback
и useMemo
, чтобы избежать ненужных рендеров
Рендер на уровне родительского компонента затрагивает все его дочерние компоненты. Если логика зависит от разных данных, лучше разместить ее внутри соответствующих дочерних компонентов.
// BAD
const Parent = () => {
// логика получения данных
const { items } = useGetSomeData()
// какие-то вычисления
const [state, setState] = useState()
const [result, setResult] = useState(0)
useEffect(() => {
console.log('RENDER')
}, [state])
// логика связанная с фильтрацией
const router = useRouter()
const page = router.query.page
useEffect(() => {
if (page) {
router.push({
url: router.asPath,
query: page,
})
}
}, [router.query])
return (
<div>
{state}
<button onClick={() => setState((prev) => prev + 1)}>Click me</button>
<Filters router={router} />
<ExpensiveCalc result={result} setResult={setResult} />
{items.map(({ id, title }) => (
<span key={id}>{title}</span>
))}
</div>
)
}
Помимо мемоизации существуют другие методы оптимизации: такие как создание чистых компонентов (Pure Components
) и разделение логики по компонентам.
Следующий пример покажет, как можно сделать мемоизацию, хотя это избыточно:
// Мемоизация компонента
const ExpensiveCalc = ({result, onClick}) => {
return <button onClick={onClick}>Increment result => {result}</button>
}
const MemoExpensiveCalc = React.memo(ExpensiveCalc)
const Parent = () => {
// логика получения данных
const { items } = useGetSomeData()
// какие-то вычисления
const [state, setState] = useState()
const [result, setResult] = useState(0)
useEffect(() => {
console.log('RENDER')
}, [state])
// логика связанная с фильтрация
const router = useRouter()
const page = router.query.page
useEffect(() => {
if (page) {
router.push({
url: router.asPath,
query: page,
})
}
}, [router.query])
// мемоизация props для ExpensiveCalc
const onClick = useCallback((params) => setResult(params), [])
const memoResult = useMemo(() => result, [result])
return (
<div>
{state}
<button onClick={() => setState((prev) => prev + 1)}>Click me</button>
<Filters router={router} />
<MemoExpensiveCalc result={memoResult} onClick={onClick} />
{items.map(({ id, title }) => (
<span key={id}>{title}</span>
))}
</div>
)
}
Мы мемоизировали props
и сам компонент, сократили количество рендеров и одного из детей, но родитель при этом также перегружен. Это сложно читаемо. Можно сделать следующим образом:
const ExpensiveCalc = () => {
const [result, setResult] = useState(0)
return (
<button onClick={() => setResult(prev => prev + 1)}>
Increment result => {result}
</button>
)
}
const Filters = () => {
const router = useRouter()
const page = router.query.page
const onChangePage = (page: number) => {
router.push({
url: router.asPath,
query: page + 1,
})
}
return (
<Pagination onPageChange={({ selected }) => onChangePage(selected)}>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</Pagination>
)
}
const Items = () => {
const { items } = useGetSomeData()
return (
<>
{items.map(({ id, title }) => (
<span key={id}>{title}</span>
))}
</>
)
}
const MemoItems = React.memo(Items)
const Parent = () => {
// какие-то вычисления
const [state, setState] = useState()
useEffect(() => {
console.log('RENDER')
}, [state])
return (
<div>
{state}
<button onClick={() => setState((prev) => prev + 1)}>Click me</button>
<Filters />
<ExpensiveCalc />
<MemoItems />
</div>
)
}
При таком подходе у родителя сократится количество рендеров, и вся логика будет инкапсулирована внутри детей.
const Parent = ({ children }) => {
const [state, setState] = useState(0)
useEffect(() => {
console.log('RENDER')
}, [state])
return (
<div>
{state}
<button onClick={() => setState((prev) => prev + 1)}>Click me</button>
{children}
</div>
)
}
const App = () => {
return (
<Parent>
<SomeChildren />
</Parent>
)
}
Мы разделили компонент приложения на две части. Части, которые зависят от цвета, вместе с самой переменной состояния цвета, перешли в Parent
.
Части, которые не заботятся о state
, остались в компоненте приложения и передаются в Parent
, как контент JSX
, также известный как children
. Когда state
меняется, Parent
повторно отрендерит. Но у него все еще есть тот жеchildren
, который он получил из приложения в прошлый раз, поэтому React
не посещает это поддерево.
И, в результате <SomeChildren />
не перерендерится.
При разработке приложений важно учитывать бизнес-требования. Иногда соблюдение сроков является приоритетом, и необходимо сосредоточиться на быстрой разработке. В других случаях производительность может быть критическим фактором, и оптимизация рендеринга становится важной задачей. Важно понимать, что в каждом конкретном случае может быть своя стратегия оптимизации.
Это основные принципы, связанные с правильным использованием мемоизации и оптимизацией в React
. Учитывайте их при разработке ваших приложений, и они помогут вам эффективнее управлять производительностью и поддержкой вашего кода.
DOM
props
, напрямую или через цепочку зависимостей компоненту, который не мемоизированprops
, напрямую или через цепочку зависимостей, компоненту, у которого хотя бы один реквизит не мемоизированuseMemo
и useCallback
надо использовать, когда:
perfomance.now()
показывает, что выгода использования мемоизации равна более 10%React
постоянно развивается и ведет работу над улучшением Developer Experience (DX) и User Experience (UX). Одним из направлений развития является уход от покрытия всего и вся с помощьюuseMemo
и useCallback
. Хотя эти инструменты могут улучшить производительность, они могут усложнить разработку и поддержку кода. Поэтому, будущие версии React
могут предоставить альтернативные подходы к оптимизации рендеринга.
Материалы для дополнительного изучения: