Как стать автором
Обновить

React memo: Преисполнимся в оптимизации

Время на прочтение4 мин
Количество просмотров31K
Как-то раз решил один фронтендер всё покрыть в useCallback
Как-то раз решил один фронтендер всё покрыть в useCallback

Привет! Сегодня поговорим про стандартные способы оптимизации 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).

Заключение

Всегда сначала думайте, а потом делайте. Оптимизация в какой-то мере искусство и требует опыта (ну например те же сложные вычисления всегда можно просто переложить на серверную часть, на курсах такому могут и не научить).

Однозначного ответа на то как правильно нет, ситуации бывают индивидуальные, но как неправильно часто становится ясно спустя пару потраченных минут на обдумывание того или иного действия.

Спасибо за внимание!

Теги:
Хабы:
Всего голосов 6: ↑5 и ↓1+4
Комментарии6

Публикации

Работа

Ближайшие события