Недавно наткнулся на статью о том, как хук useCallback может привести к утечкам памяти в React-приложении.
Оригинал статьи здесь. В статье автор демонстрирует, как при использовании нескольких useCallback в одном компоненте может сложиться ситуация, при которой сборщик мусора JavaScript не сможет избавиться от предыдущих контекстов этих useCallback-ов. И данные в этих контекстах будут накапливаться и накапливаться, требуя всё больше и больше памяти.
В этом даже можно убедиться самостоятельно, если воспроизвести пример из статьи у себя локально и заглянуть во вкладку Memory в девтулзах Chrome.
Чтобы избежать утечки, автор статьи советует:
1) Избегать использования слишком больших и сложных компонентов;
2) Выносить логику в кастомные хуки;
3) С осторожностью использовать useMemo и useCallback. И только если замечаете проблемы с производительностью при отрисовке;
4) Хранить тяжелые объекты в useRef. И очищать их вручную, если необходимо.
Но о чём в статье не говорится, так это о том, что React Compiler тоже решает эту проблему))
Допустим, внутри нашего React-компонента будет вот такая строчка, как в статье:
const bigData = new BigObject();
В привычном нам React эта переменная будет создаваться заново при каждом рендере. И да, есть вероятность того, что каждый раз эта переменная будет замыкаться в контекстах разных useCallback.
Но если прогнать через Compiler весь код из статьи, то объявление BigObject там будет выглядеть так:
const $ = _c(22);
let t0;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new BigObject();
$[1] = t0;
} else {
t0 = $[1];
}
const bigData = t0;
Сначала компайлер создаёт массив $ из такого количества Symbol.for('react.memo_cache_sentinel'), которое ему нужно для того, чтобы запомнить все данные из компонента. В случае с примером из статьи, понадобился массив из 22 штук таких Symbol.
А когда компайлер доходит до const bigData в нашем коде, то он либо выполняет new BigObject() и записывает результат в
$[1] (если он убедился, что внутри $[1] находится его этот Symbol), либо забирает этот результат из $[1], если там что-то есть, и присваевает нашей переменной тот результат, который он сохранил себе в $[1] на предыдущем рендере.И примерно по такому же принципу React Compiler запоминает вообще всё.
И да, можете проверить в Memory, накапливаться там ничего не будет.
P.S. А ещё есть React Compiler Playground, где можно посмотреть, во что он превращает ваш код.
P.P.S. А вот тут можно посмотреть, как на самом деле создаётся этот массив $ для мемоизации данных в React Compiler. Вы удивитесь))