
Привет! Сегодня поговорим про стандартные способы оптимизации web-приложения в экстремистской библиотеке React. Мотивацией послужило некоторое количество кода, который я видел. Связан он с использованием API React не по назначению или без учета каких-то очевидных проверок на производительность и тонкостей (с натяжкой).
Какие вообще мы знаем хуки, методы, способы?
Самые популярные в React (говорим о версии 16.8+) функции для оптимизации: хуки useCallback и useMemo, метод React.memo. Разберемся для чего они.
Его величество useCallback - возвращает мемоизированный колбэк.
Неповторимый useMemo - возвращает мемоизированное значение.
Господин High Order Component (HOC) React.memo - поверхностно сравнивает компоненты между отрисовками и если входные параметры (props) не изменились, то не вызывает рендер компонента, то есть мемоизирует компонент.
Интересно, сколько начинающих разработчиков разбежалось после "мемоизация, мемоизация и еще раз мемоизация"? О том, что такое HOC сегодня говорить не будем, несложная концепция, связанная с композицией.
Так что же такое мемоизация?
По сути банальное кэширование значений. Да, вот так просто. Как это работает? В документации в целом был описан алгоритм, по которому все работает. Напоминаю: в хуки useMemo и useCallback мы передаем вторым параметром массив зависимостей и если какая-то зависимость изменяется, то высчитываем значение заново (ну или пересоздаем функцию), если нет - возвращаем результат предыдущих вычислений. Так как нам очевидно, что если у нас есть переменная sum, которая содержит сумму чисел a и b, то нам не обязательно заново складывать a и b между рендерами, если мы это уже делали и переменные не изменились.
По сути это и есть вся идея мемоизации, для лучшего понимания оставлю пример мемоизирования на чистом JS, вдруг кто-то еще не сталкивался.
Мемоизация на JS
const memo = (callback) => {
// здесь будем хранить результаты вызовов функции
const cache = {};
// ну вот и понадобилось замыкание:)
return (...args) => {
// тут создаем ключ, по которому достанем/сохраним результат
// можно лучше, но сделаем пока что так
const key = JSON.stringify(args);
// очевидно достаем кэш, если он есть
if (key in cache) {
return cache[key];
}
const result = callback(...args);
cache[key] = result; // кэшируем
return result;
};
};
const sum = (a, b) => {
console.log('Call sum', a, b);
return a + b;
};
// мемоизируем функцию
const memoSum = memo(sum);
// проверяем
memoSum(1, 2);
memoSum(100, 31);
memoSum(1, 2);
memoSum(1, 2);
memoSum(1, 2);
memoSum(0, 9);

Получили: сокращение количество вызовов.
А в чем подвох? Подвох в том, что кэш надо где-то хранить, спойлер: используется память, поэтому будьте аккуратнее, совсем не обязательно оптимизировать вызовы примитивных функций, который вызываются много раз. Приложение просядет по памяти.
А как же все-таки это все поможет в React приложении?
Создадим простое приложение а-ля to-do list и будем оптимизировать.

Компонент App (корневой)
import "./App.css";
import ItemList from "./components/ItemList/ItemList";
function App() {
return (
<div className="App">
<ItemList />
</div>
);
}
export default App;
Компонент ItemList
import React, { useState } from "react";
import styles from "./ItemList.module.css";
import Item from "../Item/Item";
const ItemsInitState = [
{
id: "1dcdf741-5140-45c1-ac2d-8512339c20df",
label: "First Item",
},
{
id: "f87f7a2d-92ab-4890-909a-0795699e7f21",
label: "Second Item",
},
{
id: "8a6ff044-80fb-4fd7-b021-9eed7f9ffc24",
label: "Third Item",
},
];
const ItemList = () => {
const [items, setItems] = useState(ItemsInitState);
const remove = (id) =>
setItems((prev) => prev.filter((item) => item.id !== id));
return (
<div className={styles.ItemList}>
{
items.map((item) => <Item item={item} remove={remove} />)
}
</div>
);
};
export default ItemList;
Компонент Item
import React from "react";
import styles from "./Item.module.css";
const Item = ({ item, remove }) => {
console.log(`${item.label}`);
return (
<div className={styles.Item}>
<h2>{item.label}</h2>
<button onClick={remove.bind(null, item.id)} />
</div>
);
};
export default Item;
Что произойдет при запуске? Посмотрим в консоль.

Ай-ай-ай, самое важное, добавить key.
items.map((item) => <Item key={item.id} item={item} remove={remove} />)
А теперь удалим элемент и посмотрим сколько раз вызовется функция/компонент Item.

Окей, а как это исправить? Многие используют для этих целей useCallback и useMemo, ожидая, что это поможет, ну или код станет лучше работать. Давайте попробуем.
const remove = useCallback((id) =>
setItems((prev) => prev.filter((item) => item.id !== id)), []);
Как думаете, что случится? Да ничего, вызовы будут такими же, вы просто мемоизировали функцию. А зачем? Вот и я думаю, что смысла в этом было мало. Причины:
на рендерах это никак не отразилось
вы увеличили стек вызова (useCallback каждый раз проверяет надо ли пересоздать функцию)
в целом с таким же успехом можно было и пересоздать ее, перфоманс не уменьшится, может даже наоборот
А как все-таки правильно? Тут в дело вступает React.memo, тот самый HOC, который мемоизирует компоненты.
export default React.memo(Item); // вызываем его при экспорте

Окей, теперь все работает, по сути это и есть правильный пример использования, мемоизировать функции в большинстве случаев надо тогда, когда вы передаете их в мемоизированные компоненты.
Кстати, а что насчет свойства key? Можете проверить сами, если его убрать, то useCallback и React.memo вам не помогут.
А useMemo когда использовать? Точно также как и useCallback, ну или если вычисления сильно сложнее pages = Math.ceil(total / perPage).
Заключение
Всегда сначала думайте, а потом делайте. Оптимизация в какой-то мере искусство и требует опыта (ну например те же сложные вычисления всегда можно просто переложить на серверную часть, на курсах такому могут и не научить).
Однозначного ответа на то как правильно нет, ситуации бывают индивидуальные, но как неправильно часто становится ясно спустя пару потраченных минут на обдумывание того или иного действия.
Спасибо за внимание!