นักพัฒนาเว็บอาจคาดหวังว่าจะได้เห็นประสิทธิภาพเพียงเล็กน้อยหรือไม่ให้ผลลัพธ์เลยเมื่อแก้ไขข้อบกพร่องของโค้ด อย่างไรก็ตาม ก็ไม่ได้หมายความว่าทุกคนจะคาดหวังเช่นนั้น นักพัฒนาซอฟต์แวร์ C++ คงคาดไม่ถึงว่าบิลด์แก้ไขข้อบกพร่องของแอปพลิเคชันจะมีประสิทธิภาพเทียบเท่าเวอร์ชันที่ใช้งานจริง และในช่วงปีแรกๆ ของ Chrome การเปิดเครื่องมือสำหรับนักพัฒนาเว็บเพียงอย่างเดียวก็ส่งผลต่อประสิทธิภาพของหน้าเว็บอย่างมาก
การที่ผู้ใช้ไม่รู้สึกถึงประสิทธิภาพที่ลดลงอีกต่อไปเป็นผลมาจากการลงทุนในความสามารถด้านการแก้ไขข้อบกพร่องของ DevTools และ V8 เป็นเวลาหลายปี อย่างไรก็ตาม เราไม่สามารถลดค่าใช้จ่ายเพิ่มเติมด้านประสิทธิภาพของ DevTools ให้เป็น 0 ได้ การตั้งจุดหยุดพัก การสเต็ปผ่านโค้ด การเก็บรวบรวมสแต็กเทรซ การบันทึกการติดตามประสิทธิภาพ ฯลฯ ล้วนส่งผลต่อความเร็วในการเรียกใช้ในระดับที่แตกต่างกัน เพราะอย่างไรก็ดี การเฝ้าดูสิ่งที่เปลี่ยนแปลงสิ่งนั้น
แต่แน่นอนว่าค่าใช้จ่ายสำหรับเครื่องมือสำหรับนักพัฒนาเว็บก็สมเหตุสมผลเช่นเดียวกับโปรแกรมแก้ไขข้อบกพร่องทั่วไป เมื่อเร็วๆ นี้ เราพบว่ามีรายงานจำนวนเพิ่มขึ้นอย่างมากว่าในบางกรณี DevTools จะทําให้แอปพลิเคชันช้าลงจนใช้งานไม่ได้ ด้านล่างนี้คุณจะเห็นการเปรียบเทียบแบบเคียงข้างกันจากรายงาน chromium:1069425 ซึ่งแสดงถึงค่าใช้จ่ายด้านประสิทธิภาพของการแค่เปิดเครื่องมือสำหรับนักพัฒนาเว็บไว้
ดังที่คุณเห็นจากวิดีโอ วิดีโอช้าลงประมาณ 5-10 เท่า ซึ่งยอมรับไม่ได้ ขั้นตอนแรกคือทำความเข้าใจว่าเวลาหายไปไหนและสาเหตุที่ทำให้เกิดความล่าช้าอย่างมากเมื่อเปิด DevTools การใช้ Linux perf ในกระบวนการแสดงผลของ Chrome เผยให้เห็นการแจกแจงเวลาดำเนินการโดยรวมของโปรแกรมแสดงผลดังต่อไปนี้
แม้ว่าเราคาดหวังที่จะเห็นข้อมูลบางอย่างที่เกี่ยวข้องกับการเก็บรวบรวมสแต็กเทรซ แต่ก็ไม่ได้คาดคิดว่าเวลาในการดำเนินการโดยรวมประมาณ 90% จะใช้ไปกับการแสดงสัญลักษณ์เฟรมสแต็ก การแทนที่สัญลักษณ์ในที่นี้หมายถึงการแก้ไขชื่อฟังก์ชันและตำแหน่งแหล่งที่มาที่เฉพาะเจาะจง เช่น หมายเลขบรรทัดและคอลัมน์ในสคริปต์ จากสแต็กเฟรมดิบ
อนุมานชื่อเมธอด
สิ่งที่น่าประหลาดใจยิ่งกว่านั้นคือเวลาเกือบทั้งหมดหมดไปกับฟังก์ชัน JSStackFrame::GetMethodName()
ใน V8 แม้ว่าเราจะทราบจากการตรวจสอบก่อนหน้านี้ว่า JSStackFrame::GetMethodName()
ไม่ใช่คนแปลกหน้าในดินแดนแห่งปัญหาด้านประสิทธิภาพ ฟังก์ชันนี้จะพยายามคํานวณชื่อของเมธอดสําหรับเฟรมที่ถือว่าเป็นการเรียกใช้เมธอด (เฟรมที่แสดงการเรียกใช้ฟังก์ชันของรูปแบบ obj.func()
ไม่ใช่ func()
) การตรวจสอบโค้ดอย่างรวดเร็วพบว่าฟังก์ชันทํางานโดยการเรียกใช้การเรียกใช้ออบเจ็กต์และเชนโปรโตไทป์ทั้งหมด และมองหา
- พร็อพเพอร์ตี้ข้อมูลที่มี
value
เป็นfunc
ปิด หรือ - พร็อพเพอร์ตี้ตัวรับค่าที่
get
หรือset
เท่ากับการปิดfunc
แม้ว่าการอัปเดตนี้จะไม่ทำให้อุปกรณ์ช้าลง แต่ก็ไม่ได้อธิบายความช้าอย่างรุนแรงนี้ เราจึงเริ่มตรวจสอบตัวอย่างที่รายงานใน chromium:1069425 และพบว่ามีการรวบรวมสแต็กเทรซสําหรับงานแบบแอสซิงค์ รวมถึงสําหรับข้อความบันทึกที่มาจาก classes.js
ซึ่งเป็นไฟล์ JavaScript ขนาด 10 MiB เมื่อตรวจสอบอย่างละเอียดแล้ว พบว่านี่เป็นรันไทม์ Java บวกกับโค้ดแอปพลิเคชันที่คอมไพล์เป็น JavaScript บันทึกสแต็กมีเฟรมหลายเฟรมที่มีการเรียกใช้เมธอดในออบเจ็กต์ A
เราจึงคิดว่าควรทำความเข้าใจว่ากำลังจัดการกับออบเจ็กต์ประเภทใด
ดูเหมือนว่าคอมไพเลอร์ Java เป็น JavaScript จะสร้างออบเจ็กต์เดียวที่มีฟังก์ชัน 82,203 รายการ ซึ่งนี่เป็นเรื่องที่น่าสนมาก จากนั้นเรากลับไปที่ JSStackFrame::GetMethodName()
ของ V8 เพื่อดูว่ามีอะไรที่เราทำได้ง่ายๆ ไหม
- โดยวิธีทํางานคือจะค้นหา
"name"
ของฟังก์ชันเป็นพร็อพเพอร์ตี้ในออบเจ็กต์ก่อน หากพบ ก็จะตรวจสอบว่าค่าพร็อพเพอร์ตี้ตรงกับฟังก์ชันหรือไม่ - หากฟังก์ชันไม่มีชื่อหรือออบเจ็กต์ไม่มีพร็อพเพอร์ตี้ที่ตรงกัน ฟังก์ชันจะกลับไปใช้การค้นหาแบบย้อนกลับโดยข้ามผ่านพร็อพเพอร์ตี้ทั้งหมดของออบเจ็กต์และต้นแบบของออบเจ็กต์
ในตัวอย่างนี้ ฟังก์ชันทั้งหมดเป็นแบบไม่ระบุชื่อและมีพร็อพเพอร์ตี้ "name"
ว่าง
A.SDV = function() {
// ...
};
ผลการค้นหาแรกคือ การค้นหาย้อนกลับแบ่งออกเป็น 2 ขั้นตอน (ดำเนินการสำหรับออบเจ็กต์เองและแต่ละออบเจ็กต์ในเชนโปรโตไทป์) ดังนี้
- ดึงข้อมูลชื่อของพร็อพเพอร์ตี้ที่นับทั้งหมดได้ทั้งหมด และ
- ทำการค้นหาพร็อพเพอร์ตี้ทั่วไปสำหรับแต่ละชื่อ ทดสอบว่าค่าพร็อพเพอร์ตี้ที่ได้ตรงกับการปิดที่เรากำลังมองหาหรือไม่
ดูเหมือนว่าจะเป็นวิธีที่ง่ายที่สุดแล้ว เนื่องจากการดึงข้อมูลชื่อจะต้องเรียกใช้พร็อพเพอร์ตี้ทั้งหมดอยู่แล้ว แทนที่จะทำ 2 รอบ ซึ่งก็คือ O(N) สำหรับการดึงข้อมูลชื่อและ O(N log(N)) สำหรับการทดสอบ เราสามารถทำทุกอย่างในรอบเดียวและตรวจสอบค่าพร็อพเพอร์ตี้ได้โดยตรง ซึ่งทำให้ฟังก์ชันทั้งหมดทำงานได้เร็วขึ้นประมาณ 2-10 เท่า
ข้อมูลการค้นพบที่ 2 น่าสนใจยิ่งขึ้น แม้ว่าในทางเทคนิคแล้วฟังก์ชันเหล่านี้จะเป็นฟังก์ชันนิรนาม แต่เครื่องยนต์ V8 ก็ได้บันทึกสิ่งที่เราเรียกว่าชื่อที่อิงตามข้อมูลที่มีอยู่ไว้ให้ฟังก์ชันเหล่านั้น สําหรับลิเทอรัลฟังก์ชันที่ปรากฏทางด้านขวามือของการกำหนดในรูปแบบ obj.foo = function() {...}
โปรแกรมแยกวิเคราะห์ V8 จะจดจํา "obj.foo"
เป็นชื่อที่อิงตามบริบทสําหรับลิเทอรัลฟังก์ชัน ในกรณีของเรา หมายความว่าแม้ว่าจะไม่มีชื่อที่เหมาะสมที่จะค้นหาได้ แต่เราก็มีชื่อที่ใกล้เคียงพอ ในตัวอย่างนี้ A.SDV = function() {...}
เรามี "A.SDV"
เป็นชื่อที่อนุมาน และสามารถดึงชื่อพร็อพเพอร์ตี้จากชื่อที่อนุมานได้โดยมองหาจุดสุดท้าย แล้วค้นหาพร็อพเพอร์ตี้ "SDV"
ในออบเจ็กต์ วิธีนี้ได้ผลเกือบทุกกรณี โดยแทนที่การเรียกดูทั้งหมดที่สิ้นเปลืองทรัพยากรด้วยการค้นหาพร็อพเพอร์ตี้รายการเดียว การปรับปรุงทั้ง 2 รายการข้างต้นเป็นส่วนหนึ่งของ CL นี้ และช่วยลดการชะลอตัวได้อย่างมากสำหรับตัวอย่างที่รายงานใน chromium:1069425
Error.stack
เราอาจจบการแชทกันตรงนี้ แต่มีบางอย่างผิดปกติ เนื่องจากเครื่องมือสำหรับนักพัฒนาเว็บไม่เคยใช้ชื่อเมธอดสำหรับเฟรมสแต็ก อันที่จริง คลาส v8::StackFrame
ใน C++ API ยังไม่ได้เปิดเผยวิธีการเข้าถึงชื่อเมธอด ดูเหมือนเราจะเข้าใจผิดว่าจะต้องโทรหา JSStackFrame::GetMethodName()
ก่อน แต่เราจะใช้ (และแสดง) ชื่อเมธอดใน JavaScript Stack Trace API เท่านั้น ลองดูตัวอย่างง่ายๆ ต่อไปนี้ error-methodname.js
เพื่อทําความเข้าใจการใช้งานนี้
function foo() {
console.log((new Error).stack);
}
var object = {bar: foo};
object.bar();
ที่นี่เรามีฟังก์ชัน foo
ที่ติดตั้งภายใต้ชื่อ "bar"
ใน object
การเรียกใช้ข้อมูลโค้ดนี้ใน Chromium จะให้เอาต์พุตต่อไปนี้
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
ในที่นี้ เราจะเห็นการค้นหาชื่อเมธอด: เฟรมสแต็กด้านบนสุดแสดงขึ้นเพื่อเรียกใช้ฟังก์ชัน foo
ในอินสแตนซ์ของ Object
ผ่านเมธอดชื่อ bar
ดังนั้นพร็อพเพอร์ตี้ error.stack
ที่ไม่เป็นไปตามมาตรฐานจึงใช้ JSStackFrame::GetMethodName()
เป็นจำนวนมาก และจากการทดสอบประสิทธิภาพพบว่าการเปลี่ยนแปลงของเราทําให้ทุกอย่างเร็วขึ้นอย่างมาก
มาพูดถึงเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome กันต่อดีกว่า ข้อเท็จจริงที่ว่าชื่อเมธอดมีการคำนวณอยู่แล้วแม้ว่าจะไม่ได้ใช้ error.stack
ที่ดูไม่ถูกต้อง ประวัติบางส่วนที่เป็นประโยชน์มีดังนี้ เดิมที V8 มีกลไก 2 กลไกที่แตกต่างกันในการรวบรวมและแสดงสแต็กเทรซสําหรับ API 2 รายการที่อธิบายไว้ข้างต้น (v8::StackFrame
API ของ C++ และ API สแต็กเทรซของ JavaScript) การมี 2 วิธีที่แตกต่างกัน (โดยประมาณ) คือข้อผิดพลาดได้ง่ายและมักนำไปสู่ความไม่สอดคล้องกันและข้อบกพร่อง ด้วยเหตุนี้ในช่วงปลายปี 2018 เราจึงเริ่มโปรเจ็กต์เพื่อแก้ไขจุดคอขวดเพียงจุดเดียวสำหรับการบันทึกสแต็กเทรซ
โปรเจ็กต์ดังกล่าวประสบความสำเร็จอย่างมากและลดจำนวนปัญหาที่เกี่ยวข้องกับการรวบรวมข้อมูลสแต็กเทรซลดลงอย่างมาก ข้อมูลส่วนใหญ่ที่ให้ไว้ผ่านพร็อพเพอร์ตี้ error.stack
ที่ไม่เป็นไปตามมาตรฐานได้รับการคำนวณอย่างช้าๆ และเมื่อจำเป็นเท่านั้นด้วย แต่เราใช้เคล็ดลับเดียวกันนี้กับวัตถุ v8::StackFrame
ซึ่งเป็นส่วนหนึ่งของการเปลี่ยนโครงสร้างภายในโค้ด ระบบจะคํานวณข้อมูลทั้งหมดเกี่ยวกับเฟรมสแต็กเมื่อมีการเรียกใช้เมธอดใดก็ตามในเฟรมนั้นเป็นครั้งแรก
โดยทั่วไปแล้วการดำเนินการนี้จะปรับปรุงประสิทธิภาพ แต่น่าเสียดายที่การดำเนินการนี้กลับขัดแย้งกับวิธีใช้ออบเจ็กต์ C++ API เหล่านี้ใน Chromium และเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ โดยเฉพาะตั้งแต่ที่เราได้แนะนำคลาส v8::internal::StackFrameInfo
ใหม่ซึ่งมีข้อมูลทั้งหมดเกี่ยวกับสแตกเฟรมที่แสดงผ่าน v8::StackFrame
หรือผ่าน error.stack
เราจะคำนวณชุดข้อมูลขั้นสูงจาก API ทั้ง 2 ชุดเสมอ ซึ่งหมายความว่าสำหรับการใช้งาน v8::StackFrame
(โดยเฉพาะอย่างยิ่งสำหรับเครื่องมือสำหรับนักพัฒนาเว็บ) เราจะคำนวณชื่อเมธอดทันทีที่มีการขอข้อมูลเกี่ยวกับสแต็กเฟรม ปรากฏว่าเครื่องมือสำหรับนักพัฒนาเว็บจะขอข้อมูลแหล่งที่มาและสคริปต์ทันทีเสมอ
จากการรับรู้ดังกล่าว เราสามารถเปลี่ยนโครงสร้างภายในโค้ดและทําให้การแสดงสแต็กเฟรมง่ายขึ้นอย่างมาก และทําให้มีความขี้เล่นมากขึ้น การใช้งานใน V8 และ Chromium จึงจ่ายเพียงต้นทุนสําหรับการประมวลผลข้อมูลที่ขอเท่านั้น ซึ่งช่วยเพิ่มประสิทธิภาพให้เครื่องมือสำหรับนักพัฒนาเว็บและกรณีการใช้งานอื่นๆ ของ Chromium ได้อย่างมาก โดยต้องใช้ข้อมูลเกี่ยวกับสแต็กเฟรมเพียงส่วนน้อยเท่านั้น (โดยพื้นฐานคือชื่อสคริปต์และตำแหน่งต้นทางในรูปแบบของออฟเซ็ตเส้นและคอลัมน์) และเปิดประตูสำหรับการปรับปรุงประสิทธิภาพเพิ่มเติม
ชื่อฟังก์ชัน
เนื่องจากการเปลี่ยนโครงสร้างภายในโค้ดที่กล่าวไปแล้วข้างต้นทำให้ค่าใช้จ่ายในการแปลงรูปแบบเป็นสัญลักษณ์ (เวลาที่ใช้ใน v8_inspector::V8Debugger::symbolize
) ลดลงเหลือประมาณ 15% ของเวลาดำเนินการโดยรวม และเราเห็นได้ชัดเจนขึ้นว่า V8 ใช้เวลาส่วนใดขณะ (เก็บรวบรวมและ) แสดงสัญลักษณ์ของสแต็กเฟรมเพื่อนำไปใช้ในเครื่องมือสำหรับนักพัฒนาเว็บ
สิ่งแรกที่โดดเด่นคือต้นทุนสะสมสําหรับการคํานวณจํานวนแถวและคอลัมน์ ส่วนที่เป็นค่าใช้จ่ายสูงจริงๆ คือการคำนวณออฟเซตของตัวละครภายในสคริปต์ (อิงตามออฟเซตของไบต์โค้ดที่เราได้รับจาก V8) และปรากฏว่าเนื่องจากการรีแฟกทอริงข้างต้น เราทําการคำนวณนี้ 2 ครั้ง 1 ครั้งเมื่อคํานวณหมายเลขบรรทัด และอีก 1 ครั้งเมื่อคํานวณหมายเลขคอลัมน์ การแคชตําแหน่งแหล่งที่มาในอินสแตนซ์ v8::internal::StackFrameInfo
ช่วยแก้ปัญหานี้ได้อย่างรวดเร็วและนํา v8::internal::StackFrameInfo::GetColumnNumber
ออกจากโปรไฟล์ทั้งหมด
สิ่งที่น่าสนใจกว่าคือ v8::StackFrame::GetFunctionName
สูงอย่างน่าประหลาดใจในโปรไฟล์ทั้งหมดที่เราดู เมื่อตรวจสอบเพิ่มเติม เราพบว่าการคำนวณชื่อที่จะแสดงสำหรับฟังก์ชันในสแต็กเฟรมในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์นั้นไม่จำเป็น
- ก่อนอื่นให้มองหาพร็อพเพอร์ตี้
"displayName"
ที่ไม่เป็นไปตามมาตรฐาน และหากพร็อพเพอร์ตี้ดังกล่าวให้พร็อพเพอร์ตี้ข้อมูลที่มีค่าสตริง เราจะใช้พร็อพเพอร์ตี้นั้น - ไม่เช่นนั้นให้กลับไปค้นหาพร็อพเพอร์ตี้
"name"
มาตรฐาน แล้วตรวจสอบอีกครั้งว่าพร็อพเพอร์ตี้ดังกล่าวให้พร็อพเพอร์ตี้ข้อมูลที่มีค่าเป็นสตริงหรือไม่ - และสุดท้ายจะกลับไปใช้ชื่อการแก้ไขข้อบกพร่องภายในที่แยกแยะโดยโปรแกรมแยกวิเคราะห์ V8 และจัดเก็บไว้ในนิพจน์ฟังก์ชัน
เราได้เพิ่มพร็อพเพอร์ตี้ "displayName"
ไว้เป็นวิธีแก้ปัญหาสำหรับพร็อพเพอร์ตี้ "name"
ในอินสแตนซ์ Function
ที่เป็นแบบอ่านอย่างเดียวและไม่สามารถกําหนดค่าได้ใน JavaScript แต่ไม่เคยได้มาตรฐานและไม่ได้ใช้งานอย่างแพร่หลาย เนื่องจากเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์เบราว์เซอร์ได้เพิ่มการอนุมานชื่อฟังก์ชันที่ทํางานได้ 99.9% ของกรณี นอกจากนี้ ES2015 ยังทำให้พร็อพเพอร์ตี้ "name"
ในอินสแตนซ์ Function
กำหนดค่าได้ จึงไม่จำเป็นต้องใช้พร็อพเพอร์ตี้ "displayName"
พิเศษอีกต่อไป เนื่องจากการค้นหา "displayName"
เชิงลบนั้นค่อนข้างมีค่าใช้จ่ายสูงและไม่จำเป็นจริงๆ (ES2015 เปิดตัวมานานกว่า 5 ปีแล้ว) เราจึงตัดสินใจนำการรองรับพร็อพเพอร์ตี้ fn.displayName
ที่ไม่เป็นไปตามมาตรฐานออกจาก V8 (และเครื่องมือสำหรับนักพัฒนาเว็บ)
เมื่อการค้นหาเชิงลบของ "displayName"
เสร็จสิ้นแล้ว ระบบได้นำต้นทุนของ v8::StackFrame::GetFunctionName
ออกครึ่งหนึ่ง อีกครึ่งหนึ่งจะเป็นการค้นหาพร็อพเพอร์ตี้ทั่วไปของ "name"
โชคดีที่เรามีตรรกะที่ใช้อยู่แล้วเพื่อหลีกเลี่ยงการค้นหาราคาสูงของพร็อพเพอร์ตี้ "name"
ในอินสแตนซ์ Function
(ไม่ได้แตะ) ซึ่งเปิดตัวไปใน V8 ก่อนหน้านี้แล้วเพื่อให้ Function.prototype.bind()
ทำงานได้เร็วขึ้น เราได้พอร์ตการตรวจสอบที่จําเป็น ซึ่งช่วยให้เราข้ามการค้นหาทั่วไปที่มีค่าใช้จ่ายสูงตั้งแต่แรกได้ ผลที่ได้คือ v8::StackFrame::GetFunctionName
ไม่แสดงในโปรไฟล์ใดๆ ที่เราพิจารณาอีกต่อไป
บทสรุป
การปรับปรุงข้างต้นช่วยลดค่าใช้จ่ายเพิ่มเติมของ DevTools ในแง่ของสแต็กเทรซได้อย่างมาก
เราทราบดีว่ายังมีการปรับปรุงที่เป็นไปได้อีกมากมาย เช่น ค่าใช้จ่ายเพิ่มเติมเมื่อใช้ MutationObserver
ยังคงสังเกตได้ดังที่รายงานใน chromium:1077657 แต่ตอนนี้เราได้แก้ไขปัญหาหลักๆ แล้ว และอาจกลับมาดำเนินการเพิ่มเติมในอนาคตเพื่อปรับปรุงประสิทธิภาพการแก้ไขข้อบกพร่องให้ดียิ่งขึ้น
ดาวน์โหลดแชแนลตัวอย่าง
ลองใช้ Chrome Canary, Dev หรือเบต้าเป็นเบราว์เซอร์เริ่มต้นสำหรับการพัฒนา ช่องทางเวอร์ชันตัวอย่างเหล่านี้จะช่วยให้คุณเข้าถึงฟีเจอร์ล่าสุดของ DevTools, ทดสอบ API ของแพลตฟอร์มเว็บที่ล้ำสมัย และช่วยคุณค้นหาปัญหาในเว็บไซต์ได้ก่อนที่ผู้ใช้จะพบ
ติดต่อทีม Chrome DevTools
ใช้ตัวเลือกต่อไปนี้เพื่อพูดคุยเกี่ยวกับฟีเจอร์ใหม่ การอัปเดต หรือสิ่งอื่นๆ ที่เกี่ยวข้องกับเครื่องมือสำหรับนักพัฒนาเว็บ
- ส่งความคิดเห็นและคำขอฟีเจอร์ถึงเราได้ที่ crbug.com
- รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บโดยใช้ตัวเลือกเพิ่มเติม > ความช่วยเหลือ > รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บในเครื่องมือสำหรับนักพัฒนาเว็บ
- ทวีตถึง @ChromeDevTools
- แสดงความคิดเห็นในวิดีโอ YouTube เกี่ยวกับข่าวสารใน DevTools หรือวิดีโอ YouTube เกี่ยวกับเคล็ดลับใน DevTools