Пошук уроків, статей та іншого контенту
React перерендерює компоненти набагато частіше, ніж здається — розбираємось, коли мемоізація реально допомагає, а коли лише ускладнює код без користі.
При кожному ре-рендері React виконує тіло функції компонента заново — усі змінні, обчислення, масиви й функції, оголошені всередині, створюються з нуля, навіть якщо результат був би точно той самий. Для дешевих обчислень це непомітно, але для важких (сортування тисяч рядків, складна фільтрація) чи для дочірніх компонентів, які самі важко рендерити, повторна робота на кожен зайвий рендер батька стає реальною проблемою продуктивності.
function ProductList({ products, theme }) {
// Перераховується на КОЖЕН рендер — навіть якщо змінився лише theme,
// а products лишився тим самим масивом
const sorted = products.slice().sort((a, b) => a.price - b.price);
return <div className={theme}>{sorted.map((p) => <Product key={p.id} {...p} />)}</div>;
}useMemo(fn, deps) виконує fn і запам'ятовує результат; на наступних рендерах, якщо жоден елемент масиву deps не змінився (порівняння через Object.is, те саме, що й у масиві залежностей useEffect), React повертає закешоване значення без повторного виконання fn:
function ProductList({ products, theme }) {
const sorted = React.useMemo(
() => products.slice().sort((a, b) => a.price - b.price),
[products], // перерахувати лише якщо змінився сам масив products
);
return <div className={theme}>{sorted.map((p) => <Product key={p.id} {...p} />)}</div>;
}Функції в JavaScript — теж значення, і кожен рендер компонента створює нову функцію-посилання, навіть якщо її тіло ідентичне попередньому. Це стає проблемою, коли функцію передають дочірньому компоненту, обгорнутому в React.memo (стаття «Що таке Virtual DOM» пояснює, чому React взагалі порівнює пропси перед повторним рендером), або коли вона входить у масив залежностей іншого хука — нова посилання на кожен рендер зводить нанівець сенс мемоізації там:
function ProductList({ products, onAddToCart }) {
// Стабільна посилання на функцію між рендерами, поки onAddToCart не змінився
const handleAdd = React.useCallback(
(id) => onAddToCart(id),
[onAddToCart],
);
return products.map((p) => <MemoizedProduct key={p.id} {...p} onAdd={handleAdd} />);
}useCallback(fn, deps) — це, по суті, useMemo(() => fn, deps): той самий механізм кешування, застосований спеціально до функцій.
useMemo і useCallback мають власну вартість: React все одно порівнює масив залежностей на кожному рендері. Для дешевих обчислень (додати два числа, зібрати короткий рядок) це порівняння коштує не менше, ніж просто виконати обчислення заново — мемоізація в такому разі не пришвидшує код, а лише додає непотрібну складність.
useCallback сам по собі не пришвидшує нічого, якщо дочірній компонент не обгорнутий у React.memo — без нього компонент однаково перерендериться на будь-яку зміну пропсів батька, незалежно від того, чи змінилось посилання на функцію-колбек.
React Compiler — окремий, поки опційний інструмент збірки (не увімкнений за замовчуванням у звичайному React-проєкті), який автоматично вставляє еквівалент useMemo/useCallback під час компіляції там, де компілятор бачить у цьому сенс. Це багатообіцяючий напрямок на майбутнє, але доки він не став стандартом за замовчуванням у більшості кодових баз, розуміння того, коли й навіщо ставити мемоізацію вручну, лишається практично необхідним навиком.
Обгортати useMemo/useCallback навколо буквально кожного значення «про всяк випадок» — типова передчасна оптимізація, яка ускладнює код і не дає вимірного приросту для дешевих обчислень.
Використовувати useCallback без React.memo на дочірньому компоненті — стабільне посилання на функцію нічого не дає, якщо сам компонент не порівнює пропси перед рендером.
Неповний масив залежностей — забутий залежний параметр призводить до застарілого значення (stale closure), «замороженого» на момент першого виконання useMemo/useCallback.
useMemo кешує обчислене значення, useCallback кешує функцію — обидва пропускають повторну роботу, поки елементи масиву залежностей не змінились. Мемоізація виправдана, коли обчислення реально важке або коли дочірній компонент, обгорнутий у React.memo, отримує колбек як пропс; для дешевих обчислень і компонентів без memo вона зазвичай лише додає складність без вимірної користі.