Перестаньте утекать память с WeakMap
Исправляем слабый код с помощью слабых ссылок!
Помните то чувство, когда меняешь одну строку кода и видишь, как потребление памяти падает на 50%? У меня был такой момент, когда я смотрел на Performance Monitor в Chrome DevTools: панель управления, которая раньше выжирала 100 МБ в час, работала чисто целый день после обеда.
Изменение в одну строку: new Map() превратилось в new WeakMap().
Вот и всё. Тот же API, тот же паттерн использования, совершенно разное поведение под капотом. Но понимание того, почему это работает, требует осознания того, о чём большинство JavaScript-разработчиков никогда не задумываются: что происходит, когда на ваши данные больше никто не смотрит.
Когда ссылки становятся якорями
Обычный Map в JavaScript держится за свои ключи мёртвой хваткой. Как только вы что-то туда поместили, Map будет держать это железной хваткой. Сборщик мусора видит эту связь и думает: «Очевидно, этот объект всё ещё нужен, лучше его не трогать».
Эта защитная инстинктивность становится проблемой, когда вы храните метаданные о временных вещах. DOM-узлы, которые будут удалены. Сессии пользователей, которые истекают. Экземпляры компонентов, которые размонтируются. Map не знает, что эти объекты больше не нужны. Он просто знает, что у него есть ссылка, поэтому он держит их живыми.
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// Элемент исчез из DOM, но cache всё ещё держит его в памятиСборщик мусора не может очистить element, потому что cache всё ещё указывает на него. Это называется «сильная ссылка», и в долгоживущих Single Page App это превращается в утечку, которая в конечном итоге роняет браузер.
WeakMap меняет правила
WeakMap работает иначе. Он относится к своим ключам как к временным жильцам, а не как к постоянным резидентам. Когда вы сохраняете что-то в WeakMap, вы по сути говорите: «Я хочу связать эти данные с этим объектом, но я не хочу быть причиной того, что он остаётся в живых».
Если единственное, что удерживает объект в памяти, — это WeakMap, сборщик мусора может его забрать. Когда объект исчезает, запись в WeakMap исчезает вместе с ним. Ручная очистка не нужна.
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// Элемент уходит в сборку мусора// Запись в кеше исчезает автоматическиЯ запустил бенчмарк: создал 100 000 DOM-узлов, сохранил метаданные о каждом, а затем удалил их все. С Map браузер держал 150–200 МБ. С WeakMap — 70–80 МБ. Тот же код, та же функциональность, вдвое меньше памяти.
От чего придётся отказаться
У WeakMap есть ограничения, которые кажутся недостатками, пока вы не поймёте, что именно они и обеспечивают всю магию.
Нельзя итерировать WeakMap. Нет forEach, нет keys(), нет values(). Это логично, если подумать: сборщик мусора может удалить запись прямо во время вашего цикла. Вы действительно хотите с этим разбираться?
Нельзя узнать размер. Нет .size, нет .length. Опять же, это подвижная цель. Число может измениться между тем, как вы спросили, и тем, когда получили ответ.
Ключами могут быть только объекты. Ни строк, ни чисел, ни примитивов. Это фундаментально для работы слабых ссылок: у примитивных значений нет идентичности, которую можно отслеживать отдельно от их значения.
Это не баги. Это дизайн. WeakMap создан для одной конкретной задачи: прикреплять метаданные к объектам, не мешая этим объектам быть очищенными. Если вам нужен обход, примитивные ключи или подсчёт записей, вы, скорее всего, решаете другую проблему и должны использовать обычный Map.
Где это действительно помогает
Паттерн «приватные данные» был оригинальным вариантом использования WeakMap, ещё до того, как в JavaScript появились #private поля. Библиотеки создавали WeakMap вне класса и использовали его для хранения данных, которые не должны быть доступны на экземпляре.
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}Когда экземпляр User уходит в сборку мусора, приватные данные уходят вместе с ним. Код для очистки не нужен.
Мемоизация — ещё одно естественное применение, особенно когда вы кешируете результаты на основе объектных аргументов, а не примитивных значений. Если ваше дорогое вычисление принимает на вход конфигурационный объект, WeakMap позволяет не беспокоиться о том, что кеш переживёт конфиги.
const cache = new WeakMap();
function expensiveCalc(obj) { if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj); cache.set(obj, result); return result;}Кеш живёт ровно столько, сколько живут кешируемые объекты. Как только на obj больше нет ссылок, результат и запись в кеше исчезают вместе.
Когда стоит применять
Утечки памяти в современных веб-приложениях обычно возникают из-за устаревших ссылок на объекты, которые давно пора было очистить. Если вы создаёте что-то долгоживущее — панель управления, которая открыта весь день, чат, работающий часами, админку, которая никогда не обновляется — вам нужно думать о том, что происходит со старыми данными.
WeakMap особенно полезен, когда вы связываете данные с DOM-узлами, экземплярами компонентов или любыми объектами, чей жизненный цикл вы не контролируете. Если вы храните что-то на основе ссылки, и эта ссылка может исчезнуть, WeakMap значительно упрощает очистку.
Обычный Map остаётся правильным выбором, когда вы строите настоящий кеш с политиками вытеснения, когда нужно итерировать записи, когда вы используете примитивные ключи или когда важны сами данные, а не их привязка к объекту.
Приятная особенность WeakMap в том, что обычно очевидно, когда он нужен. Если вы ловите себя на том, что пишете код для удаления записей из Map при уничтожении объектов — это признак. Если вы переживаете, что память растёт бесконтрольно, потому что не знаете, когда удалять данные — это ещё один признак.
Иногда лучшая функциональность — это та, которая просто работает, не заставляя вас о ней думать.
Ресурсы
- MDN: WeakMap
- MDN: Управление памятью
- V8 Blog: Слабые ссылки и финализаторы
- JavaScript.info: WeakMap и WeakSet