Узнайте, как использовать useCallback и useMemo to optimize performance

В наших последних публикациях мы рассмотрели хуки:
useStateuseReduceruseContext
Продолжая серию хуков, в этой статье мы рассмотрим ловушки useCallback и useMemo и то, как они помогают оптимизировать наши функциональные компоненты.
Объединяйте и делитесь своими компонентами в облаке, повторно используйте их в приложениях, предлагайте обновления из любого приложения и создавайте быстрее в команде. Попытайся.
используйте их в разных приложениях, предлагайте обновления из любого проекта и улучшайте свою команду.
Небольшой совет: используйте Bit (Github) для совместного использования, повторного использования и обновления компонентов React в приложениях. Битовые треки и пакеты повторно используемых компонентов в ваших проектах и экспортируют их инкапсулированными вместе с их зависимостями, компиляторами и всем остальным. Затем компоненты могут быть установлены с помощью диспетчеров пакетов и даже обновлены прямо из любого нового проекта. Попробуйте.

useMemo
Это используется для запоминания функций. Вау! запоминать? Что это значит? memoize - это действие по сохранению результата ввода (ов) и возврату результата, когда ввод (ы) повторяются снова. Вы слышали о мемоизации, верно? Это акт запоминания функций.
Чтобы понять мемоизацию, давайте проведем аналогию. Возьмем, к примеру, ваш Учитель вызывает вас перед классом и поручает вам дать ему результат любого умножения. Теперь он дает вам таблицу умножения, в которой вы можете найти их. Вы можете найти задание найти любое умножение, о котором вас просят. Вы можете запомнить неоднократно задаваемые вопросы и возвращать ответ, не сверяясь с таблицей.
Допустим, он (ваш Учитель) задает вам следующие вопросы в течение 1 часа:
4 * 87 = 348
5 * 7 = 35
667 * 66 = 44022
6 * 76 = 456
4 * 87 = 348
5 * 88 = 440
667 * 66 = 44022
7 * 6 = 42
4 * 87 = 348
667 * 66 = 44022
4 * 87 = 348
См. 4 * 87 спросили четыре раза и 667 * 66 трижды. Вам нужно будет запомнить их, чтобы, когда вас снова спросят, вам не придется смотреть на таблицу умножения, вы просто дадите ответ, потому что вы его уже запомнили. Никаких дорогостоящих поисков.
То же самое происходит с мемоизацией. Memorization Memoization, звучат они одинаково. Думаю, riz в мире программирования изменили на iz, чтобы у них было другое произношение :).
В функциях его входы / аргументы можно запоминать, скажем, у нас есть дорогостоящая функция, выполнение которой занимает 3 минуты:
const waitSync = (ms) => {
// simulate running for `ms` time
}
function expensiveFunction(a) {
waitSync(180000) // runs for 3 mins
return a * 34;
}
Если мы назовем RoadFunction со значением 4 следующим образом:
const call1 = expensiveFunction(4) // 3 mins const call2 = expensiveFunction(4) // 3 mins const call3 = expensiveFunction(4) // 3 mins // Total: 9 mins
RoadFunction был вызван со значением 4 трижды, выполнение нашего скрипта займет 9 минут !!! Дорогое функция должна запоминать или запоминать входные данные, которые встречаются более двух раз, чтобы не приходилось каждый раз заново вычислять их. Мы должны сделать так, чтобы наша функция RoadFunction запоминала предыдущие входные данные и сохраняла свой результат, чтобы при повторном появлении входных данных она просто возвращалась из хранилища, минуя 3-минутное ожидание.
function expensiveFunction(a) {
expensiveFunction.cache = expensiveFunction.cache || {}
if(expensiveFunction.cache.a) {
return expensiveFunction.cache.a
}
waitSync(180000) // runs for 3 mins
return expensiveFunction.cache.a = a * 34;
}
Видите, мы запомнили нашу функцию RoadFunction, теперь она сначала проверяет любую ссылку на ввод a в своем объекте кэша, если обнаружено, возвращает значение, если нет, то выполняет вычисление и сохраняет результат в объекте кэша с вводом как ключ.
С этим давайте повторно запустим наш скрипт:
const call1 = expensiveFunction(4) // 3 mins const call2 = expensiveFunction(4) // 0.03s const call3 = expensiveFunction(4) // 0.03s // Total: 3.006 mins
Смотрите, первый звонок обычно занимал 3 минуты, но второй и последний звонки заняли более четверти первого звонка !! Дело в том, что в первом он кэшировал ввод с его результатом, поэтому, когда мы вызывали его во второй раз, он просто возвращал результат из кеша, а не пересчитывал в течение 3 минут.
Вместо того, чтобы напрямую писать наш алгоритм мемоизации. в наши функции мы можем создать функцию, которая запоминает переданную ей функцию:
function memoize(fn) {
return function() {
var args =
Array.prototype.slice.call(arguments)
fn.cache = fn.cache || {};
return fn.cache[args] ? fn.cache[args] : (fn.cache[args] = fn.apply(this, args))
}
}
Итак, мы можем передать наш исходный expensiveFunction в memoize следующим образом:
const memoizedExpensiveFunction = memoize(expensiveFunction); const istCall = memoizedExpensiveFunction(90); // 3 mins const secondCall = memoizedExpensiveFunction(90); // 0.03s const thirdCall = memoizedExpensiveFunction(90); // 0.03s
Функция memoize мемоизирует переданную ей функцию и возвращает функцию более высокого порядка, которая реализует алгоритм мемоизации.
Теперь мы видели и знаем, как работает мемоизация. Хук useMemo работает точно так же, мы предоставляем ему функцию и входные зависимости. Сначала он оценивает функцию с входными данными и возвращает результат. Общая форма useMemo такова:
const memoizedOutput = useMemo(create: ()=> mixed, inputs: Array<mixed> | void | null)
create - это функция, которая должна быть запомнена, inputs - это массив входных данных, с которыми функция create должна работать. Если ввод изменится, memoizedOutput будет пересчитан.
Давайте посмотрим на пример:
function App() {
const [count, setCount] = useState(0)
const expFunc = (count)=> {
waitSync(180000);
return count * 90;
}
const resCount = expFunc(count)
return (
<>
Count: {resCount}
<input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
</>
)
}
У нас есть дорогая функция expFunc, выполнение которой занимает 3 минуты, она требует ввода count, ожидает 3 минуты, прежде чем вернуть кратное 90. У нас есть переменная resCount, которая вызывает expFunc с переменной count из ловушки useState. У нас есть вход, который устанавливает состояние count всякий раз, когда мы что-либо вводим.
Каждый раз, когда мы что-либо вводим, наш компонент App повторно отображается, вызывая функцию expFunc. Мы увидим, что если мы будем печатать непрерывно, функция будет вызываться, что приведет к серьезному снижению производительности. Для каждого ввода потребуется 3 минуты для его рендеринга. Если мы введем 3, expFunc будет выполняться в течение 3 минут, а если мы введем 3 снова, это снова займет 3 минуты. Он не должен запускаться снова на втором входе, потому что он такой же, как и предыдущий, он должен где-то хранить результат и возвращать его без запуска функции (expFunc).
Попробуйте это в своем браузере
Мы будем использовать ловушку useMemo, чтобы запоминать функцию expFunc. Помните, мы говорили, что хук useMemo принимает необязательный массив входных данных, который он проверяет, при изменении он запускает переданную ему функцию. Итак, чтобы запоминать expFunc, мы передадим его в ловушку useMemo, а также передадим состояние count из ловушки useState, потому что это вход функции expFunc:
const resCount = useMemo(()=> {
return expFunc(count)
}, [count])
В нашем компоненте App это будет выглядеть так:
function App() {
const [count, setCount] = useState(0)
const expFunc = (count)=> {
waitSync(180000);
return count * 90;
}
const resCount = useMemo(()=> {
return expFunc(count)
}, [count])
return (
<>
Count: {resCount}
<input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
</>
)
}
Теперь, если мы введем 3, он будет первым, поэтому будет вызван expFunc, и нам потребуется 3 минуты, чтобы увидеть значение resCount, которое будет отображено. Если мы снова наберем 3, expFunc не будет вызываться и не будет 3 минутной задержки, потому что useMemo вернет результат из кеша.
Благодаря этому мы оптимизировали наш компонент приложения, чтобы он был высокоэффективным.
Мы также можем обернуть возвращаемое значение нашего функционального компонента в обратный вызов useMemo для мемоизации, компонент будет повторно отрисован, но возвращаемое значение будет основано на его зависимостях, при изменении вернет новое значение, если нет, вернет кешированную версию.
Если мы вызовем наш expFunc в JSX следующим образом:
function App() {
const [count, setCount] = useState(0)
const expFunc = (count)=> {
waitSync(180000);
return count * 90;
}
return (
<>
Count: {expFunc(count)}
<input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
</>
)
}
Возвращаемое значение - это JSX, который React использует для создания файла virt. Дерево DOM и добавьте его в DOM браузера. Таким образом, если приложение постоянно обновляет рендеринг, обновление займет 3 минуты. Если мы не хотим разделять логику, мы передадим оператор return в функцию обратного вызова useMemo.
function App() {
const [count, setCount] = useState(0);
const expFunc = (count)=> {
waitSync(180000);
return count * 90;
}
return useMemo(()=> {
return (
<>
Count: {expFunc(count)}
<input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
</>
)
}, [count])
}
Компонент приложения будет повторно запущен, но он должен быть дешевым, потому что он ничего не делает. Новое значение JSX будет возвращено при изменении состояния счетчика. Вы видите, что expFunc не будет повторно выполняться без необходимости. Если мы введем 1, useMemo выполнит свой обратный вызов и вернет разметку JSX, поэтому expFunc будет выполнен. Если мы снова введем 1, useMemo увидит то же значение count, что и в прошлый раз, и не выполнит свой обратный вызов, поэтому expFunc не будет выполняться.
Примечание. React.memo и React.useMemo используют один и тот же метод оптимизации, но существуют разные варианты использования. React.memo используется только для компонентов, но useMemo может использоваться как для компонентов, так и для автономных функций.
В useMemo есть много ошибок, которые обычно делают новички. Всякий раз, когда мы хотим запомнить функцию:
function toBeMemoed(input) {
// ...
}
Мы не передаем такую функцию в useMemo:
useMemo(toBeMemoed, [input])
Это не сработает, потому что React не будет вызывать toBeMemoed функцию с вводом, поэтому функция не будет запоминаться. Правильный способ - вызвать функцию toBeMemoed внутри функции и передать ее ловушке useMemo.
useMemo(()=> {
return toBeMemoed(input)
}, [input])
useMemo(()=> toBeMemoed(input), [input])
useMemo естественно ожидает функцию обратного вызова, а затем ожидает, что мы вызовем функцию, которую хотим запомнить, внутри тела обратного вызова. По правде говоря, useMemo запоминает функцию обратного вызова, но поскольку наша собственная функция, которую нужно запомнить, вызывается внутри обратного вызова, она, в свою очередь, запоминается. Он будет выполняться, когда useMemo выполнит свой обратный вызов. Кроме того, мы должны вернуть значение нашей запоминаемой функции, которую мы вызвали внутри тела обратного вызова, чтобы получить последнее значение.
Мы можем использовать хук useMemo для оптимизации хуков useState, useReducer, useContext. Это будет очень длинный пост, если мы углубимся в него, мы оставим его для другой статьи Оптимизация крючков: избавление от крючков
useCallback
Это работает как useMemo, но разница в том, что он используется для запоминания объявлений функций.
Допустим, у нас есть это:
function TestComp(props) {
l('rendering TestComp')
return (
<>
TestComp
<button onClick={props.func}>Set Count in 'TestComp'</button>
</>
)
}
TestComp = React.memo(TestComp)
function App() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={()=> setCount(count + 1)}>Set Count</button>
<TestComp func={()=> setCount(count + 1)} />
</>
)
}
У нас есть компонент App, который поддерживает состояние подсчета с помощью useState, всякий раз, когда мы вызываем функцию setCount, компонент App будет повторно визуализировать. Он отображает кнопку и компонент TestComp, если мы нажмем кнопку «Установить счетчик», компонент приложения будет повторно отображен вместе со своим дочерним деревом. Теперь TestComp запоминается с помощью memo, чтобы избежать ненужного повторного рендеринга. React.memo запоминает компонент, сравнивая его текущие / следующие реквизиты с предыдущими реквизитами, если они совпадают, он не выполняет повторный рендеринг компонента. TestComp получает опору, фактически являющуюся функцией в атрибуте props func, всякий раз, когда приложение выполняет повторный рендеринг, функция props из TestComp будет проверяться на идентичность, если обнаружится, что она такая же, она не будет повторно визуализирована.
Проблема здесь в том, что TestComp получает новый экземпляр функции prop. Как? Посмотрите на JSX:
...
return (
<>
...
<TestComp func={()=> setCount(count + 1)} />
</>
)
...
Передается объявление функции стрелки, поэтому всякий раз, когда выполняется рендеринг приложения, всегда создается объявление новой функции с новой ссылкой (указатель адреса памяти). Таким образом, поверхностное сравнение React.memo зафиксирует разницу и даст добро на повторный рендеринг.
Итак, как нам решить эту проблему? Если мы переместим функцию за пределы функции, это будет хорошо, но у нее не будет ссылки на функцию setCount. Здесь на помощь приходит useCallback: мы передадим реквизиты функции в useCallback и укажем зависимость, хук useCallback вернет запомненную версию свойства функции, которую мы передадим TestComp.
function App() {
const check = 90
const [count, setCount] = useState(0)
const clickHndlr = useCallback(()=> { setCount(check) }, [check]);
return (
<>
<button onClick={()=> setCount(count + 1)}>Set Count</button>
<TestComp func={clickHndlr} />
</>
)
}
Здесь clickHndlr не будет повторно создаваться при каждом повторном рендеринге App компонента, если только его зависимость check не изменится, поэтому, когда мы несколько раз нажимаем Set Count кнопку, TestComp не будет повторно визуализироваться. useCallback проверит переменную check, если она не совпадает с ее предыдущим значением, он вернет переданную функцию, поэтому TestComp и React.memo увидят новую ссылку и повторно отобразят TestComp, если не такая же useCallback, ничего не вернет, поэтому React.memo увидит ссылку на функцию, такую же, как его предыдущее значение и отмените повторную визуализацию TestComp.
Заключение
Мы видели, как useMemo и useCallback помогают нам написать высокооптимизированное приложение React. Они оба используют мемоизацию, чтобы убедиться, что наши приложения не теряют зря рендеры.
Знания useMemo и useCallback недостаточно, вы должны знать, как создавать и организовывать дерево компонентов, знать, когда использовать презентационные компоненты и интеллектуальные компоненты, потому что нам нужно, когда разделять компоненты на два или более, чтобы добавить мемоизацию. Благодаря всему этому мы можем создать почти идеальное приложение со 100% производительностью.
Если у вас есть какие-либо вопросы относительно этого или чего-либо, что я должен добавить, исправить или удалить, не стесняйтесь комментировать, писать мне по электронной почте или в прямой переписке.
Спасибо !!!