🫠 Утечки памяти в JavaScript: причины и способы устранения

Утечка памяти – один из самых коварных багов, способный превратить простое приложение в неповоротливого монстра. В этой статье мы разберем основные причины утечек памяти в JavaScript и научимся их эффективно предотвращать.
🫠 Утечки памяти в JavaScript: причины и способы устранения

Производительность приложений играет ключевую роль в удержании пользователей. Даже небольшое замедление может привести к потере клиентов и снижению конверсии. Одной из частых причин деградации производительности являются утечки памяти – ситуации, при которой программа не освобождает память, которая ей больше не нужна. Давайте разберемся, почему это происходит и как с этим бороться.

Что такое утечка памяти в JavaScript

Когда вы пишете код на JavaScript, компьютер выделяет память для хранения переменных, объектов и других данных. Но если эта память не освобождается вовремя (и правильно), возникает явление, называемое утечкой памяти.

Пример из жизни: представьте, что у вас есть ящик для хранения вещей. Если вы постоянно кладете в него новые вещи, не убирая старые и ненужные, рано или поздно ящик переполнится. Утечка памяти – это тот же самый переполненный «ящик», только в вашей программе.

Что вызывает утечку памяти в JavaScript

В JavaScript утечка памяти происходит, когда память, которая больше не используется, не освобождается, и программа продолжает ее удерживать. Это приводит к тому, что:

  • Приложение начинает потреблять все больше памяти.
  • Работа приложения ощутимо замедляется.
  • В крайних случаях приложение может зависнуть или вылететь из-за нехватки памяти.
👨‍💻🎨 Библиотека фронтендера
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Как JavaScript управляет памятью с помощью сборщика мусора

Как JavaScript управляет памятью с помощью сборщика мусора
Как JavaScript управляет памятью с помощью сборщика мусора

В JavaScript управление памятью происходит с помощью механизма под названием сборщик мусора. По аналогии с уже упомянутым ящиком для хранения это уборщик, который регулярно проверяет ваш ящик (память) и выкидывает все ненужные вещи (результаты работы программы).

Как работает сборщик мусора:

  • Находит неиспользуемые переменные, объекты и функции. Если у объекта больше нет ссылок (он нигде не используется), JavaScript определяет его как «мусор».
  • Освобождает занятую мусором память, автоматически удаляя неиспользуемые данные.

Почему сборщик мусора допускает утечки

Иногда уборщик не справляется со своей задачей. Например:

  • Объекты остаются связаны друг с другом (через замыкания или глобальные переменные), даже если они больше не нужны.
  • Уборщик видит, что есть связи между объектами, и ошибочно считает их нужными.

В результате такой мусор остается в памяти, и возникает утечка.

Основные причины утечек памяти в JavaScript и как их избежать

Глобальные переменные

Почему возникает проблема: глобальные переменные остаются в памяти в течение всего времени работы приложения. Если случайно создать переменную в глобальной области видимости, она будет занимать память, даже если уже не нужна. Здесь переменная leakyVar создается в глобальной области из-за отсутствия ключевого слова let или const:

        // Пример утечки памяти
function createGlobalVariable() {
    leakyVar = "Я глобальная переменная!"; // переменная объявлена без 'let' или 'const'
createGlobalVariable();

    

Решение: всегда объявляйте переменные с помощью let или const, чтобы ограничить их область видимости.

        function createGlobalVariable() {
    let safeVar = "Я локальная переменная!"; // Локальная область видимости
}
createGlobalVariable();

    
Утечки памяти мешают пользователям долго работать с вашим веб-приложением – из-за них можно потерять аудиторию. Попробуем разобраться как этого издежать.

Незакрытые слушатели событий

Почему возникает проблема: eсли добавить слушатель события (event listener), он будет удерживать ссылку на объект. Если элемент удалить из DOM, но не убрать слушатель, память останется занятой:

        // Пример утечки памяти
const button = document.getElementById("myButton");

button.addEventListener("click", () => {
    console.log("Кнопка нажата!");
});

// Если удалить кнопку из DOM, но оставить слушатель,
// память для button останется занятой.
document.body.removeChild(button);

    

Решение: перед удалением элемента из DOM необходимо удалить его слушатели событий.

        // Сохраняем ссылку на функцию
function handleClick() {
    console.log("Кнопка нажата!");
}

button.addEventListener("click", handleClick);

// Перед удалением кнопки — снимаем слушатель
button.removeEventListener("click", handleClick);
document.body.removeChild(button);

    

Ссылки на удаленные DOM-элементы

Почему возникает проблема: если в коде остается ссылка на DOM-элемент, который уже удален со страницы, память, занимаемая этим элементом, не будет освобождена. Здесь память не освободится, так как переменная cachedDiv все еще содержит ссылку на удаленный элемент:

        // Пример утечки памяти
let cachedDiv = document.getElementById("myDiv");

// Удаляем элемент из DOM
document.body.removeChild(cachedDiv);

// Но переменная cachedDiv все еще хранит ссылку на удаленный элемент

    

Решение: после удаления DOM-элемента из памяти нужно обнулить ссылки на него.

        // Освобождаем память, обнуляя ссылку
cachedDiv = null;

    

Замыкания

Замыкание возникает, когда внутренняя функция запоминает переменные из внешней области видимости, даже если внешняя функция уже завершила работу. Замыкания — мощный инструмент, но их неправильное использование может привести к утечкам памяти.

Почему возникает проблема: если замыкание хранит ссылку на большие объекты или массивы, то эти данные останутся в памяти, даже если уже не нужны. В приведенном ниже примере:

  • Переменная largeArray создается внутри функции createClosure().
  • Вложенная функция запоминает largeArray через замыкание.
  • Когда функция createClosure() завершает работу, большой массив остается в памяти, потому что на него ссылается переменная leakyClosure.
        function createClosure() {
    const largeArray = new Array(1000000); // Большой массив
    return function() {
        console.log(largeArray.length);
    };
}

const leakyClosure = createClosure();

    

Решение:

  • Избегайте ненужных замыканий. Если большой объект не нужен в замыкании, не оставляйте его внутри.
  • Не создавайте замыкания внутри циклов без необходимости.
  • Разрывайте ссылки на большие объекты, если они больше не нужны.
        function createSafeClosure() {
    let largeArray = new Array(1000000);
    
    // Очищаем память после использования
    largeArray = null; 
    
    return function() {
        console.log("Замыкание создано без утечки памяти!");
    };
}

const safeClosure = createSafeClosure();

    
Знакомимся с фундаментальным строительным блоком любого приложения на JavaScript — функцией.

Как обнаружить утечки памяти

Современные браузеры оснащены инструментами для разработчиков, которые помогают выявлять проблемы с памятью. Например:

  • В Chrome откройте «Инструменты разработчика» (нажмите F12) и перейдите во вкладку «Память».
  • Запишите снимок памяти и найдите объекты, которые остаются в памяти дольше, чем должны.
  • Сравните несколько снимков памяти, сделанных с интервалом. Если одни и те же объекты остаются между снимками, это признак утечки.
  • Отслеживайте потребление памяти вашим приложением в реальном времени. Если график использования памяти постоянно растет и не снижается после выполнения задач, это указывает на возможную утечку памяти.

В заключение

Чтобы предотвратить утечку памяти, нужно:

  • Избегать глобальных переменных. Применяйте let и const, так как они ограничивают область видимости переменных.
  • Очищать слушатели событий. При работе с DOM обязательно удаляйте слушатели событий, когда они больше не нужны.
  • Разрывать ссылки на большие объекты. Устанавливайте ненужные переменные и объекты в null, чтобы освободить память.
  • Аккуратно использовать замыкания. Они могут удерживать ссылки на переменные, что приводит к утечкам, особенно в циклах, таймерах и интервалах.
  • Регулярно мониторить использование памяти. Используйте браузерные инструменты разработчика или специализированные инструменты.
***

При подготовке статьи использовалась публикация «Memory Leaks in JavaScript: A Simple Guide».

***

Весенний апгрейд навыков: -35% на курсы по программированию от Proglib Academy

  1. Основы IT для непрограммистов – для рекрутеров, маркетологов, проджект- и продакт-менеджеров
  2. Frontend Basic – стек технологий для старта в веб-разработке (HTML, CSS, React, Git, JavaScript)
  3. Математика для Data Science – подготовка к решению задач уровня FAANG-компаний
  4. Алгоритмы и структуры данных – глубокое погружение для подготовки к техническим собеседованиям
  5. Базовые модели ML – введение в машинное обучение с фокусом на tree-based модели
  6. Архитектуры и шаблоны проектирования – освоите основные паттерны проектирования и прокачаете свои навыки архитектора программного обеспечения

Комментарии

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ