
Привет! Сегодня поговорим про стандартные способы оптимизации 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).
Заключение
Всегда сначала думайте, а потом делайте. Оптимизация в какой-то мере искусство и требует опыта (ну например те же сложные вычисления всегда можно просто переложить на серверную часть, на курсах такому могут и не научить).
Однозначного ответа на то как правильно нет, ситуации бывают индивидуальные, но как неправильно часто становится ясно спустя пару потраченных минут на обдумывание того или иного действия.
Спасибо за внимание!
