useMemo та оптимізація “важких” операцій.useCallback та уникнення перестворення логіки.React.memo (HOC): Переривання ланцюга рендерингу через Shallow Comparison.useMemo робить гірше (Over-optimization).Вивчення механізмів мемоїзації для запобігання зайвим обчисленням та рендерингам. Аналіз випадків, коли використання цих хуків є виправданим, а коли — надлишковим. Розглядається React.memo як засіб поверхневого порівняння пропсів для компонентів.
Для студентів 4-го курсу важливо розуміти, що швидкість сучасних веб-додатків визначається не стільки швидкістю інтернету, скільки ефективністю використання процесора пристрою (CPU). React за замовчуванням оптимізований для швидкого рендерингу, але на складних архітектурах каскадні оновлення (коли зміна кореневого стану викликає перерахунок тисяч “дочірніх” компонентів) знижують FPS сторінки. Ця лекція присвячена інженерному патерну мемоїзації (Memoization) — збереженню результатів “важких” обчислень у пам’яті для уникнення їхнього повторного виконання. Ми розглянемо інструментарій мемоїзації в React та головну проблему розробників: коли оптимізація стає шкідливою.
Щоразу, коли в компоненті змінюється стан (useState) або приходять нові пропси (props), React викликає функцію цього компонента наново, генеруючи новий Virtual DOM.
Фундаментальне правило React: Рендер батьківського компонента автоматично (рекурсивно) викликає рендер УСІХ його дочірніх компонентів, незалежно від того, змінилися пропси цих компонентів чи ні.
const Parent = () => {
const [ticker, setTicker] = useState(0); // Оновлюється кожну секунду
// Хоча Footer візуально статичний, функція Footer() буде викликатися
// кожної секунди тільки тому, що вона знаходиться всередині Parent.
return (
<div>
<h1>Таймер: {ticker}</h1>
<Footer companyName="My Corp" />
</div>
);
};
У більшості випадків виклик маленьких функцій-компонентів не є проблемою. Але якщо Footer містить складну SVG-графіку або таблицю на 1000 рядків, додаток почне гальмувати (Drop Frames).
useMemoІдеалогія функціонального програмування стверджує, що чисті функції (pure functions) для однакових вхідних аргументів завжди повертають однаковий результат. Якщо функція математично складна (O(n^2), сортування масивів, обробка зображень), ми можемо не виконувати її наново, а просто “згадати” збережений результат. Цей процес і є мемоїзацією.
Хук useMemo дозволяє “закешувати” результат виконання блоку коду між рендерами.
const DataView = ({ items, filterText }) => {
// Це обчислення виконуватиметься ПРИ КОЖНОМУ рендері.
// Якщо items містить 50 000 елементів, інтерфейс зависне під час набору букв в input.
const filteredItems = items.filter((item) => item.name.includes(filterText));
// ОПТИМІЗАЦІЯ з useMemo:
// React запам'ятає результат. Він виконає фільтрацію наново ТІЛЬКИ якщо
// масив items АБО рядок filterText зміняться.
const memoizedFilteredItems = useMemo(() => {
return items.filter((item) => item.name.includes(filterText));
}, [items, filterText]);
return <List data={memoizedFilteredItems} />;
};
useCallbackФункції в JavaScript є об’єктами першого класу (First-class citizens). Це означає, що при кожному виклику компонента-функції, всі локальні змінні та локальні функції створюються наново, отримуючи нові адреси в оперативній пам’яті комп’ютера.
const App = () => {
// Ця функція перестворюється (нове місце в RAM) при кожному рендері App
const handleClick = () => console.log("Click");
return <Button onClick={handleClick} />;
};
Сам по собі процес створення об’єкта функції є миттєвим, тому useCallback майже ніколи не використовують “просто так”. Його використовують виключно для стабілізації посилань (Referential Equality) при передачі цієї функції дочірнім компонентам.
// React збереже оригінальну функцію і повертатиме те саме посилання,
// поки не зміниться масив залежностей (в даному випадку [] - ніколи).
const handleClick = useCallback(() => {
console.log("Click");
}, []);
Це найважливіша концепція теми. У JavaScript є два типи порівняння:
5 === 5 -> true).[1, 2, 3] === [1, 2, 3]; // FALSE! (Це два різні масиви в пам'яті)
const a = () => {};
const b = () => {};
a === b; // FALSE! (Це дві різні функції)
Коли React перевіряє, чи змінилися пропси компонента, чи варто запускати ефекти у масиві залежностей (useEffect([...])), він використовує метод Object.is (схоже на ===).
Отже, якщо ви передаєте інлайн-об’єкт style= як пропс, ви гарантуєте, що для React цей пропс ЗАВЖДИ буде новим при кожному рендері, оскільки ви постійно виділяєте нову пам’ять (Allocation).
React.memo (HOC) та Shallow ComparisonЩоб зупинити нескінченний каскад рендерингів “вниз”, React надає компонент вищого порядку (HOC) — React.memo.
Він огортає ваш компонент і створює “перешкоду”. Перед тим як відрендерити дочірній компонент, React.memo робить Shallow Comparison (Поверхневе порівняння) старих та нових пропсів. Якщо жоден з пропсів не змінився (за посиланням/значенням), компонент ігнорує виклик рендеру від батька і залишається старим.
// HeavyChart.tsx
const HeavyChart = ({ data, onZoom }) => {
/* ... дуже важкий графік canvas ... */
};
// Експортуємо обгорнуту версію
export default React.memo(HeavyChart);
Чим React.memo відрізняється від useMemo?
useMemo зберігає в пам’яті результат функції / обчислення (звичайні дані, масиви).React.memo зберігає статус всього React-компонента.Робота React.memo ПОВНІСТЮ ЗНИЩУЄТЬСЯ, якщо батько передає йому “нестабільні” (немемоїзовані) об’єкти чи функції!
const Dashboard = () => {
// ПРОБЛЕМА: Кожен рендер створює нову функцію в пам'яті
const handleZoom = () => setZoom((z) => z + 1);
// React.memo робить Shallow Compare: oldProps.onZoom !== newProps.onZoom
// РЕЗУЛЬТАТ: false. Компонент HeavyChart відрендериться,
// хоча візуально нічого не змінилося. Всі зусилля React.memo марні!
return <HeavyChart data={historicalData} onZoom={handleZoom} />;
};
Наслідок (Single Source of Truth): Якщо ви оптимізуєте “дитину” через React.memo, ви зобов’язані оптимізувати всі функції (useCallback) та об’єкти (useMemo), які ви туди передаєте.
Коли useMemo / useCallback є ШКІДЛИВИМИ?
Мемоїзація не є безкоштовною. Сама функція useMemo має свій код, вимагає порівняння масиву залежностей і виділяє додаткову оперативну пам’ять (RAM) для зберігання кешу.
A + B) або створення простої функції (() => console.log(1)) у мемоїзацію, вартість роботи механізмів useMemo / useCallback буде вищою за перестворення результату. Ви робите додаток повільнішим.React.memo. 90% компонентів в додатку достатньо швидкі, щоб перерендеритись за 1-2 мілісекунди. Перевірка пропсів (Shallow Compare) в HOC забере стільки ж часу.Золоті правила використання:
useMemo ТІЛЬКИ для масивних обчислень (O(N) де N > 1000) або дуже складного парсингу.useCallback і useMemo (для об’єктів), ЯКЩО ви передаєте їх як пропс до компонента, що вже обгорнутий у React.memo.useEffect([...]), щоб уникнути нескінченних циклів спрацювання ефекту (Reference Bug).Senior-інженер ніколи не оптимізує код “всліпу” (на основі припущень). Процес оптимізації завжди розпочинається з вимірювань (Profiling). Для цього використовується вкладка Profiler у офіційному розширенні React Developer Tools.
Він будує Flame Graph (Графік полум’я).
useMemo зберігає результат обчислення у пам’яті (кешування обчислень). useCallback зберігає посилання на одну і ту ж саму функцію в пам’яті (кешування інструкцій).{} === {} дорівнює false, кожен інлайн-об’єкт та інлайн-функція в компоненті сприймається фреймворком як абсолютно нова сутність під час наступного виконання.React.memo (Shallow-порівняння пропсів) працює у симбіозі з хуками useMemo/useCallback. Якщо порушити стабільність посилань батьківських пропсів, React.memo стане паразитичним (викликатиме марні порівняння перед неминучим ререндером).Navbar (розміщеному в корені додатку App) гарантовано вплине на важкий статичний компонент Footer, і як цьому запобігти?React.memo. Чому не використовується глибоке (Deep) порівняння всіх полів об’єкта?useMemo(() => func()) та useCallback(func) під час їх декларації в коді?useMemo до кожного виразу в компоненті зробить рендеринг повільнішим і призведе до втрати продуктивності процесора пристрою?[] в анонімну функцію-колбек неможлива без руйнування бізнес логіки, якщо всередині тієї функції ми використовуємо змінні зі Стейту компонента (замикання)?