Память и сборщик мусора в TypeScript: что скрывается за кулисами?
Представьте, что вы строите дом. Вы закупаете кирпичи, цемент, арматуру — но если не убирать строительный мусор, скоро стройплощадка превратится в свалку. То же самое происходит в вашем коде на TypeScript. Вы создаёте объекты, массивы, замыкания — и если вовремя не освобождать память, приложение начинает тормозить, а потом и вовсе падает с ошибкой Out of Memory.
В этой статье мы разберём, как работает управление памятью в TypeScript и его главный инструмент — сборщик мусора. Вы узнаете, какие механизмы стоят за освобождением памяти, как избежать утечек и почему ваш код может «кушать» мегабайты даже после того, как вы перестали им пользоваться.
Поехали!
Как TypeScript управляет памятью? Основы
Начнём с важного: TypeScript — это не отдельный язык, а надстройка над JavaScript. Это значит, что память и сборщик мусора в TypeScript работают точно так же, как в JavaScript. TypeScript не добавляет новых механизмов управления памятью, но даёт нам инструменты (типы, интерфейсы), чтобы писать более безопасный код.
В JavaScript память делится на две основные области:
- Стек (Stack) — хранит примитивы (числа, строки, булевы значения) и ссылки на объекты. Освобождается автоматически при выходе из функции.
- Куча (Heap) — хранит объекты, массивы, функции. Именно здесь работает сборщик мусора.
Когда вы пишете:
const user = { name: 'Alice', age: 30 };
Переменная user хранится в стеке и содержит ссылку на объект в куче. Пока есть ссылка — объект жив. Как только ссылка исчезает — объект становится «мусором».
Сборщик мусора в TypeScript: как он работает?
Сборщик мусора (Garbage Collector, GC) — это автоматический процесс, который находит объекты, на которые больше нет ссылок, и освобождает занимаемую ими память. Современные движки (V8 в Chrome, SpiderMonkey в Firefox) используют сложные алгоритмы, но основа — это алгоритм пометок и очистки (Mark-and-Sweep).
Как это работает:
- GC начинает с «корней» — глобальных объектов, локальных переменных в функциях, которые выполняются.
- Он проходит по всем ссылкам от корней и помечает каждый достижимый объект.
- Все непомеченные объекты считаются мусором и удаляются.
Звучит просто, но на практике есть нюансы. Например, циклические ссылки:
function createCycle() {
const obj1: any = {};
const obj2: any = {};
obj1.ref = obj2;
obj2.ref = obj1;
return 'done';
}
Раньше такие структуры приводили к утечкам, но современные сборщики мусора отлично справляются с циклами — главное, чтобы оба объекта были недостижимы из корней.
Типичные утечки памяти в TypeScript-приложениях
Даже с умным сборщиком мусора можно «наступить на грабли». Вот самые частые сценарии, которые приводят к утечкам памяти в TypeScript:
1. Забытые таймеры и обработчики событий
Одна из самых распространённых причин. Вы подписались на событие, а отписаться забыли:
class TimerComponent {
private intervalId: number;
start() {
this.intervalId = window.setInterval(() => {
console.log('Tick');
}, 1000);
}
// Забыли вызвать stop() — таймер будет жить вечно!
}
Решение: всегда очищайте таймеры и отписывайтесь от событий в методе dispose или ngOnDestroy (для Angular).
2. Замыкания, которые держат ссылки
Замыкания — мощный инструмент, но они могут случайно удерживать огромные объекты:
function createHeavyClosure() {
const hugeData = new Array(1000000).fill('data');
return function() {
console.log('Hello');
// hugeData остаётся в памяти, даже если не используется
};
}
const fn = createHeavyClosure();
// hugeData будет жить, пока жива fn
В таких случаях помогает обнуление ссылок или использование WeakMap для хранения данных.
3. Глобальные переменные и синглтоны
Если вы храните данные в глобальном объекте или синглтоне, они никогда не будут собраны сборщиком мусора:
class GlobalCache {
private static cache: Map<string, any> = new Map();
static set(key: string, value: any) {
this.cache.set(key, value);
}
// Нет метода clear() — кэш растёт бесконечно
}
Решение: добавляйте методы очистки, используйте WeakMap или ограничивайте размер кэша.
4. DOM-ссылки в одностраничных приложениях
Если вы удаляете DOM-элемент, но где-то осталась ссылка на него в JavaScript, элемент не будет собран:
const elements: HTMLElement[] = [];
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // Ссылка сохраняется
}
// Даже если div удалён из DOM, он остаётся в памяти
Инструменты для борьбы с утечками: WeakMap и WeakSet
TypeScript (и JavaScript) предоставляют специальные коллекции, которые «дружат» со сборщиком мусора — WeakMap и WeakSet. Их ключи могут быть только объектами, и они не препятствуют сборке мусора.
Пример — кеширование данных для DOM-элементов:
const cache = new WeakMap<HTMLElement, any>();
function processElement(el: HTMLElement) {
if (!cache.has(el)) {
cache.set(el, { processed: true, timestamp: Date.now() });
}
return cache.get(el);
}
// Когда элемент удаляется из DOM и теряет все ссылки,
// запись в WeakMap автоматически удаляется сборщиком мусора
Это элегантное решение для оптимизации памяти в TypeScript без ручного управления.
Как отслеживать утечки памяти?
Самый надёжный способ — использовать инструменты разработчика в браузере:
- Откройте вкладку Memory в Chrome DevTools.
- Сделайте снимок кучи (Heap Snapshot).
- Выполните действия в приложении (например, откройте и закройте модальное окно).
- Сделайте второй снимок и сравните — если количество объектов раст