Kodokon kodokon.com

หน่วยความจำและประสิทธิภาพ: GC, WeakMap/WeakRef, debounce/throttle

ทำความเข้าใจว่า garbage collector ทำงานอย่างไร ใช้ weak reference เพื่อหลีกเลี่ยงการรั่วไหล และปรับงานที่มีต้นทุนสูงให้ราบรื่นด้วย debounce และ throttle

11 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

garbage collector ของ V8 อิงกับ reachability อ็อบเจกต์จะเก็บกวาดได้ทันทีที่ไม่มีสายโซ่ของการอ้างอิงเชื่อมมันกับ root (สแตก, ขอบเขตที่กำลังทำงาน, global) GC เป็นแบบ generational อ็อบเจกต์วัยเยาว์อยู่ใน *nursery* ซึ่งถูกเก็บกวาดบ่อยและรวดเร็ว (scavenge) ผู้รอดชีวิตจะถูกเลื่อนไปสู่ generation เก่า ซึ่งจัดการด้วย mark-and-sweep แบบ incremental การรั่วไหลแบบคลาสสิกไม่ได้มาจาก GC แต่มาจากการอ้างอิงที่คุณลืม: แคชที่อิง Map, listener ที่ไม่เคยถูกถอด, closure ที่จับอ็อบเจกต์ขนาดใหญ่ไว้

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 ไม่ยึดคีย์ของมันไว้ เมื่อคีย์กลายเป็นเข้าถึงไม่ได้จากที่อื่นใด ทั้งรายการจะหายไป ผลที่ตามมาต่อการออกแบบ: คีย์ต้องเป็น อ็อบเจกต์ (หรือ symbol ที่ไม่ได้ลงทะเบียน) และโครงสร้างนี้ ทั้งวนซ้ำไม่ได้และวัดขนาดไม่ได้ การเปิดเผยขนาดของมันจะเผยพฤติกรรมของ GC ซึ่งเป็นแบบ non-deterministic WeakRef ไปไกลกว่านั้น มันให้การอ้างอิงแบบ weak โดยตรงแก่คุณ ซึ่งต้อง dereference ด้วย 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

ในด้านประสิทธิภาพที่รับรู้ได้ event ที่ความถี่สูง (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 จึงต้องเป็นอ็อบเจกต์ ไม่ใช่ primitive อย่างสตริง?
    • ด้วยเหตุผลด้านประสิทธิภาพของ hashing
    • เพราะมีแต่การอ้างอิงถึงอ็อบเจกต์เท่านั้นที่สามารถกลายเป็นเข้าถึงไม่ได้และทริกเกอร์การลบรายการ
    • เป็นข้อจำกัดเชิงประวัติศาสตร์ที่ถูกยกเลิกตั้งแต่ ES2021
    • เพราะ primitive ไม่สามารถใช้เป็นคีย์ใน JavaScript ได้
  2. อะไรคือความแตกต่างพื้นฐานระหว่าง debounce และ throttle?
    • debounce รันหลังช่วงเงียบ throttle รับประกันการทำงานอย่างมากหนึ่งครั้งต่อช่วงเวลา
    • debounce เป็นแบบอะซิงโครนัส throttle เป็นแบบซิงโครนัส
    • throttle ยกเลิกการเรียกก่อนหน้า debounce จัดคิวไว้
    • ทั้งสองเป็นชื่อของเทคนิคเดียวกัน
  3. ref.deref() บน WeakRef สามารถคืนค่าอะไรได้บ้าง?
    • อ็อบเจกต์เดิมเสมอ ตราบเท่าที่ WeakRef ยังอยู่
    • อ็อบเจกต์ถ้ามันยังมีชีวิตอยู่ หรือ undefined ถ้ามันถูกเก็บกวาดไปแล้ว
    • สำเนาแบบลึกของอ็อบเจกต์
    • null ถ้าอ็อบเจกต์ถูกเก็บกวาดไปแล้ว