นักพัฒนาเว็บคาดว่าจะมีผลกระทบด้านประสิทธิภาพเพียงเล็กน้อยหรือไม่มีเลยเมื่อแก้ไขข้อบกพร่องของโค้ด อย่างไรก็ตาม ความคาดหวังนี้ยังไม่เป็นสากล นักพัฒนาซอฟต์แวร์ C++ ไม่คาดหวังว่ารุ่นการแก้ไขข้อบกพร่องของแอปพลิเคชันของตนจะได้ประสิทธิภาพในเวอร์ชันที่ใช้งานจริง และในช่วงปีแรกๆ ที่ Chrome เปิดตัว แค่เปิด DevTools ก็ส่งผลต่อประสิทธิภาพของหน้าอย่างมาก
เราไม่รู้สึกว่าประสิทธิภาพที่ลดลงนี้เป็นผลมาจากการลงทุนด้านความสามารถในการแก้ไขข้อบกพร่องของ DevTools และ V8 เป็นเวลาหลายปีอีกต่อไป อย่างไรก็ตาม เราไม่สามารถลดภาระการทำงานของเครื่องมือสำหรับนักพัฒนาเว็บให้เป็น 0 ได้ การตั้งค่าเบรกพอยท์ การก้าวผ่านโค้ด การรวบรวมสแต็กเทรซ การบันทึกการติดตามประสิทธิภาพ และอื่นๆ ทั้งหมดจะส่งผลต่อความเร็วในการดำเนินการในระดับที่ต่างกัน ทั้งนี้ก็เพราะการสังเกตบางอย่างจะเปลี่ยนแปลง
แต่แน่นอนว่าค่าใช้จ่ายสำหรับ DevTools อย่างโปรแกรมแก้ไขข้อบกพร่องต่างๆ ก็น่าจะสมเหตุสมผล เมื่อเร็วๆ นี้เราพบว่ามีการรายงานเพิ่มขึ้นอย่างมากซึ่งในบางกรณี เครื่องมือสำหรับนักพัฒนาเว็บจะชะลอแอปพลิเคชันจนไม่สามารถใช้งานได้อีก คุณสามารถดูการเปรียบเทียบเทียบเคียงกันได้จากรายงาน chromium:1069425 ซึ่งจะแสดงประสิทธิภาพของส่วนเกินของการเปิดเครื่องมือสำหรับนักพัฒนาเว็บเท่านั้น
คุณจะเห็นได้จากวิดีโอว่าความเร็วช้าลงตามลำดับที่ 5-10 เท่า ซึ่งเป็นเรื่องที่เรายอมรับไม่ได้ ขั้นตอนแรกคือการทำความเข้าใจจุดที่ต้องปรับปรุงและสาเหตุของชะลอตัวครั้งใหญ่นี้เมื่อเปิดเครื่องมือสำหรับนักพัฒนาเว็บ การใช้ 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) สำหรับการทดสอบ เราสามารถทำทุกอย่างได้ในขั้นตอนเดียวและตรวจสอบค่าพร็อพเพอร์ตี้โดยตรง ทำให้ฟังก์ชันทั้งหมดเร็วขึ้นประมาณ 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()
ตั้งแต่แรก แต่เป็นที่เดียวที่เราใช้ (และแสดง) ชื่อเมธอดใน API สแต็กเทรซ JavaScript เพื่อทำความเข้าใจการใช้งานดังกล่าว ลองดูตัวอย่างง่ายๆ ต่อไปนี้ 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 รายการที่อธิบายไว้ข้างต้น (API ของ v8::StackFrame
สำหรับ C++ และ API สแต็กเทรซ JavaScript) การมี 2 วิธีที่ทำได้เหมือนกัน (คร่าวๆ) มักเกิดข้อผิดพลาดและมักทำให้เกิดความไม่สอดคล้องและข้อบกพร่อง ดังนั้นในช่วงปลายปี 2018 เราจึงเริ่มโปรเจ็กต์ที่ต้องแก้ปัญหาคอขวดเดียวในการจับภาพสแต็กเทรซ
โครงการดังกล่าวประสบความสำเร็จอย่างมากและลดจำนวนปัญหาเกี่ยวกับการรวบรวมสแต็กเทรซลงอย่างมาก ข้อมูลส่วนใหญ่ที่ให้ผ่านพร็อพเพอร์ตี้ error.stack
ที่ไม่เป็นมาตรฐานยังได้รับการคำนวณแบบ Lazy Loading และเมื่อจำเป็นจริงๆ เท่านั้น แต่ในขั้นตอนการเปลี่ยนโครงสร้างวัตถุ เราได้ใช้กลวิธีเดียวกันนี้กับออบเจ็กต์ v8::StackFrame
รายการ ระบบจะคำนวณข้อมูลทั้งหมดเกี่ยวกับสแต็กเฟรมเมื่อมีการเรียกใช้เมธอดเป็นครั้งแรก
โดยทั่วไปแล้วการทำเช่นนี้จะช่วยปรับปรุงประสิทธิภาพ แต่ปรากฏว่ามีความขัดแย้งกับวิธีการใช้ออบเจ็กต์ API ของ C++ เหล่านี้ใน Chromium และเครื่องมือสำหรับนักพัฒนาเว็บ โดยเฉพาะอย่างยิ่ง ตั้งแต่ที่เราได้แนะนำคลาส v8::internal::StackFrameInfo
ใหม่ซึ่งเก็บข้อมูลทั้งหมดเกี่ยวกับสแต็กเฟรมที่แสดงผ่าน v8::StackFrame
หรือผ่าน error.stack
เราจึงจะคำนวณชุดขั้นสูงของข้อมูลที่ได้รับจาก API ทั้งสองเสมอ ซึ่งหมายความว่าสำหรับการใช้งาน v8::StackFrame
(และโดยเฉพาะสำหรับ DevTools) เราจะคำนวณชื่อเมธอดทันทีที่มีการขอข้อมูลเกี่ยวกับสแต็กเฟรม ดูเหมือนว่าเครื่องมือสำหรับนักพัฒนาเว็บจะขอข้อมูลแหล่งที่มาและสคริปต์โดยทันทีเสมอ
จากการรับรู้ดังกล่าว เราสามารถเปลี่ยนโครงสร้างภายในและแปลงการนำเสนอสแต็กเฟรมให้ง่ายขึ้นอย่างมาก และทำให้ทำงานแบบ Lazy Loading ได้มากยิ่งขึ้น ดังนั้นการใช้งานทั่วทั้ง V8 และ Chromium จะจ่ายเฉพาะค่าใช้จ่ายในการประมวลผลข้อมูลที่ขอเท่านั้น วิธีนี้ช่วยเพิ่มประสิทธิภาพให้กับเครื่องมือสำหรับนักพัฒนาเว็บและกรณีการใช้งานอื่นๆ ของ Chromium ได้อย่างมาก ซึ่งต้องการเพียงข้อมูลส่วนเล็กๆ เกี่ยวกับสแต็กเฟรม (โดยเฉพาะอย่างยิ่งชื่อสคริปต์และตำแหน่งแหล่งที่มาในรูปแบบออฟเซ็ตเส้นและคอลัมน์) และเปิดประตูสู่การปรับปรุงประสิทธิภาพให้ดียิ่งขึ้น
ชื่อฟังก์ชัน
จากการปรับโครงสร้างที่กล่าวไปข้างต้น ทำให้มีค่าใช้จ่ายในการแทนที่ด้วยสัญลักษณ์ (เวลาที่ใช้ในเดือนv8_inspector::V8Debugger::symbolize
) ลดลงเหลือประมาณ 15% ของเวลาดำเนินการโดยรวม และเราเห็นได้ชัดเจนขึ้นว่า V8 ใช้เวลาไปกับส่วนใดเมื่อ (รวบรวมและ) เป็นสัญลักษณ์ของสแต็กเฟรมเพื่อการบริโภคในเครื่องมือสำหรับนักพัฒนาเว็บ
สิ่งแรกที่โดดเด่นคือค่าใช้จ่ายสะสมสำหรับหมายเลขบรรทัดและคอลัมน์ ส่วนที่แพงในที่นี้คือการคำนวณออฟเซ็ตอักขระภายในสคริปต์ (อิงตามออฟเซ็ตไบต์โค้ดที่ได้จาก V8) และปรากฏว่าเนื่องจากเราทำการเปลี่ยนโครงสร้างโค้ดข้างต้น 2 ครั้ง คือครั้งหนึ่งคำนวณเลขบรรทัด และอีกครั้งหนึ่งเมื่อคำนวณหมายเลขคอลัมน์ การแคชตำแหน่งแหล่งที่มาในอินสแตนซ์ 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
จะไม่แสดงในโปรไฟล์ใดๆ ที่เราพิจารณาอีกต่อไป
บทสรุป
จากการปรับปรุงข้างต้น เราได้ลดภาระของเครื่องมือสำหรับนักพัฒนาเว็บลงได้อย่างมากในแง่ของสแต็กเทรซ
เราทราบว่ายังคงมีการปรับปรุงที่เป็นไปได้อีกมากมาย ตัวอย่างเช่น ค่าใช้จ่ายในการดำเนินการเมื่อใช้ MutationObserver
ยังคงสังเกตเห็นได้ชัดเจน ดังที่รายงานใน chromium:1077657 แต่ในขณะนี้เราได้แก้ไขปัญหาหลักๆ แล้ว และเราอาจกลับมาปรับปรุงประสิทธิภาพการแก้ไขข้อบกพร่องในอนาคต
ดาวน์โหลดช่องตัวอย่าง
ลองใช้ Chrome Canary, Dev หรือเบต้าเป็นเบราว์เซอร์สำหรับการพัฒนาเริ่มต้น ช่องทางแสดงตัวอย่างเหล่านี้ช่วยให้คุณได้เข้าถึงฟีเจอร์ล่าสุดของเครื่องมือสำหรับนักพัฒนาเว็บ ทดสอบ API แพลตฟอร์มเว็บที่ล้ำสมัย และค้นหาปัญหาในเว็บไซต์ก่อนที่ผู้ใช้จะได้ใช้งาน
ติดต่อทีมเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
ใช้ตัวเลือกต่อไปนี้เพื่อพูดคุยเกี่ยวกับฟีเจอร์ใหม่ๆ และการเปลี่ยนแปลงในโพสต์ หรือเรื่องอื่นๆ ที่เกี่ยวข้องกับเครื่องมือสำหรับนักพัฒนาเว็บ
- ส่งคำแนะนำหรือความคิดเห็นถึงเราผ่าน crbug.com
- รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บโดยใช้ตัวเลือกเพิ่มเติม > ความช่วยเหลือ > รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บในเครื่องมือสำหรับนักพัฒนาเว็บ
- ทวีตที่ @ChromeDevTools
- โปรดแสดงความคิดเห็นในวิดีโอ YouTube เกี่ยวกับมีอะไรใหม่ในเครื่องมือสำหรับนักพัฒนาเว็บในวิดีโอ YouTube หรือเคล็ดลับสำหรับเครื่องมือสำหรับนักพัฒนาเว็บ