Kodokon kodokon.com

Память и производительность: GC, WeakMap/WeakRef, debounce/throttle

Разберись, как работает сборщик мусора, используй слабые ссылки, чтобы избежать утечек, и сглаживай дорогую работу через debounce и throttle.

11 мин · 3 вопросов

Открыть этот урок в Kodokon

Сборщик мусора V8 основан на достижимости: объект становится собираемым, как только ни одна цепочка ссылок не связывает его с корнями (стеком, активными областями видимости, глобальными объектами). Сборщик поколенческий: молодые объекты живут в *nursery*, где их собирают часто и быстро (scavenge); выжившие переходят в старое поколение, которым занимается инкрементальный mark-and-sweep. Классические утечки идут не от сборщика, а от ссылок, которые ты забыл убрать: кэши на основе Map, слушатели, которые никогда не отсоединяются, замыкания, захватывающие крупные объекты.

JAVASCRIPT
const strongCache = new Map();
const weakCache = new WeakMap();

let session = { user: "ada" };
strongCache.set(session, "data");
weakCache.set(session, "data");

session = null;
// Map: the key retains the object -> memory leak
// WeakMap: the entry becomes collectable
Один и тот же код, две противоположные судьбы в памяти.

WeakMap не удерживает свои ключи: когда ключ становится недостижимым отовсюду ещё, вся запись исчезает. Следствия для проектирования: ключи должны быть объектами (или незарегистрированными символами), а сама структура не итерируема и не измерима - раскрытие её размера выдало бы поведение сборщика мусора, которое недетерминировано. WeakRef идёт дальше: он даёт тебе прямую слабую ссылку, которую нужно разыменовывать через deref().

JAVASCRIPT
let config = { theme: "dark" };
const ref = new WeakRef(config);

function readTheme() {
  const target = ref.deref();
  return target ? target.theme : "default";
}

console.log(readTheme()); // "dark"
config = null;
// After a GC pass, deref() may
// return undefined: plan a fallback.
deref() может вернуть объект... или undefined.

Что касается воспринимаемой производительности, события высокой частоты (scroll, resize, input, mousemove) могут вызывать сотни срабатываний в секунду. Две взаимодополняющие стратегии: debounce запускает функцию только после паузы (идеально для поиска по мере ввода), тогда как throttle гарантирует не больше одного выполнения за интервал (идеально для отслеживания прокрутки).

JAVASCRIPT
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(
      () => fn.apply(this, args),
      delay
    );
  };
}
Debounce: считается только последний вызов из серии.
JAVASCRIPT
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}
Throttle: не больше одного выполнения за интервал.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Почему ключи WeakMap должны быть объектами, а не примитивами вроде строк?
    • По соображениям производительности хеширования
    • Потому что только ссылка на объект может стать недостижимой и вызвать удаление записи
    • Это историческое ограничение, снятое начиная с ES2021
    • Потому что примитивы нельзя использовать как ключи в JavaScript
  2. В чём фундаментальное различие между debounce и throttle?
    • Debounce выполняется после паузы, throttle гарантирует не больше одного выполнения за интервал
    • Debounce асинхронный, throttle синхронный
    • Throttle отменяет предыдущие вызовы, debounce ставит их в очередь
    • Это два названия одной и той же техники
  3. Что может вернуть ref.deref() у WeakRef?
    • Всегда исходный объект, пока существует WeakRef
    • Объект, если он ещё жив, или undefined, если он был собран
    • Глубокую копию объекта
    • null, если объект был собран