По мере того как приложения React становятся все больше и сложнее, производительность становится все более проблематичной. Поскольку компоненты становятся больше и содержат все больше и больше подкомпонентов, рендеринг становится медленным и превращается в узкое место.
Как решить эту проблему? Если вы еще не использовали useMemo
и useCallback
, мы можем начать с них.
В этом уроке мы рассмотрим, как работают эти два очень простых и удобных обратных вызова, и почему они так полезны. На самом деле, в наши дни у меня болят глаза, когда я не вижу их использования. Итак, давайте погрузимся в то, что они делают.
React.useMemo
Единственная цель этого хука React — сохранить значение для последующего использования, а не пересчитывать его на месте.
Давайте рассмотрим на примере дорогостоящей логики, которая выполняется в нашей функции рендеринга:
const ExpensiveComponent: React.FC = (props) => {
const [list, setList] = React.useState([])
const [counter, setCounter] = React.useState(0)
const multiplied = list.map((i) => (i * 972 + 1000) / 5213).join(', ')
function addRandom() {
setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
}
function increaseCounter() {
setCounter((prev) => ++prev)
}
return (
<div>
Counter: {counter}
<br />
Multiplied: {multiplied}
<br />
<button onClick={addRandom}>Add Random</button>
<button onClick={increaseCounter}>Increase Counter</button>
</div>
)
}
Это не кажется очень проблематичным, но посмотрите на переменную multiplied
. Логика в этом примере не так уж плоха, но представьте себе работу с гигантским списком специальных объектов. Одно только это сопоставление может стать проблемой производительности, особенно если оно зациклено в родительском компоненте.
На этот случай существует еще один крючок состояния — counter
. Когда вызывается setCounter
, multiplied
будет вычисляться заново, тратя предыдущие ресурсы, даже когда обновление в этом случае не требуется, так как эти переменные независимы друг от друга.
Вот тут-то и приходит на помощь useMemo
(читайте официальную документацию).
Вы можете использовать этот хук, чтобы сохранить значение и получить тот же объект, пока не потребуется повторный расчет.
Вот как он используется, единственная строка, которую нам нужно изменить, это определение multiplied
:
const multiplied = React.useMemo(() => {
console.log('recalculating multiplied:', list)
return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
}, [list])
Хук useMemo
принимает 2 аргумента:
- Функция
create
— используется для возврата вычисленного значения переменной, которую мы хотим в конечном итоге использовать. - Список зависимостей. Список зависимостей используется для определения того, когда должно быть вычислено новое значение — то есть, когда снова запускать функцию
create
.
Мы добавили сюда вызов console.log
, просто чтобы отметить, когда вычисляется новое значение.
И с этими изменениями мы можем попробовать наш компонент снова (на всякий случай здесь приведен обновленный код):
const ExpensiveComponent: React.FC = (props) => {
const [list, setList] = React.useState([])
const [counter, setCounter] = React.useState(0)
const multiplied = React.useMemo(() => {
console.log('recalculating multiplied:', list)
return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
}, [list])
function addRandom() {
setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
}
function increaseCounter() {
setCounter((prev) => ++prev)
}
return (
<div>
Counter: {counter}
<br />
Multiplied: {multiplied}
<br />
<button onClick={addRandom}>Add Random</button>
<button onClick={increaseCounter}>Increase Counter</button>
</div>
)
}
Если теперь вы измените счетчик с помощью кнопки «Increase Counter», вы увидите, что наш вызов console.log
не будет вызван снова, пока мы не воспользуемся другой кнопкой «Add Random».
React.useCallback
Теперь у нас есть другой хук — useCallback
(читайте официальную документацию).
Он работает точно так же, как и хук useMemo
— за исключением того, что он предназначен для функций, а не для значений переменных.
Мы можем взять наши функции кнопок и обернуть каждую из них в этот хук, чтобы убедиться, что наша ссылка на функцию изменяется только тогда, когда это необходимо.
const ExpensiveComponent: React.FC = (props) => {
const [list, setList] = React.useState([])
const [counter, setCounter] = React.useState(0)
const multiplied = React.useMemo(
() => list.map((i) => (i * 972 + 1000) / 5213).join(', '),
[list],
)
const addRandom = React.useCallback(
() => setList((prev) => [...prev, Math.floor(Math.random() * 10000)]),
[setList],
)
const increaseCounter = React.useCallback(() => setCounter((prev) => ++prev), [setCounter])
return (
<div>
Counter: {counter}
<br />
Multiplied: {multiplied}
<br />
<button onClick={addRandom}>Add Random</button>
<button onClick={increaseCounter}>Increase Counter</button>
</div>
)
}
Теперь и переменные, и функции мемоизированы и будут менять ссылку только тогда, когда этого требуют их зависимости.
Предостережения
Использование этих крючков не обходится без проблем.
-
Подумайте, действительно ли это улучшает производительность или нет в вашем конкретном случае. Если ваше состояние меняется довольно регулярно и эти мемоизации должны выполняться довольно часто, то увеличение производительности может быть перевешено затратами на выполнение логики мемоизации.
-
Проверка и генерация зависимостей может быть дорогостоящей. Будьте осторожны с тем, что вы помещаете в списки зависимостей, и, если необходимо, сделайте еще несколько мемоизаций и отобразите ваши объекты и списки детерминированными способами, чтобы их можно было легко проверить статистически. Также избегайте использования дорогих методов, таких как
JSON.stringify
, для создания этих мемоизаций или зависимостей, так как это может оказаться слишком дорогим, чтобы стоило того, и может ухудшить ситуацию.
Другие моменты, которые следует учитывать
Возможно, вы захотите убедиться, что в вашем проекте используются правила lint, которые обеспечивают исчерпывающие зависимости, так как они значительно упрощают отслеживание таких вещей.
В некоторых случаях вы можете добавить комментарии игнорирования в очень специфических местах, но это дает понять, что эта часть построена таким образом намеренно, и предотвращает дальнейшую путаницу относительно того, когда обновлять зависимости.
Надеюсь, вы нашли это полезным. Есть много других хуков, о которых стоит узнать, но эти два очень полезны и часто игнорируются, поэтому я подумал, что было бы неплохо выделить их. Если вам интересно, вы можете посмотреть useRef
и чем он отличается от useMemo
, или, возможно, я сделаю еще одну часть об этом в будущем. Кто знает?