วิธีที่ Google Chat แก้ไขปัญหาเกี่ยวกับหน่วยความจำมากกว่า 30 รายการด้วยเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome สำหรับ Agent

Vatan Aggarwal
Vatan Aggarwal Software Engineer

เผยแพร่เมื่อวันที่ 29 ต.ค. 2026

Google Chat เป็นแอปพลิเคชันหน้าเว็บเดียวขนาดใหญ่ ผู้ใช้มักจะเปิดเว็บแอปทิ้งไว้ในแท็บเบราว์เซอร์เป็นเวลาหลายวันหรือหลายสัปดาห์ในแต่ละครั้ง ในแอปพลิเคชันที่มีอายุการใช้งานยาวนาน ประสิทธิภาพของหน่วยความจำฝั่งไคลเอ็นต์เป็นข้อกังวลหลัก การปิดที่ไม่ได้รวบรวมและแผนผัง DOM ที่แยกออกมาจะค่อยๆ สะสม ซึ่งท้ายที่สุดจะส่งผลต่อประสิทธิภาพของ UI, การหยุดชั่วคราวของ Garbage Collection หรือการขัดข้องของแท็บในอุปกรณ์ที่มีข้อจำกัด

ทีม Google Chat จึงได้เปิดตัวโครงการริเริ่มเพื่อค้นหาและแก้ไขปัญหาเกี่ยวกับหน่วยความจำเพื่อแก้ไขปัญหานี้ โดยใช้เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome สำหรับ Agent ซึ่งรวมถึงเซิร์ฟเวอร์ DevTools MCP เพื่อเปลี่ยนกระบวนการที่ต้องทำด้วยตนเองที่ยุ่งยากให้เป็นเวิร์กโฟลว์อัตโนมัติ

ความท้าทาย: งมเข็มในมหาสมุทรขนาด 500 MB

การระบุสาเหตุที่แน่ชัดว่าทำไมออบเจ็กต์ที่รั่วไหลจึงยังคงอยู่ในหน่วยความจำนั้นต้องใช้การติดตามเส้นทางการเก็บรักษาผ่านโค้ดที่ลดขนาดลงอย่างมาก แม้ว่าการปรับปรุงล่าสุดในแผงหน่วยความจำของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome (เช่น การกรอง Edge ขั้นสูงและการค้นหาออบเจ็กต์) จะช่วยให้ดำเนินการนี้ได้ง่ายขึ้น แต่การดำเนินการด้วยตนเองในสแนปชอตขนาด 500 MB ก็ยังอาจช้าและทำให้เหนื่อยล้าได้

และการแก้ไขการรั่วไหลต้องอาศัยความรู้ 2 ชุดที่แตกต่างกัน ดังนี้

  • คุณต้องเข้าใจวิธีที่เครื่องมือ V8 JavaScript จัดการกับไบต์โค้ดที่คอมไพล์แล้ว
  • นอกจากนี้ คุณยังต้องเข้าใจการจัดการสถานะและวงจรของคอมโพเนนต์ที่เฉพาะเจาะจงของแอปพลิเคชันด้วย

การเชื่อมโยงความรู้ 2 ชุดที่แตกต่างกันนี้ด้วยตนเองมักใช้เวลาหลายวันในการตรวจสอบและทำงานร่วมกับนักพัฒนาซอฟต์แวร์หลายคนต่อการรั่วไหล 1 ครั้ง

วิธีแก้ปัญหา: สำรวจฮีปด้วย Agent

ทีมตระหนักว่าแม้โมเดลภาษาขนาดใหญ่จะให้เหตุผลเกี่ยวกับข้อมูลที่มีโครงสร้างได้ดีมาก แต่ก็ยังต้องการเครื่องมือที่เหมาะสมในการประมวลผลไฟล์ขนาดใหญ่โดยที่ยังคงใช้โทเค็นอย่างสมเหตุสมผล โดยการเชื่อมต่อ Google Antigravity กับเซิร์ฟเวอร์ MCP ของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome แบบโอเพนซอร์สที่เปิดใช้เครื่องมือหน่วยความจำ (--memoryDebugging=true) พวกเขาให้สิทธิ์เข้าถึงแบบเป็นโปรแกรมโดยตรงแก่ Agent ไปยังฮีป V8

แทนที่นักพัฒนาแอปจะต้องขยายแผนผัง Retainer ด้วยตนเองในแผงหน่วยความจำของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome ตัวแทนจะค้นหาภาพรวมโดยตรง ทีมใช้พรอมต์ที่ตรงไปตรงมาเพื่อเริ่มกระบวนการ ดังนี้

Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.

เมื่อใช้ทักษะการแก้ไขข้อบกพร่องของหน่วยความจำ Agent จะเรียกใช้เวิร์กโฟลว์ที่มีโครงสร้างโดยใช้เครื่องมือ MCP ที่เฉพาะเจาะจง ดังนี้

  1. บันทึกหรือโหลดสแนปชอต: ใช้เครื่องมืออย่าง take_heapsnapshot หรืออ่านไฟล์ที่มีอยู่
  2. วิเคราะห์โครงสร้างฮีป: สแกนหาแหล่งที่มาของการรั่วไหลที่พบบ่อยโดยใช้ get_heapsnapshot_details และ compare_heapsnapshots
  3. ติดตามเส้นทางการเก็บรักษา: ใช้ get_heapsnapshot_retaining_paths เพื่อย้อนกลับ Dominator Tree ไปยังรูทของ Garbage Collection
  4. สร้างการแก้ไข: เครื่องมือจะตรวจสอบเพื่อเปรียบเทียบเส้นทางการเก็บรักษาข้อมูลกับซอร์สโค้ดของแอปพลิเคชันเพื่อสร้างแพตช์ที่เฉพาะเจาะจงอย่างมาก

ข้อมูลเชิงลึกทางเทคนิค: รูปแบบหน่วยความจำรั่วไหลที่พบบ่อย 3 รูปแบบ

ตัวแทนพบการรั่วไหลที่แตกต่างกันกว่า 30 จุด รูปแบบ 3 รูปแบบต่อไปนี้เป็นรูปแบบที่ส่งผลกระทบมากที่สุดที่ Agent พบ ซึ่งแสดงถึงการประหยัดหน่วยความจำมากที่สุดและรูปแบบต่อต้านสถาปัตยกรรมที่พบบ่อยที่สุดในเว็บแอปพลิเคชันสมัยใหม่

แทรปสแต็กเทรซข้อผิดพลาดแบบคงที่

ตัวแทนระบุว่าการรั่วไหลครั้งใหญ่ที่สุดครั้งเดียวเชื่อมโยงกับออบเจ็กต์ข้อผิดพลาดที่จัดสรรไว้ล่วงหน้าซึ่งเก็บหน่วยความจำไว้เกือบ 20 MB อย่างถาวร ตัวอย่างเช่น ค่าคงที่ข้อผิดพลาดแบบคงที่ (EMPTY_KEY_ERROR) รายการเดียวเก็บข้อมูลไว้ 14.1 MB ด้วยตัวเอง

ปัญหา: ในเครื่องมือ JavaScript สมัยใหม่อย่าง V8 การสร้างออบเจ็กต์ Error จะจับภาพสแนปชอตของสแต็กการเรียกใช้ปัจจุบันเพื่อช่วยในการแก้ไขข้อบกพร่อง ขนาดที่คงไว้จำนวนมากเกิดขึ้นเนื่องจากกลไกนี้

  • ข้อผิดพลาดเป็นค่าคงที่ส่วนกลาง: มีการสร้างอินสแตนซ์ระหว่างการประเมินโมดูลเริ่มต้น
  • เครื่องมือ JavaScript บันทึกสแต็ก: ฟังก์ชันที่ทำงานอยู่ในสแต็กในขณะนั้นคือ Wrapper การเริ่มต้นหลักของแอปพลิเคชัน (Immediately Invoked Function Expression หรือ IIFE)
  • อาร์เรย์ดักจับหน่วยความจำ: เนื่องจากแอปพลิเคชันไม่เคยเก็บขยะข้อผิดพลาด อาร์เรย์สแต็กเทรซภายในจึงเก็บการอ้างอิงถาวรไปยังฟังก์ชันการเริ่มต้นของแพ็กเกจหลัก
// Ineffective pattern: Instantiating Error objects at module evaluation time
export const EMPTY_KEY_ERROR = new Error("Key cannot be empty");

export function validateKey(key: string) {
  if (!key) {
    throw EMPTY_KEY_ERROR;
  }
}

การแก้ไข: ทีมได้เปลี่ยนโค้ดเพื่อสร้างข้อผิดพลาดแบบไดนามิกเมื่อมีการส่งข้อผิดพลาด หากจำเป็นต้องใช้ข้อผิดพลาดแบบคงที่ที่นำกลับมาใช้ซ้ำได้ การล้างสแต็กอย่างชัดแจ้ง (EMPTY_KEY_ERROR.stack = '';) จะทำให้เฟรมภายใน V8 ที่บันทึกไว้ว่างทันที

// Recommended: Construct errors dynamically
export function validateKey(key: string) {
  if (!key) {
    throw new Error("Key cannot be empty");
  }
}

// Or clear the stack property if a static instance is necessary:
export const REUSABLE_ERROR = new Error("Static error");
REUSABLE_ERROR.stack = ""; // Releases the captured initialization frame

การปรับขนาด Listener เหตุการณ์ของหน้าต่างส่วนกลาง

Agent พบปัญหาการปรับขนาดหน่วยความจำที่เกี่ยวข้องกับ Listener เหตุการณ์ visibilitychange รายการ แม้ว่านี่จะไม่ใช่ "การรั่วไหล" ที่เข้มงวด (เนื่องจากระบบจะล้างข้อมูลในที่สุดเมื่อคอมโพเนนต์ยกเลิกการต่อเชื่อม) แต่ก็เพิ่มการใช้งานหน่วยความจำเนื่องจากมี Listener 1 รายสำหรับทุกข้อความบนหน้าจอ ภาพรวมแสดงให้เห็น Listener เหตุการณ์ในตัว 1,142 รายการ โดยมี Listener ที่ใช้งานอยู่ 128 รายการซึ่งแนบมากับออบเจ็กต์หน้าต่างส่วนกลางโดยตรง

ปัญหา: ใน JavaScript Callback ของ Listener เหตุการณ์คือ Closure Closure จะเก็บการอ้างอิงไปยังขอบเขตคำศัพท์ที่ล้อมรอบไว้เพื่อให้แน่ใจว่าตัวแปรที่ต้องการจะยังคงเข้าถึงได้ ตัวแทนติดตามผู้ฟัง 126 คนเหล่านี้กลับไปยังองค์ประกอบเดียว นั่นคือ RelativeTimeController การประทับเวลาแต่ละรายการใน UI ของ Chat (เช่น "5 นาทีที่ผ่านมา") จะจัดการตัวควบคุมของตัวเอง ซึ่งลงทะเบียน Listener ระดับการเข้าถึงแบบส่วนตัวเพื่ออัปเดตเวลา เนื่องจาก Listener แต่ละรายการยึดอินสแตนซ์ของตัวควบคุมไว้กับรูทของหน้าต่าง เว็บที่หนาแน่นของ Closure ที่ใช้งานอยู่จึงใช้หน่วยความจำจำนวนมากตลอดอายุการใช้งานของชุดข้อความแชทที่มองเห็นได้

// Ineffective pattern: Attaching global window listeners in individual UI controllers
export class RelativeTimeController {
  constructor() {
    this.visibilityListener = () => this.updateTime();
    window.addEventListener("visibilitychange", this.visibilityListener);
  }

  // If dispose() fails to run or unregister, the closure pins the controller
}

การแก้ไข: ทีมได้รวมศูนย์เหตุการณ์ของหน้าต่างส่วนกลาง ตอนนี้ TimerService ที่แชร์จะลงทะเบียน Listener การมองเห็นได้เพียง 1 รายการในหน้าต่างและประกาศการอัปเดตไปยังตัวควบคุมการประทับเวลาที่ใช้งานอยู่ทั้งหมด ซึ่งทำให้ผู้ฟัง 126 คนลดลงเหลือเพียง 1 คนทันที

// Recommended: Centralized event coordination using a shared service
export class VisibilityService {
  private listeners = new Set<() => void>();

  constructor() {
    // Single global listener throttled for the entire application
    window.addEventListener("visibilitychange", () => this.notifyAll());
  }

  subscribe(callback: () => void): () => void {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback); // Unsubscribe handle
  }
}

SDK ของบุคคลที่สามและตัวยึด iframe แบบฝัง

โหนด DOM ที่ถูกปลดออกคือองค์ประกอบที่ถูกนำออกจากโครงสร้างเอกสาร แต่ยังคงมีการอ้างอิงโดยตัวแปร JavaScript หากตัวแปรรากเก็บองค์ประกอบเดียวในแผนผัง DOM ที่ถูกปลดออกไว้ ระบบจะไม่สามารถเรียกคืนแผนผังที่ถูกปลดออกทั้งหมดได้

ปัญหา: เมื่อผู้ใช้ไปยังส่วนต่างๆ ระหว่างพื้นที่ทำงานของแชท Agent พบว่าแผนผัง DOM ที่แยกออกทั้งหมดยังคงอยู่ในหน่วยความจำ โดยติดตามเส้นทางการเก็บรักษาข้อมูลกลับไปยังมีเดียเพลเยอร์ที่ฝังอยู่ภายใน UI ของพื้นที่ใน Chat เมื่อยกเลิกการต่อเชื่อมพื้นที่ทำงานในแชท ระบบจะนำคอมโพเนนต์ UI ออกจากหน้า แต่ SDK ของมีเดียเพลเยอร์ที่เกี่ยวข้องจะไม่ถูกทำลายอย่างชัดแจ้ง ไลบรารีของบุคคลที่สามเก็บรักษาพจนานุกรมส่วนกลางภายในของรหัส iframe ที่ใช้งานอยู่ ซึ่งทำให้ DOM ที่แยกออกมายังคงทำงานอยู่

วิธีแก้ไข: ตรวจสอบว่า iframe ที่ฝังและวิดเจ็ตของบุคคลที่สามทั้งหมดเชื่อมโยงกับวงจรการกำจัดคอมโพเนนต์ การเรียกใช้เมธอด .destroy() ในโปรแกรมเล่นอย่างชัดเจนจะล้างรายการออกจากรีจิสทรี SDK ส่วนกลางและอนุญาตให้เก็บขยะ DOM ได้

// Recommended: Explicit teardown of embedded frames and SDK instances
export class MediaPlayerController {
  private player: ThirdPartyPlayer | null = null;

  dispose(): void {
    if (this.player) {
      this.player.destroy(); // Clears entry from global SDK registry
      this.player = null;
    }
  }
}

รูปแบบเพิ่มเติม

แม้ว่ารูปแบบทั้ง 3 ที่กล่าวถึงก่อนหน้านี้จะส่งผลกระทบมากที่สุดในแง่ของเมกะไบต์ดิบที่ประหยัดได้ แต่ Agent ยังตรวจพบและแก้ไขการรั่วไหลอื่นๆ อีกกว่า 27 รายการด้วย ซึ่งรวมถึง

  • การวัดและส่งข้อมูลทางไกลของอาร์เรย์รูปภาพ: แผนผัง DOM ที่ถูกปลดออกยังคงทำงานอยู่เนื่องจากอาร์เรย์การวินิจฉัยที่กำหนดเองซึ่งติดตามเมตริกรูปภาพครึ่งหน้าบน
  • ข้อบกพร่องเกี่ยวกับวงจรของคอมโพเนนต์เฟรมเวิร์ก: กรณีที่เมนูแบบเลื่อนลงที่กำหนดเองซึ่งยกขึ้นไปยังเนื้อหาเอกสารข้ามการยกเลิกการต่อเชื่อมขององค์ประกอบหลักเดิม
  • กับดักการปิดออบเจ็กต์อาร์กิวเมนต์โดยนัย: โดยที่การปิดภายในจับอาร์เรย์อาร์กิวเมนต์ทั้งหมดของฟังก์ชันภายนอกโดยไม่ตั้งใจ

ผลกระทบ: การจัดการหน่วยความจำเชิงรุกเป็นเวิร์กโฟลว์

ทีม Google Chat ได้ปรับงานที่มีความเชี่ยวชาญสูงให้เป็นอัตโนมัติด้วยการติดตั้งเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome สำหรับ Agent ให้กับ Agent การเขียนโค้ด การติดตามหน่วยความจำรั่วไหลที่ซับซ้อนลดลงจากการสำรวจฮีปด้วยตนเองหลายวันเหลือเพียงการวิเคราะห์อัตโนมัติไม่กี่นาที

เช่นเดียวกับที่ P2ER ใช้ "การตรวจสอบเชิงประจักษ์ที่จำเป็น" เพื่อบังคับใช้ความเสถียรของ UI ทีมสามารถใช้แนวทางการตรวจสอบหน่วยความจำนี้เป็นมาตรการป้องกันเชิงรุกได้ นักพัฒนาแอปมักจะมองข้ามปัญหาเกี่ยวกับหน่วยความจำ โดยคิดว่าปัญหาเหล่านี้จะเกิดขึ้นเฉพาะในแอปพลิเคชันขนาดใหญ่หรือซับซ้อนเท่านั้น อย่างไรก็ตาม ดังที่กรณีศึกษานี้แสดงให้เห็น รูปแบบที่พบบ่อย เช่น Listener เหตุการณ์มาตรฐานหรือข้อผิดพลาดแบบคงที่อาจสะสมขึ้นอย่างเงียบๆ การผสานรวมเครื่องมือต่างๆ เช่น เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome สำหรับ Agent เข้ากับวงจรการพัฒนาประจำวันจะเปลี่ยน AI จากเครื่องมือสร้างโค้ดพื้นฐานให้เป็นเกตคุณภาพที่เชื่อถือได้ ซึ่งจะตรวจจับการถดถอยแบบไม่แสดงอาการก่อนที่จะเข้าสู่เวอร์ชันที่ใช้งานจริง