[JavaScript, ReactJS] Какая настоящая цена useMemo?
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет хабр!В одной из предыдущих моих статей “Все ли вы знаете о useCallback?” мы оценивали когда стоит использовать useCallback, а когда это избыточно. Та статья вызвала большой интерес, поэтому было решено сделать похожую статью на тему: "когда стоит использовать менее популярный хук useMemo, а когда не стоит" (данная статья является расшифровкой видео).Пишем конвертер валютЧтобы анализ был нагляднее, начнем как всегда с примера. Допустим мы хотим написать конвертер валют. Мы вводим доллары, и автоматически получаем ту же сумму в евро, русских рублях, белорусских рублях и гривнах.
Давайте набросаем такой компонент. Введенное значение в долларах будем хранить в state. Потом передаем его в кастомный хук и от туда уже получаем все остальные значения валют. Останется дело за малым, сверстать интерфейс для пользователя:
const Converter = () => {
const [dollars, setDollars] = useState(0);
const { euros, rubles, belRubles, hryvnia } = useConverte(dollars);
// ...
}
Но нас больше интересует не компонент, а сам хук расчета валют. Поэтому рассмотрим имплементацию кастомного хука.
export default (dollars) => {
const euros = (dollars * DOLLARS_TO_EUROS_RATE).toFixed(2);
const rubles = (dollars * DOLLARS_TO_RUBLES_RATE).toFixed(2);
const belRubles = (dollars * DOLLARS_TO_BEL_RUBLES_RATE).toFixed(2);
const hryvnia = (dollars * DOLLARS_TO_HRYVNIA_RATE).toFixed(2);
return {
euros,
rubles,
belRubles,
hryvnia,
};
};
Как мы видим, здесь целый ряд примитивных операций конвертации одной валюты в другую.Первая мысль, которая пришла мне в голову, а не завернуть ли эти вычисления в хук useMemo. Давайте даже пофантазируем, что наш компонент не изолирован от окружающей среды и он рендерится по многим причинам, не только из-за изменения количества долларов. Тогда, кажется, однозначно стоит завернуть вычисления в хук useMemo.
export default (dollars) => {
return useMemo(() => {
const euros = (dollars * DOLLARS_TO_EUROS_RATE).toFixed(2);
const rubles = (dollars * DOLLARS_TO_RUBLES_RATE).toFixed(2);
const belRubles = (dollars * DOLLARS_TO_BEL_RUBLES_RATE).toFixed(2);
const hryvnia = (dollars * DOLLARS_TO_HRYVNIA_RATE).toFixed(2);
return {
euros,
rubles,
belRubles,
hryvnia,
};
}, [dollars]);
};
Изменение небольшое и читабельность сильно не пострадала. Ну и самая главная цель - улучшить перфоманс компонента, вроде как достигнута.Но так ли это на самом деле? Почему мы решили, что перфоманс улучшился? Потому что так в документации написано? Но ведь мы знаем, что использование любого хука не бесплатно. И я решил посчитать, а сколько мы платим за мемоизацию.Учимся считатьДля того чтобы, что-то измерить, нужны какие-то единицы измерения. Для того чтобы их обозначить, рассмотрим следующий пример:
let text = 'Hello world';
Его можно разделить на целых 3 базовых элемента:
- Создание переменной let text, мы можем посчитать кол-во созданных.
- Операция присваивания =, также легко поддается счету.
- Выделение памяти 'Hello world', в нашем случае память для хранения строки.
Да, я понимаю, что мы собираемся считать спички, но на мое мнение это самый наглядный способ для технаря, что то оценить, это перевести в количественное значение. Поэтому я все же продолжу математическую выкладку.Учимся выделять памятьЯ остановился на моменте выделения памяти. Чтобы лучше понять как она выделяется, я нашел статью на MDN “Memory Management”. Там, в примере, хорошо показано, когда выделяется память:
var n = 123; // allocates memory for a number
var s = 'azerty'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and contained values
// (like object) allocates memory for the array and
// contained values
var a = [1, null, 'abra'];
function f(a) {
return a + 2;
} // allocates a function (which is a callable object)
Интересно, что когда создается объект выделяется память под сам объект и отдельно под каждое его свойство:
var x = {
a: {
b: 2
}
};
// 2 objects are created. One is referenced by the other as one of its properties.
// The other is referenced by virtue of being assigned to the 'x' variable.
// Obviously, none can be garbage-collected.
Точно такая же логика и с массивом, каждое его значение, это новая ячейка в памяти. Если интересно можете более подробно изучить статью по ссылке на MDN.Оставшиеся критерииА мы перейдем к оставшимся единицам измерения:
- количество if конструкций
- количество алгоритмических операция * / + - Math.round() .toFixed(2)
Итоговые базовые единицы измеренияИтого у нас получилось целых 5 базовых величин:
- Количество переменных
- Количество присваиваний
- Количество раз выделения памяти
- Количество if конструкций
- Количество алгоритмических операций
Судя по тому что я нагуглил, выполнить if куда дороже чем создать переменную, но во сколько раз посчитать у меня нет такого опыта, поэтому в итоге мы будем сравнивать, каждую категорию в отдельности.Начнем считать!Перед тем как начать подсчеты, хотелось бы разобраться, что именно и с чем мы будем сравнивать. Для этого рассмотрим возможные сценарии рендера компонента.Разные сценарии рендера компонентаКак вы знаете из моей предыдущей статьи “Первое погружение в исходники хуков”, под одним хуком useMemo скрыты две функции mountMemo и updateMemo. Таким образом можно насчитать три разных сценария.
- Первый рендер, когда в любом случае произойдет вычисление валют и при этом выполнится функция mountMemo -> calculateCurrencies() + mountMemo()
- Обновление значения в долларах. В этом случае снова произойдет пересчет валют и выполнится функция updateMemo ->calculateCurrencies() + updateMemo()
- Рендер произошел по какой-либо другой причине и updateMemo вернул мемоизированный результат -> updateMemo()
Сценарий на котором мы планируем сэкономить больше всего, это конечно же третий сценарий, где мы просто достаем мемоизированное значение, без каких-либо дополнительных вычислений. Поэтому, давайте начнем подсчеты именно с этого сценария. Дисклеймер!Перед тем как я начну, хочу вставить дисклеймер. В расчетах могут быть какие-то косяки, т.к. я не профи, например в работе с памятью, но точная цифра вычислений не очень то и важна в этой статье. Если вы увидите какие-то ошибки или упущения, тогда смело поделитесь в комментариях, где я обманываю читателя.Считаем код вычисления валютНачнем с кода вычисления валют. Напомню он выглядит следующим образом:
const euros = (dollars * DOLLARS_TO_EUROS_RATE).toFixed(2);
const rubles = (dollars * DOLLARS_TO_RUBLES_RATE).toFixed(2);
const belRubles = (dollars * DOLLARS_TO_BEL_RUBLES_RATE).toFixed(2);
const hryvnia = (dollars * DOLLARS_TO_HRYVNIA_RATE).toFixed(2);
Здесь мы видим создание 4-ех переменных, 4-ре присваивания, 8-ем алгебраических операций (4-ре умножения и 4-ре округления). И т.к. мы работаем с примитивами, это значит после всех операций выделяется память под вычисленные значения разных валют.Итог следующий:
- 4 - Количество переменных
- 4 - Количество присваиваний
- 4 - Количество раз выделения памяти
- 0 - Количество if конструкций
- 8 - Количество алгоритмических операций
На первый взгляд кажется, что количество действий действительно большое, но давайте теперь рассмотрим сколько стоит хук updateMemo.Считаем updateMemoПервое с чего стоит начать подсчеты, это то что теперь код мы обернули в функцию. Таким образом мы выделяем под нее память, а вторым параметром передаем массив зависимостей, так же под нее выделяем память:
useMemo(() => { ... }, [...]); // 2 раза выделели память
Дальше уже пойдем в исходники updateMemo. Метод updateMemo вы найдете в пакете react-reconcilier в файле ReactFiberHooks.new.js:
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
Здесь начать хочется с параметров, мы передали сюда функцию первым параметром и массив вторым, а это значит что мы создали две переменные и положили в них ссылки на функцию и на массив:
function updateMemo<T>(
nextCreate: () => T, // создали переменную
deps: Array<mixed> | void | null, // создали переменную
): T { ... }
Сократим подсчетыЯ уже представляю ваши мысли: "И что он сейчас будет писать портянку текста как считал каждую строчку кода". Я не буду описывать текстом, как же все это считал, я покажу вам сразу результаты промежуточных расчетов. Я называю расчеты промежуточными, т.к. я остановил свои подсчеты на изучении функции updateWorkInProgressHook() и даже не считал сколько внутри обходится использование функции invariant(), т.к. мне показалось этих цифр достаточно, чтобы понять суть происходящего.Промежуточные результаты следующие:
- 6 - Количество переменных
- 7 - Количество присваиваний
- 3.75 - Количество раз выделения памяти (значения не целые из-за if конструкция)
- 4.5 - Количество if конструкций (значения не целые из-за if конструкция)
- 0 - Количество алгоритмических операций
А если вам интересно проследить как я считал, смотрите видео привязанное к минутеСравниваем промежуточные результаты
Как вы видите, даже на текущем этапе эти цифры почти одинаковые, а если мы продолжим подсчеты, тогда updateMemo будет становится все дороже и дороже.Таким образом можно прийти к выводу, что вычислить значение валют скорей всего дешевле, чем получить мемоизированое значение из useMemoА я вам напомню, что это казался самым выгодным вариантом из трех:
- calculateCurrencies() + mountMemo()
- calculateCurrencies() + updateMemo()
- updateMemo()<- Вот этот
А насколько тогда получается невыгодным первый рендер или рендер, когда значение долларов меняется. И самый смешной случай получится, если у нас компонент рендериться, только лишь по одной причине, когда значение доллара меняется, а это значит, что useMemo никогда не вернет мемоизированное значение.Итоги:
- Удостоверьтесь, что ваши вычисления действительно настолько тяжелые, что их стоит завернуть в useMemo
- Если все причины рендера текущего компонента указаны в зависимостях useMemo, тогда использование useMemo избыточно, так как тяжеловесная функция будет и так выполняться при каждом рендере.
- Не забывайте, в этом выпуске, мы считали спички, для расширения кругозора, и если у вас политика на проекте не смотря ни на что все мемоизировать, я думаю, что это не скажется на перфомансе вашего проекта. Помните об этом!
P.S.Хорошим дополнением к этой статье могли бы быть бенчмарки, но банчмарки могут лишь доказать, что действительно использование useMemo ухудшает перфоманс, но не отвечает почему именно так происходит. Поэтому, этот странный подход с подсчетом переменных был выбран, чтобы более явно показать, что мемоизация отнюдь не бесплатная и построчно рассмотреть, как мы платим за мемоизацию.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, ReactJS] Biscuit-store — еще один взгляд на state-management в JavaScript приложениях
- [JavaScript, Программирование, HTML, TensorFlow] Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 1 (перевод)
- [JavaScript, Программирование] Почему вы можете обойтись без Babel (перевод)
- [JavaScript] Неудачный опыт миграции Electron приложения на ECMAScript модули
- [JavaScript, ReactJS] Поиск данных в столбцах таблицы с пагинацией (front-часть)
- [JavaScript, ReactJS, Карьера в IT-индустрии, TypeScript] Яндекс.Практикум запустил курс «React-разработчик»
- [Разработка мобильных приложений] Как выбрать мобильную кросс-платформу в 2021 году (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] Разрабатываем чат на React с использованием Socket.IO
- [JavaScript, API] ExtendScript Работа с композициями
- [Высокая производительность, JavaScript, Программирование, Клиентская оптимизация, TypeScript] JavaScript нанобенчмарки и преждевременные тормоза
Теги для поиска: #_javascript, #_reactjs, #_react, #_hook, #_hooks, #_memoization, #_javascript, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:14
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет хабр!В одной из предыдущих моих статей “Все ли вы знаете о useCallback?” мы оценивали когда стоит использовать useCallback, а когда это избыточно. Та статья вызвала большой интерес, поэтому было решено сделать похожую статью на тему: "когда стоит использовать менее популярный хук useMemo, а когда не стоит" (данная статья является расшифровкой видео).Пишем конвертер валютЧтобы анализ был нагляднее, начнем как всегда с примера. Допустим мы хотим написать конвертер валют. Мы вводим доллары, и автоматически получаем ту же сумму в евро, русских рублях, белорусских рублях и гривнах. Давайте набросаем такой компонент. Введенное значение в долларах будем хранить в state. Потом передаем его в кастомный хук и от туда уже получаем все остальные значения валют. Останется дело за малым, сверстать интерфейс для пользователя: const Converter = () => {
const [dollars, setDollars] = useState(0); const { euros, rubles, belRubles, hryvnia } = useConverte(dollars); // ... } export default (dollars) => {
const euros = (dollars * DOLLARS_TO_EUROS_RATE).toFixed(2); const rubles = (dollars * DOLLARS_TO_RUBLES_RATE).toFixed(2); const belRubles = (dollars * DOLLARS_TO_BEL_RUBLES_RATE).toFixed(2); const hryvnia = (dollars * DOLLARS_TO_HRYVNIA_RATE).toFixed(2); return { euros, rubles, belRubles, hryvnia, }; }; export default (dollars) => {
return useMemo(() => { const euros = (dollars * DOLLARS_TO_EUROS_RATE).toFixed(2); const rubles = (dollars * DOLLARS_TO_RUBLES_RATE).toFixed(2); const belRubles = (dollars * DOLLARS_TO_BEL_RUBLES_RATE).toFixed(2); const hryvnia = (dollars * DOLLARS_TO_HRYVNIA_RATE).toFixed(2); return { euros, rubles, belRubles, hryvnia, }; }, [dollars]); }; let text = 'Hello world';
var n = 123; // allocates memory for a number
var s = 'azerty'; // allocates memory for a string var o = { a: 1, b: null }; // allocates memory for an object and contained values // (like object) allocates memory for the array and // contained values var a = [1, null, 'abra']; function f(a) { return a + 2; } // allocates a function (which is a callable object) var x = {
a: { b: 2 } }; // 2 objects are created. One is referenced by the other as one of its properties. // The other is referenced by virtue of being assigned to the 'x' variable. // Obviously, none can be garbage-collected.
const euros = (dollars * DOLLARS_TO_EUROS_RATE).toFixed(2);
const rubles = (dollars * DOLLARS_TO_RUBLES_RATE).toFixed(2); const belRubles = (dollars * DOLLARS_TO_BEL_RUBLES_RATE).toFixed(2); const hryvnia = (dollars * DOLLARS_TO_HRYVNIA_RATE).toFixed(2);
useMemo(() => { ... }, [...]); // 2 раза выделели память
function updateMemo<T>(
nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { // Assume these are defined. If they're not, areHookInputsEqual will warn. if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateMemo<T>(
nextCreate: () => T, // создали переменную deps: Array<mixed> | void | null, // создали переменную ): T { ... }
Как вы видите, даже на текущем этапе эти цифры почти одинаковые, а если мы продолжим подсчеты, тогда updateMemo будет становится все дороже и дороже.Таким образом можно прийти к выводу, что вычислить значение валют скорей всего дешевле, чем получить мемоизированое значение из useMemoА я вам напомню, что это казался самым выгодным вариантом из трех:
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:14
Часовой пояс: UTC + 5