เผยแพร่เมื่อวันที่ 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 ที่เฉพาะเจาะจง ดังนี้
- บันทึกหรือโหลดสแนปชอต: ใช้เครื่องมืออย่าง
take_heapsnapshotหรืออ่านไฟล์ที่มีอยู่ - วิเคราะห์โครงสร้างฮีป: สแกนหาแหล่งที่มาของการรั่วไหลที่พบบ่อยโดยใช้
get_heapsnapshot_detailsและcompare_heapsnapshots - ติดตามเส้นทางการเก็บรักษา: ใช้
get_heapsnapshot_retaining_pathsเพื่อย้อนกลับ Dominator Tree ไปยังรูทของ Garbage Collection - สร้างการแก้ไข: เครื่องมือจะตรวจสอบเพื่อเปรียบเทียบเส้นทางการเก็บรักษาข้อมูลกับซอร์สโค้ดของแอปพลิเคชันเพื่อสร้างแพตช์ที่เฉพาะเจาะจงอย่างมาก
ข้อมูลเชิงลึกทางเทคนิค: รูปแบบหน่วยความจำรั่วไหลที่พบบ่อย 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 จากเครื่องมือสร้างโค้ดพื้นฐานให้เป็นเกตคุณภาพที่เชื่อถือได้ ซึ่งจะตรวจจับการถดถอยแบบไม่แสดงอาการก่อนที่จะเข้าสู่เวอร์ชันที่ใช้งานจริง