Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке мемоізація?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Мемоізація** - це оптимізація, при якій функція зберігає результат для конкретних аргументів і при повторному виклику повертає його з кешу замість повторного обчислення. ```javascript const memoize = (fn) => { const cache = {}; return (...args) => { const key = JSON.stringify(args); return cache[key] ?? (cache[key] = fn(...args)); }; }; ``` **Ключове:** працює тільки на чистих функціях; якщо зовнішній стан змінився між викликами, кеш поверне застарілий результат.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Мемоізація** - це оптимізація, при якій функція зберігає результат для конкретного набору аргументів і при повторному виклику з тими самими аргументами повертає збережений результат замість повторного обчислення. ## Теорія ### TL;DR - Уяви кухаря, який записує час приготування в нотатнику: та сама страва - дивишся в нотатник замість готування з нуля - Працює тільки на чистих функціях; якщо функція читає `Date.now()` або глобальний лічильник, кеш поверне застарілий результат - Використовуй, коли та сама функція запускається багато разів з однаковими аргументами і кожен виклик коштує більше ~1мс - В React для цього є `useMemo` і `useCallback` ### Швидкий приклад ```javascript // Без мемоізації: fib(40) робить ~2 мільярди рекурсивних викликів function fib(n) { if (n < 2) return n; return fib(n - 1) + fib(n - 2); } // З мемоізацією: кожне n обчислюється один раз, решта з кешу const memoize = (fn) => { const cache = {}; return (...args) => { const key = JSON.stringify(args); return cache[key] ?? (cache[key] = fn(...args)); }; }; const fastFib = memoize(fib); fastFib(40); // Обчислює один раз fastFib(40); // Повертає з кешу, без повторного обчислення ``` Кеш - це звичайний об'єкт, де ключ - серіалізовані аргументи. При попаданні в кеш тіло функції не запускається взагалі. ### Тільки чисті функції Мемоізація працює тому, що одні й ті самі аргументи завжди дають один і той самий результат. Ця умова порушується, як тільки функція читає зовнішній стан: `Math.random()`, `Date.now()`, запит до бази - будь-що таке робить кешований результат одразу застарілим. Кеш зберігає першу відповідь назавжди, навіть коли дані навколо вже змінились. На практиці найнепомітніший варіант цього бага - функція, яка читає змінну на рівні модуля. Виглядає як чиста, бо аргументи стабільні, але результат залежить від чогось зовнішнього. В React це проявляється з об'єктними пропсами. Якщо батьківський компонент щоразу передає нове посилання на об'єкт, `useMemo` перераховує значення при кожному рендері, навіть якщо дані всередині ідентичні. React порівнює масив deps через `Object.is` неглибоко (shallow compare), тому два окремі `{a: 1}` - це різні значення, якщо це різні об'єкти в пам'яті. ### Коли використовувати - Рекурсивні алгоритми з перекривними підзадачами, як числа Фібоначчі: `fib(40)` падає з O(2^n) до O(n) - React-компоненти, що фільтрують або трансформують великі списки при кожному рендері - Функції, які часто викликаються з однаковими ID, наприклад конфігурація per-user - Пропускай для одноразових викликів, функцій з побічними ефектами або операцій швидше ~1мс ### Як це працює всередині Базова реалізація - це замикання (closure) навколо звичайного об'єкта або `Map`. При кожному виклику аргументи серіалізуються в ключ кешу і перевіряється попадання. `useMemo` у React зберігає кешоване значення у вузлі fiber і при кожному рендері порівнює масив deps через `Object.is`. Якщо deps рівні - повертає збережений результат і не запускає функцію. ### Типові помилки **Мемоізація нечистої функції** ```javascript const getRandom = memoize(() => Math.random()); getRandom(); // 0.42 getRandom(); // Знову 0.42 - заморожено на першому виклику ``` Немає аргументів - немає диференціації ключа. Перший результат кешується і ніколи не оновлюється. **Застарілі deps в React** ```javascript // БАГ: порожній масив deps закриває початкове значення items const result = useMemo(() => heavyCalc(items), []); // Виправлення: вказуй реальні залежності const result = useMemo(() => heavyCalc(items), [items]); ``` Це найпоширеніша помилка на співбесідах. Правило лінтера `exhaustive-deps` відловлює її автоматично. **Зайва мемоізація дешевих операцій** ```javascript // Не варто: V8 і так оптимізує цей цикл const doubled = useMemo(() => arr.map(x => x * 2), [arr]); ``` Мемоізація сама по собі має накладні витрати: генерація ключа, пошук у кеші, пам'ять. Для операцій швидше ~1мс ці витрати часто перевищують вигоду. Спочатку профілюй. **Мутація закешованого об'єкта** ```javascript function getData(id) { if (!cache[id]) cache[id] = fetchSync(id); cache[id].tags.push('new-tag'); // Ламає всіх, хто тримає це посилання } ``` Кеш зберігає посилання, не копію. Будь-який код, що звертається до цього запису, побачить мутацію. ### Де зустрічається - React 18: `useMemo` для фільтрованих списків, `useCallback` для стабілізації посилань на обробники подій - Lodash: `_.memoize` для утилітарних функцій, які використовуються в багатьох компонентах - Redux Toolkit: `createSelector` через Reselect для похідного стану на зразок todo, відфільтрованих за userId - Next.js 14: `unstable_cache` для серверних компонентів, що запитують однакові дані в різних запитах - Node.js/Express: middleware для кешування JSON-відповідей на read-heavy ендпоінтах ### Питання на співбесіді **Q:** В чому різниця між мемоізацією і кешуванням? **A:** Мемоізація прив'язана до функції і живе в пам'яті програми або замикання. Загальне кешування - це на рівні застосунку, часто в зовнішніх сховищах на зразок Redis з явною политикою expire. **Q:** Коли `useMemo` не запобігає повторному рендеру? **A:** Коли батьківський компонент передає нове посилання на об'єкт, навіть якщо дані всередині однакові. Порівняння deps - shallow через `Object.is`, тому два окремі `{a: 1}` не рівні. Потрібен `useCallback` вище по дереву, щоб стабілізувати посилання. **Q:** Який просторовий трейдоф у мемоізації? **A:** Кеш росте з кожною унікальною комбінацією аргументів. Для функцій з великою кількістю різних вхідних даних це стає помітним. Виправлення для довгоживучих процесів - TTL: зберігати `{value, expiry}` і перераховувати після закінчення терміну. **Q:** Навіщо мемоізувати `fib`, якщо V8 вже оптимізує JS? **A:** В JavaScript немає оптимізації хвостових викликів (TCO) на практиці. Без мемоізації `fib(40)` робить ~2^40 рекурсивних викликів. З нею кожне значення від 0 до 40 обчислюється рівно один раз. ## Приклади ### Базовий: функція-обгортка memoize ```javascript function memoize(fn) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; }; } function slowSquare(n) { // Уяви, що це дорога операція на 50мс return n * n; } const fastSquare = memoize(slowSquare); fastSquare(4); // 16, обчислено fastSquare(4); // 16, з кешу fastSquare(9); // 81, обчислено (новий аргумент) ``` Обгортка перехоплює кожен виклик, спочатку перевіряє кеш і запускає оригінальну функцію тільки при промаху. `Map` надійніше обробляє ключі, ніж звичайний об'єкт для нерядкових аргументів. ### Середній рівень: React-список з useMemo Список 1000 todo не повинен фільтруватися при кожному натисканні клавіші у непов'язаному інпуті. ```javascript import { useMemo, useState } from 'react'; function TodoList({ todos }) { const [filter, setFilter] = useState('all'); const [searchText, setSearchText] = useState(''); // Без useMemo: фільтрує 1000 елементів при кожному рендері, включно зі зміною searchText const visibleTodos = useMemo(() => todos.filter(todo => { if (filter === 'all') return true; return filter === 'done' ? todo.done : !todo.done; }), [todos, filter] // Перераховує тільки при зміні todos або filter, але не searchText ); return ( <> <input value={searchText} onChange={e => setSearchText(e.target.value)} placeholder="Пошук..." /> <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul> </> ); } ``` Друкування в пошуковому інпуті викликає рендери, але `visibleTodos` щоразу читається з кешу. Фільтр запускається знову тільки коли `todos` або `filter` справді змінились.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.