Память и производительность
JavaScript управляет памятью автоматически: программисту не нужно вручную выделять и освобождать её, как в C или C++. Однако автоматическое управление не освобождает от ответственности. Неправильная работа с замыканиями, событиями, глобальными объектами и ссылками может привести к утечкам памяти, замедлению приложения и даже к падению вкладки. Понимание того, как движок выделяет, хранит и освобождает память, помогает писать эффективный и стабильный код.
Содержание
- Стек и куча
- Сборка мусора
- Поколения и алгоритм Mark-and-Sweep
- Типичные утечки памяти
- Инструменты для поиска утечек
WeakRefиFinalizationRegistry- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Стек и куча
В JavaScript данные хранятся в двух основных областях памяти: стеке и куче.
Стек
Стек — это быстрая, но ограниченная по размеру область памяти. В нём хранятся:
- примитивные значения (
number,string,booleanи т.д.); - ссылки на объекты из кучи;
- локальные переменные и параметры функций;
- адреса возврата из функций.
Когда функция завершается, её фрейм удаляется из стека автоматически — вместе с локальными переменными.
function calc() {
let a = 10; // a хранится в стеке
let b = 20; // b хранится в стеке
return a + b;
}
calc(); // после вызова фрейм calc удаляется из стека
Куча
Куча — это большая область памяти для долгоживущих объектов. В ней хранятся объекты, массивы, функции, замыкания и другие сложные структуры.
function createUser(name) {
return { name }; // объект создаётся в куче
}
const user = createUser('Alice');
После выполнения createUser локальная переменная name исчезает из стека, но объект { name: 'Alice' } остаётся в куче, потому что на него ссылается переменная user.
Размеры и ограничения
Стек обычно ограничен несколькими мегабайтами. Слишком глубокая рекурсия переполняет стек:
function infinite() {
infinite();
}
infinite(); // RangeError: Maximum call stack size exceeded
Куча значительно больше, но её тоже можно исчерпать — например, бесконечным ростом массива или утечками памяти.
Сборка мусора
Сборка мусора (Garbage Collection, GC) — это процесс автоматического освобождения памяти, которая больше не нужна программе. В JavaScript за это отвечает движок (V8 в Chrome и Node.js, SpiderMonkey в Firefox, JavaScriptCore в Safari).
Основной принцип: если на объект нельзя добраться из корней (roots), его можно удалить. Корни — это глобальные объекты, текущий стек вызовов, локальные переменные и замыкания.
let user = { name: 'Alice' };
user = null;
// Теперь объект { name: 'Alice' } недостижим из корней
// Сборщик мусора освободит его память
Важно: программист не может явно запустить сборку мусора. В Node.js есть global.gc(), но он отключён по умолчанию и не рекомендуется к использованию в продакшене.
Поколения и алгоритм Mark-and-Sweep
Современные движки используют генерационную сборку мусора: объекты делятся на молодое и старое поколения.
Молодое поколение
Большинство объектов живут недолго: временные переменные, промежуточные массивы, объекты в коротких функциях. Для них используется быстрый, но частый сборщик — обычно Scavenge.
Старое поколение
Объекты, пережившие несколько циклов сборки мусора, попадают в старое поколение. Для них используется более тщательный, но реже запускаемый Mark-and-Sweep:
- Mark — движок проходит по всем достижимым объектам от корней и помечает их как «живые».
- Sweep — непомеченные объекты считаются мусором, и их память возвращается в пул свободной.
Stop-the-world
Полная сборка мусора в старом поколении может приостанавливать выполнение кода на короткое время. Это называется stop-the-world pause. В современных движках эти паузы минимизируются с помощью инкрементальной и параллельной сборки.
Чтобы снизить нагрузку на GC:
- избегайте создания лишних объектов в горячих циклах;
- не держите в памяти данные, которые больше не нужны;
- используйте
WeakMap/WeakSetдля кэшей и подписок.
Типичные утечки памяти
Утечка памяти возникает, когда объект больше не нужен программе, но на него остаётся ссылка, и сборщик мусора не может его удалить.
1. Глобальные переменные
Неявные глобальные переменные в нестрогом режиме и явные глобальные объекты живут до закрытия вкладки или завершения процесса.
function leak() {
window.bigData = new Array(10_000_000).fill('x');
}
// bigData останется в памяти до перезагрузки страницы
Решение: минимизируйте использование глобальных переменных, используйте let/const, включайте strict mode.
2. Забытые слушатели событий
const button = document.getElementById('btn');
function handleClick() {
console.log('clicked');
}
button.addEventListener('click', handleClick);
// Если элемент удаляется из DOM, но обработчик не снят,
// элемент продолжает удерживаться в памяти.
Решение: снимайте слушатели перед удалением элементов или используйте WeakRef/AbortController.
3. Замыкания
Замыкания могут удерживать в памяти больше данных, чем нужно:
function createHeavyClosure() {
const hugeArray = new Array(1_000_000).fill('data');
return function () {
console.log(hugeArray.length);
};
}
const closure = createHeavyClosure();
// hugeArray остаётся в памяти, пока живёт closure
Если внутренняя функция использует лишь часть данных, старайтесь не захватывать всё окружение.
4. Таймеры и интервалы
const data = { value: 'important' };
setInterval(() => {
console.log(data.value);
}, 1000);
// Если интервал больше не нужен, но не очищен,
// data и callback останутся в памяти.
Решение: всегда вызывайте clearInterval/clearTimeout при размонтировании компонентов или завершении работы.
5. Отсоединённые DOM-узлы
const element = document.getElementById('node');
element.remove();
// Если в коде осталась ссылка на element,
// узел не будет собран сборщиком мусора.
Решение: обнуляйте ссылки на удалённые элементы, если они больше не нужны.
6. Кэши без ограничений
const cache = new Map();
function getData(key) {
if (cache.has(key)) return cache.get(key);
const data = fetchData(key);
cache.set(key, data); // кэш растёт бесконечно
return data;
}
Решение: ограничивайте размер кэша (LRU), используйте WeakMap, если ключи — объекты, и жизненный цикл которых не должен продлеваться кэшем.
Инструменты для поиска утечек
Chrome DevTools — Memory
Вкладка Memory позволяет:
- сделать heap snapshot — снимок кучи и найти крупные объекты;
- записать allocation timeline — увидеть, когда и сколько памяти выделяется;
- запустить allocation sampling — профилирование выделения памяти во времени.
Типичный сценарий:
- Открыть приложение.
- Сделать первый снимок кучи.
- Выполнить подозрительные действия (например, открыть/закрыть модалку несколько раз).
- Сделать второй снимок.
- Сравнить снимки и найти объекты, которые не были освобождены.
Node.js — --inspect
node --inspect index.js
После этого можно подключиться Chrome DevTools и анализировать память Node.js-приложения.
WeakRef и FinalizationRegistry
WeakRef и FinalizationRegistry — относительно новые API для слабых ссылок на объекты. Они позволяют держать ссылку на объект, не препятствуя его сборке мусора.
WeakRef
WeakRef не удерживает объект от сборки мусора. Чтобы получить объект, нужно вызвать .deref(). Если объект уже удалён, .deref() вернёт undefined.
let user = { name: 'Alice' };
const ref = new WeakRef(user);
user = null; // объект может быть собран
setTimeout(() => {
const obj = ref.deref();
if (obj) {
console.log(obj.name);
} else {
console.log('Object was garbage collected');
}
}, 1000);
WeakRef полезен для кэширования больших объектов, которые можно пересоздать, если их удалили.
FinalizationRegistry
FinalizationRegistry позволяет зарегистрировать callback, который выполнится, когда объект собран сборщиком мусора.
const registry = new FinalizationRegistry((name) => {
console.log(`${name} was garbage collected`);
});
let user = { name: 'Alice' };
registry.register(user, 'Alice');
user = null;
// Через какое-то время может появиться сообщение:
// Alice was garbage collected
Важно: callback запускается с неопределённой задержкой, и на него нельзя полагаться для точного управления ресурсами. FinalizationRegistry подходит для логирования, метрик и некритичной очистки, но не для освобождения файловых дескрипторов или сетевых соединений.
Ключевые тезисы для интервью
- JavaScript управляет памятью через стек (примитивы, ссылки, локальные переменные) и кучу (объекты, массивы, замыкания).
- Сборка мусора работает по принципу достижимости: если на объект нельзя добраться из корней (roots), его память освобождается.
- Движок использует генерационную сборку: Scavenge для молодого поколения и Mark-and-Sweep для старого.
- Глобальные переменные, забытые слушатели событий и замыкания — типичные источники утечек памяти во фронтенде.
WeakMapиWeakSetпозволяют хранить ссылки, не препятствуя сборке мусора, и подходят для кэшей и подписок.- Chrome DevTools (вкладка Memory) позволяет находить утечки через heap snapshots и allocation timeline.
WeakRefдержит слабую ссылку на объект, аFinalizationRegistryрегистрирует callback при его сборке.- На
FinalizationRegistryнельзя полагаться для критичных ресурсов, потому что callback выполняется с неопределённой задержкой. - Кэш без ограничений растёт бесконечно; для контроля размера используют LRU-стратегию или
WeakMap.
Заключение
JavaScript автоматически управляет памятью, но это не снимает с разработчика ответственности за утечки. Понимание работы стека и кучи, алгоритмов сборки мусора и типичных источников утечек помогает писать стабильные и производительные приложения. Для диагностики проблем используйте Chrome DevTools и Node.js --inspect, а для предотвращения утечек применяйте WeakMap, WeakRef и ограниченные кэши.