Посібник розробника з витоків пам’яті в фронтенд-застосунках

pic

Цілі

Коротко визначити проблему витоків пам'яті та пояснити, чому це важливо в контексті фронтенд-розробки.

Пояснити, чому витоки пам'яті є поширеною проблемою в фронтенд-розробці (таких фреймворках як React, Vue та Angular) і як вони можуть негативно впливати на продуктивність сайту та досвід користувача.

Визначити загальний шаблон витоків пам'яті та пояснити, як їх можна виявити.

Реальна проблема: боротьба з витоками пам'яті у Vue.js (фронтенд-фреймворк)

Витоки пам'яті часто виникають через здавалося б незначні проблеми в функціональності, але їх може бути важко виявити, поки вони не починають спричиняти помітні проблеми — зазвичай, коли додаток сповільнюється або з'являється помилка в консолі.

Кілька днів тому я працював над додатком Vue.js, який містив форму, де користувачі могли вибирати категорії з випадаючого списку або створювати нову категорію, якщо результат пошуку порожній.
Цю функціональність було написано в одному компоненті, який відповідав за створення та редагування форми.

Спочатку все здавалося добре, але я швидко натрапив на дивну проблему. Коли я створював нову категорію, виконувалася API-запит на сервер. Однак, коли я перейшов до форми в режимі редагування і створив ще одну нову категорію, сталося щось дивне: перший API-запит (який мав на меті створення нової категорії) не вдалося виконати, оскільки категорія вже існувала в базі даних. Одночасно виконувався другий API-запит для створення нової категорії. Це означало, що попередній API-запит (який не вдався) все ще мав посилання в пам'яті, і після того як нову категорію було створено, все наче відновилося — але пам'ять не була очищена.

Попри використання хука життєвого циклу onUnmount для очищення всього, проблема залишалася.
Я не міг зрозуміти, чому старий API-запит все ще виконується після того, як компонент було демонтовано, а стан очищено.

Проблема: Після глибшого аналізу я зрозумів, що проблема полягає в механізмі спостерігачів (watcher) Vue. Кожного разу, коли оновлювалося поле введення, спостерігач ініціював API-запит для створення нової категорії. Однак через замикання (closures) пам'ять не очищалася належним чином після демонтажу компонента. Попередній API-запит продовжував виконуватися в фоновому режимі, що призводило до спроби створити ту ж саму категорію знову, навіть після очищення стану.

Це був класичний випадок витоку пам'яті: замикання у спостерігачі тримало посилання на попередній API-запит, а компонент намагався обробити нові введення, не очищаючи старі.

Рішення: Реальною проблемою було те, що спостерігач спрацьовував при кожному натисканні клавіші або зміні в полі введення, що призводило до множинних непотрібних API-запитів.

Кожен виклик накопичувався та спричиняв проблеми з пам'яттю, навіть попри те, що компонент демонтувався, а стан очищався.

Щоб виправити це, я перейшов від використання спостерігача (watcher) до використання події @change на полі введення. Це забезпечило, що API-запит викликався лише після того, як користувач завершив вибір або введення, а не на кожну зміну. Завдяки цій зміні я зміг запобігти непотрібним множинним API-запитам, і проблема з витоком пам'яті була вирішена.

Поширені причини витоків пам'яті: (JavaScript)

Невидалений прослуховувач подій (Event Listener): Прослуховувачі подій (Event listeners) прикріплюються до елементів DOM, але не видаляються, коли ці елементи більше не потрібні.
Ці прослуховувачі подій (Event listeners) утримують посилання в пам'яті, що призводить до витоків.

pic

Хоча кнопка видалена з DOM, прослуховувач події handleClick все ще утримує посилання на неї.

Рішення:

pic

(Правильне очищення за допомогою removeEventListener)

Неочищені таймери: Функції setTimeout або setInterval утримують посилання на свої зворотні виклики (callbacks). Якщо вони не будуть очищені, це може призвести до їх збереження навіть після того, як їхня пов'язана функціональність вже не потрібна.

pic

Без очищення таймерів (витік пам'яті)

Рішення:

pic

(Правильне очищення за допомогою clearInterval)

Закриття (Closures), які утримують посилання: Закриття (Closures) в JavaScript можуть зберігати посилання на змінні навіть після того, як вони більше не потрібні.
Це може призвести до витоків пам'яті, якщо збережене посилання велике або непотрібне.

pic

Закриття (Closure), яке утримує непотрібне посилання

Рішення:

pic

Уникати утримання посилань на створені функції

Забуті Promises: Promises, які не обробляються належним чином або залишаються невиконаними, можуть призвести до збереження пам'яті. Це викликає помилку, яку складно знайти за допомогою відлагоджувача.

pic

Без належного очищення, викликаного невикористаними promises

Рішення:

pic

Правильне оброблення promises

Висновок:

Витоки пам'яті можуть здаватися невидимою проблемою, але їхній вплив може поступово погіршувати продуктивність вашого додатку та досвід користувачів.
Розуміючи основні причини — такі як прослуховувачі подій (Event listeners), неправильне використання спостерігачів (watchers), неочищені таймери (timers) і закриття (closures) — і застосовуючи належні стратегії очищення, ви можете ефективно зменшити ці проблеми. Пам'ятайте, що регулярне профілювання, моніторинг вашого додатку та дотримання кращих практик можуть заощадити вам безліч годин налагодження та забезпечити ефективність і надійність вашого коду.

Сподіваюся, що цей блог дав вам чітке розуміння витоків пам'яті, їхніх можливих причин та способів їх усунення в реальних сценаріях. Завдяки проактивному підходу до виявлення та усунення витоків пам'яті, ви будете добре підготовлені до створення масштабованих і високопродуктивних додатків.

Дякую за прочитання, і не соромтесь поділитись своїм досвідом або думками в коментарях нижче!

Перекладено з: A Developer’s Guide to Memory Leaks in Frontend Applications

Leave a Reply

Your email address will not be published. Required fields are marked *