Kodokon kodokon.com

メモリとパフォーマンス: GC、WeakMap/WeakRef、debounce/throttle

ガベージコレクタの仕組みを理解し、弱参照を使ってリークを避け、debounce と throttle で高コストな処理を平準化します。

11 分 · 3 問

このレッスンを Kodokon で開く

V8 のガベージコレクタは到達可能性(reachability)に基づいています。オブジェクトは、それをルート(スタック、アクティブなスコープ、グローバル)につなぐ参照の連鎖がなくなった途端、回収可能になります。GC は世代別(generational)です。若いオブジェクトは *nursery* に住み、頻繁かつ高速に回収されます(scavenge)。生き残ったものは旧世代に昇格し、インクリメンタルなマーク・アンド・スイープで扱われます。古典的なリークは GC が原因ではなく、あなたが忘れた参照が原因です。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 はキーを保持しません。キーが他のどこからも到達不能になると、エントリ全体が消えます。設計上の帰結はこうです。キーはオブジェクト(または未登録のシンボル)でなければならず、この構造は反復も計測もできません。そのサイズを公開してしまうと、非決定的な GC の挙動が露見してしまうからです。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 を返すこともあります。

体感パフォーマンスの側面では、高頻度のイベント(scrollresizeinputmousemove)は 1 秒に数百回もの呼び出しを引き起こしかねません。相補的な 2 つの戦略があります。debounce は静かな期間が続いたあとにのみ関数を実行します(入力しながらの検索に理想的)。一方throttle は間隔ごとに最大 1 回の実行を保証します(スクロールの追跡に理想的)。

JAVASCRIPT
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(
      () => fn.apply(this, args),
      delay
    );
  };
}
Debounce: 連続呼び出しのうち最後の 1 回だけが有効になります。
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 回の実行。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. なぜ WeakMap のキーは文字列のようなプリミティブではなくオブジェクトでなければならないのですか?
    • ハッシュ計算のパフォーマンス上の理由から
    • オブジェクトへの参照だけが到達不能になり、エントリの削除を引き起こせるから
    • ES2021 以降は解消された歴史的な制約だから
    • JavaScript ではプリミティブをキーとして使えないから
  2. debounce と throttle の根本的な違いは何ですか?
    • debounce は静かな期間のあとに実行し、throttle は間隔ごとに最大 1 回の実行を保証する
    • debounce は非同期で、throttle は同期である
    • throttle は以前の呼び出しをキャンセルし、debounce はそれらをキューに入れる
    • この 2 つは同じ手法の別名である
  3. WeakRef に対する ref.deref() は何を返しうるでしょうか?
    • WeakRef が存在するかぎり、常に元のオブジェクト
    • まだ生きていればそのオブジェクト、回収されていれば undefined
    • オブジェクトのディープコピー
    • オブジェクトが回収されていれば null