Как устроены стек вызовов и куча в JavaScript
Что проверяют
Понимаете ли вы, где выполняется код и где живут данные, и можете ли связать это с event loop и утечками памяти во фронтенде. Следом обычно дают задачу на порядок вывода setTimeout и Promise.then или спрашивают, как вы искали утечку в SPA.
Как отвечать
Стек вызовов это стек контекстов выполнения. При вызове функции на него кладётся фрейм с аргументами, локальными переменными и точкой возврата, при выходе фрейм снимается. В одной вкладке или воркере поток JavaScript один, поэтому пока стек не пуст, ничего другого не выполняется. Слишком глубокая рекурсия заканчивается RangeError: Maximum call stack size exceeded.
Асинхронность устроена поверх стека:
- Web API: таймеры,
fetch, события работают вне движка и по готовности ставят колбэк в очередь; - микрозадачи: колбэки промисов, продолжения после
await,queueMicrotask. Как только стек опустел, выполняются все микрозадачи подряд; - макрозадачи: таймеры, события,
postMessage. Берётся одна, затем снова все микрозадачи, между задачами браузер может отрисовать кадр.
Куча это область для объектов, массивов, функций и замыканий. Фраза «примитивы в стеке, объекты в куче» годится как упрощение, но в V8 строки лежат в куче, а захваченные замыканием переменные переезжают в объект контекста, иначе исчезли бы вместе с фреймом.
Память освобождает сборщик мусора по достижимости от корней (глобальный объект, стек), поэтому циклические ссылки ему не мешают. В V8 он поколенческий: молодые объекты чистит копирующий Scavenger, выживших в старом поколении обрабатывает Mark-Compact.
Где спотыкаются
Говорят, что setTimeout(fn, 0) выполнится сразу после текущей строки. Он встанет в очередь макрозадач и отработает только после всех микрозадач.
Путают утечку с просто большой памятью. Типичные утечки во фронтенде: забытые addEventListener и setInterval, отсоединённые DOM-узлы, на которые осталась ссылка, растущие кэши на уровне модуля. Ищут их снимками кучи во вкладке Memory в Chrome DevTools.
Считают, что сборщик работает по счётчику ссылок и не справляется с циклами. Современные движки используют трассирующую сборку, mark-and-sweep и её варианты.