← Назад к вопросам

Когда возникает утечка памяти?

2.2 Middle🔥 112 комментариев
#JavaScript Core

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Когда возникает утечка памяти в JavaScript

Утечка памяти — это ситуация, когда приложение занимает всё больше и больше памяти с течением времени, потому что объекты, которые больше не нужны, не удаляются из памяти. Это особенно актуально для фронтенда, где приложения работают долгое время.

1. Забытые обработчики событий

Оди из самых распространённых причин утечек памяти — это забытые event listeners:

class Modal {
  constructor() {
    this.element = document.getElementById('modal');
    
    // Добавляем слушателя
    this.element.addEventListener('click', this.handleClick);
  }
  
  handleClick = () => {
    console.log('Clicked');
  }
  
  // ПРОБЛЕМА: слушатель не удалён
  destroy() {
    this.element.remove();
  }
}

const modal = new Modal();
modal.destroy(); // Слушатель остаётся в памяти!

Правильно:

class Modal {
  constructor() {
    this.element = document.getElementById('modal');
    this.handleClick = this.handleClick.bind(this);
    this.element.addEventListener('click', this.handleClick);
  }
  
  handleClick() {
    console.log('Clicked');
  }
  
  // ПРАВИЛЬНО: удаляем слушателя
  destroy() {
    this.element.removeEventListener('click', this.handleClick);
    this.element.remove();
  }
}

2. Забытые таймеры (setInterval, setTimeout)

class Timer {
  constructor() {
    this.counter = 0;
  }
  
  start() {
    // ПРОБЛЕМА: setInterval никогда не останавливается
    setInterval(() => {
      this.counter++;
      console.log(this.counter);
    }, 1000);
  }
  
  destroy() {
    // Но что с интервалом?
  }
}

const timer = new Timer();
timer.start();
timer.destroy(); // Интервал продолжает работать и занимать память

Правильно:

class Timer {
  constructor() {
    this.counter = 0;
    this.intervalId = null;
  }
  
  start() {
    // Сохраняем ID интервала
    this.intervalId = setInterval(() => {
      this.counter++;
      console.log(this.counter);
    }, 1000);
  }
  
  destroy() {
    // ПРАВИЛЬНО: очищаем интервал
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }
}

3. Замыкания с большими объектами

function processLargeData() {
  const largeArray = new Array(1000000).fill(Math.random());
  
  // ПРОБЛЕМА: функция обратного вызова замыкает largeArray
  // даже если использует только один элемент
  const callback = () => {
    console.log(largeArray[0]);
  };
  
  // largeArray остаётся в памяти, пока существует callback
  someEventEmitter.on('event', callback);
}

Правильно:

function processLargeData() {
  const largeArray = new Array(1000000).fill(Math.random());
  const firstElement = largeArray[0];
  
  // ПРАВИЛЬНО: замыкаем только необходимое значение
  const callback = () => {
    console.log(firstElement);
  };
  
  someEventEmitter.on('event', callback);
}

4. Циклические ссылки

class Node {
  constructor(value) {
    this.value = value;
    this.parent = null;
    this.children = [];
  }
}

const parent = new Node('parent');
const child = new Node('child');

// ПРОБЛЕМА: циклические ссылки
parent.children.push(child);
child.parent = parent;

// Даже если удалить переменные, они останутся в памяти
delete parent; // Но parent всё ещё доступен через child.parent
delete child;  // Но child всё ещё доступен через parent.children

Решение: Используйте WeakMap/WeakSet для небольших ссылок:

class Node {
  constructor(value) {
    this.value = value;
    // WeakMap не препятствует garbage collection
    this.parentRef = null;
    this.children = [];
  }
  
  setParent(parent) {
    // WeakMap позволяет удалить parent, если на него нет других ссылок
    this.parentRef = parent;
  }
}

5. Запросы AJAX и промисы

class DataLoader {
  loadData() {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        // ПРОБЛЕМА: если это вызывается много раз,
        // промисы остаются в памяти
        console.log(data);
      });
  }
}

const loader = new DataLoader();
// При каждой загрузке создаётся новый промис
for (let i = 0; i < 1000; i++) {
  loader.loadData();
}

Правильно:

class DataLoader {
  constructor() {
    this.abortController = null;
  }
  
  loadData() {
    // Отменяем предыдущий запрос
    if (this.abortController) {
      this.abortController.abort();
    }
    
    this.abortController = new AbortController();
    
    return fetch('/api/data', { signal: this.abortController.signal })
      .then(response => response.json())
      .then(data => console.log(data));
  }
  
  destroy() {
    if (this.abortController) {
      this.abortController.abort();
    }
  }
}

6. В React: забытые эффекты

function UserProfile({ userId }) {
  useEffect(() => {
    // ПРОБЛЕМА: fetch не отменяется при размонтировании
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => setUser(data));
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

Правильно:

function UserProfile({ userId }) {
  useEffect(() => {
    const abortController = new AbortController();
    
    fetch(`/api/users/${userId}`, { signal: abortController.signal })
      .then(r => r.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });
    
    // ПРАВИЛЬНО: cleanup function отменяет fetch
    return () => abortController.abort();
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

7. В React: забытые подписки

function LiveCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // ПРОБЛЕМА: подписка никогда не удаляется
    eventEmitter.on('increment', () => {
      setCount(c => c + 1);
    });
  }, []);
  
  return <div>{count}</div>;
}

Правильно:

function LiveCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handler = () => setCount(c => c + 1);
    eventEmitter.on('increment', handler);
    
    // ПРАВИЛЬНО: cleanup function удаляет подписку
    return () => eventEmitter.off('increment', handler);
  }, []);
  
  return <div>{count}</div>;
}

8. Большие объекты в глобальной области

// ПРОБЛЕМА: глобальный объект не удаляется
window.hugeCache = new Array(10000000).fill(Math.random());

// Или
let globalData = { /*большой объект*/ };

Правильно:

// Используйте Map с WeakKey
const cache = new Map();

function getCached(key) {
  return cache.get(key);
}

function setCached(key, value) {
  cache.set(key, value);
}

// Или используйте Library для LRU Cache
import LRUCache from 'lru-cache';
const cache = new LRUCache({ max: 500 });

Как найти утечку памяти

1. Chrome DevTools Memory tab

1. F12 -> Memory
2. Take a heap snapshot
3. Выполните несколько операций
4. Take another heap snapshot
5. Сравните — ищите растущие объекты

2. Профилирование

console.time('operation');
// Ваш код
console.timeEnd('operation');

Лучшие практики

  • Всегда удаляйте event listeners при удалении элементов
  • Очищайте таймеры в cleanup functions
  • Отменяйте fetch запросы при размонтировании
  • Используйте WeakMap/WeakSet для вспомогательных данных
  • Избегайте циклических ссылок где это возможно
  • Профилируйте память в Chrome DevTools
  • Ограничивайте размер кешей (LRU, Limited size)

Вывод

Утечки памяти возникают из-за забытых ссылок на объекты, которые приложение больше не использует. В фронтенде основные виновники — event listeners, таймеры, промисы и замыкания. Всегда помните о cleanup functions в React и удалении обработчиков событий вручную.

Когда возникает утечка памяти? | PrepBro