ผมชื่อ Ian Kilpatrick เป็นหัวหน้าทีมวิศวกรของทีมเลย์เอาต์ Blink ร่วมกับ Koji Ishii ก่อนที่จะมาทำงานในทีม Blink ฉันเป็นวิศวกรฝั่งหน้าเว็บ (ก่อนที่ Google จะมีบทบาท "วิศวกรฝั่งหน้าเว็บ") เพื่อสร้างฟีเจอร์ต่างๆ ใน Google เอกสาร, ไดรฟ์ และ Gmail หลังจากทำงานในตำแหน่งดังกล่าวประมาณ 5 ปี ฉันได้เสี่ยงที่จะเปลี่ยนไปทำงานกับทีม Blink เพื่อเรียนรู้ C++ ในการทำงานอย่างมีประสิทธิภาพ และพยายามเพิ่มประสิทธิภาพโค้ดเบสของ Blink ที่ยุ่งยากอย่างมาก ทุกวันนี้ เรายังเข้าใจภาษานี้เพียงส่วนน้อยเท่านั้น ขอขอบคุณที่สละเวลาให้เราในระยะเวลานี้ ฉันรู้สึกสบายใจเมื่อทราบว่ามี "วิศวกรฝั่งหน้าเว็บที่กลับมาทำงานอีกครั้ง" จำนวนมากได้เปลี่ยนไปเป็น "วิศวกรเบราว์เซอร์" มาก่อนฉัน
ประสบการณ์ที่ผ่านมาเป็นแนวทางให้ผมในขณะที่อยู่ในทีม Blink ในฐานะวิศวกรฝั่งหน้าเว็บ ฉันพบปัญหาความไม่สอดคล้องของเบราว์เซอร์ ปัญหาด้านประสิทธิภาพ ข้อบกพร่องในการเรนเดอร์ และฟีเจอร์ที่ขาดหายไปอยู่เสมอ LayoutNG เป็นโอกาสที่ฉันได้ช่วยแก้ไขปัญหาเหล่านี้อย่างเป็นระบบภายในระบบเลย์เอาต์ของ Blink และเป็นผลงานรวมของความพยายามของวิศวกรหลายคนตลอดหลายปีที่ผ่านมา
ในโพสต์นี้ เราจะอธิบายว่าการเปลี่ยนแปลงสถาปัตยกรรมครั้งใหญ่เช่นนี้จะช่วยลดและบรรเทาข้อบกพร่องและปัญหาด้านประสิทธิภาพประเภทต่างๆ ได้อย่างไร
ภาพรวมระดับ 30,000 ฟุตของสถาปัตยกรรมเครื่องมือจัดวาง
ก่อนหน้านี้ ต้นไม้เลย์เอาต์ของ Blink เป็นสิ่งที่เราเรียกว่า "ต้นไม้ที่เปลี่ยนแปลงได้"
ออบเจ็กต์แต่ละรายการในลําดับชั้นเลย์เอาต์จะมีข้อมูลอินพุต เช่น ขนาดที่ใช้ได้ซึ่งกำหนดโดยองค์ประกอบหลัก ตำแหน่งขององค์ประกอบลอย และข้อมูลเอาต์พุต เช่น ความกว้างและความสูงสุดท้ายของออบเจ็กต์ หรือตำแหน่ง x และ y ของออบเจ็กต์
ระบบจะเก็บออบเจ็กต์เหล่านี้ไว้ระหว่างการเรนเดอร์ เมื่อเกิดการเปลี่ยนแปลงสไตล์ขึ้น เราได้ทําเครื่องหมายออบเจ็กต์นั้นว่า "ไม่สะอาด" และทําเครื่องหมายออบเจ็กต์หลักทั้งหมดในแผนภูมิด้วย เมื่อเรียกใช้ระยะการวางเลย์เอาต์ของไปป์ไลน์การแสดงผลแล้ว เราจะล้างต้นไม้ เรียกใช้วัตถุที่ไม่เป็นระเบียบ จากนั้นเรียกใช้เลย์เอาต์เพื่อให้อยู่ในสถานะที่สะอาด
เราพบว่าสถาปัตยกรรมนี้ทำให้เกิดปัญหาหลายประเภท ซึ่งเราจะอธิบายด้านล่าง แต่ก่อนอื่น เรามาทบทวนข้อมูลเข้าและข้อมูลออกของเลย์เอาต์กัน
การแสดงผลเลย์เอาต์บนโหนดในต้นไม้นี้ใช้ "สไตล์และ DOM" เป็นหลัก และข้อจำกัดหลักจากระบบเลย์เอาต์หลัก (ตารางกริด บล็อก หรือ Flex) เรียกใช้อัลกอริทึมข้อจำกัดเลย์เอาต์ และแสดงผลลัพธ์
สถาปัตยกรรมใหม่ของเราทำให้โมเดลแนวคิดนี้เป็นรูปธรรม เรายังคงมีลําดับชั้นเลย์เอาต์ แต่ใช้เพื่อเก็บข้อมูลอินพุตและเอาต์พุตของเลย์เอาต์เป็นหลัก สำหรับเอาต์พุต เราจะสร้างออบเจ็กต์ใหม่ทั้งหมดที่แก้ไขไม่ได้ ซึ่งเรียกว่าต้นไม้ข้อมูลโค้ด
เราได้อธิบายต้นไม้ Fragment แบบคงที่ก่อนหน้านี้ โดยอธิบายถึงวิธีออกแบบให้นำต้นไม้ส่วนใหญ่ก่อนหน้ามาใช้ซ้ำสำหรับเลย์เอาต์ที่เพิ่มขึ้น
นอกจากนี้ เรายังจัดเก็บออบเจ็กต์ข้อจำกัดหลักที่สร้างข้อมูลโค้ดนั้นด้วย เราใช้ข้อมูลนี้เป็นคีย์แคช ซึ่งจะอธิบายเพิ่มเติมด้านล่าง
นอกจากนี้ เรายังเขียนอัลกอริทึมเลย์เอาต์แบบแทรกในบรรทัด (ข้อความ) ใหม่ให้ตรงกับสถาปัตยกรรมแบบคงที่ใหม่ด้วย ไม่เพียงแต่จะสร้างการแสดงรายการแบบแบนที่ไม่เปลี่ยนแปลงสำหรับเลย์เอาต์ในบรรทัดเท่านั้น แต่ยังมีการแคชระดับย่อหน้าเพื่อให้จัดเรียงใหม่ได้เร็วขึ้น การกำหนดรูปร่างต่อย่อหน้าเพื่อใช้ฟีเจอร์แบบอักษรในองค์ประกอบและคำต่างๆ อัลกอริทึมแบบ 2 ทิศทาง Unicode ใหม่ที่ใช้ ICU การแก้ไขความถูกต้องจำนวนมาก และอื่นๆ
ประเภทข้อบกพร่องของเลย์เอาต์
โดยรวมแล้วข้อบกพร่องของเลย์เอาต์จะแบ่งออกเป็น 4 หมวดหมู่ที่แตกต่างกัน แต่ละหมวดหมู่มีสาเหตุที่ต่างกัน
ความถูกต้อง
เมื่อพูดถึงข้อบกพร่องในระบบการแสดงผล โดยทั่วไปเรามักจะพูดถึงความถูกต้อง เช่น "เบราว์เซอร์ ก มีการทำงานแบบ ก ส่วนเบราว์เซอร์ ข มีการทำงานแบบ ข" หรือ "ทั้งเบราว์เซอร์ ก และ ข ใช้งานไม่ได้" ก่อนหน้านี้เราใช้เวลาไปกับเรื่องนี้มาก และในกระบวนการนี้ เราต่อสู้กับระบบอยู่ตลอดเวลา รูปแบบความล้มเหลวที่พบบ่อยคือการใช้การแก้ไขที่มุ่งเน้นข้อบกพร่องข้อหนึ่งโดยเฉพาะ แต่หลังจากผ่านไปหลายสัปดาห์ เราพบว่าเราทําให้ระบบส่วนอื่น (ดูเหมือนว่าจะไม่เกี่ยวข้อง) กลับไปอยู่ในสถานะเดิม
ดังที่อธิบายไว้ในโพสต์ก่อนหน้า ปัญหานี้บ่งชี้ว่าระบบมีความเปราะบางมาก สำหรับเลย์เอาต์โดยเฉพาะ เราไม่มีสัญญาที่ชัดเจนระหว่างคลาสต่างๆ ซึ่งทำให้วิศวกรเบราว์เซอร์ต้องอาศัยสถานะที่ไม่ควรใช้ หรือตีความค่าบางอย่างจากส่วนอื่นของระบบผิด
ตัวอย่างเช่น ในช่วงหนึ่ง เราพบข้อบกพร่องประมาณ 10 ข้อที่เกี่ยวข้องกับเลย์เอาต์ Flex ในช่วงระยะเวลากว่า 1 ปี การแก้ไขแต่ละครั้งทำให้เกิดปัญหาความถูกต้องหรือประสิทธิภาพในส่วนหนึ่งของระบบ ซึ่งนำไปสู่ข้อบกพร่องอีกรายการหนึ่ง
เมื่อ LayoutNG กำหนดสัญญาระหว่างคอมโพเนนต์ทั้งหมดในระบบเลย์เอาต์อย่างชัดเจนแล้ว เราพบว่าเราสามารถใช้การเปลี่ยนแปลงได้อย่างมั่นใจมากขึ้น นอกจากนี้ เรายังได้รับประโยชน์อย่างมากจากโปรเจ็กต์การทดสอบแพลตฟอร์มเว็บ (WPT) ที่ยอดเยี่ยม ซึ่งช่วยให้หลายฝ่ายมีส่วนร่วมในชุดทดสอบเว็บทั่วไป
ปัจจุบันเราพบว่าหากเผยแพร่การถดถอยจริงในช่องที่เสถียร โดยทั่วไปการถดถอยดังกล่าวจะไม่มีการทดสอบที่เกี่ยวข้องในที่เก็บ WPT และไม่ได้เกิดจากความเข้าใจผิดเกี่ยวกับสัญญาคอมโพเนนต์ นอกจากนี้ เรายังเพิ่มการทดสอบ WPT ใหม่เสมอตามนโยบายการแก้ไขข้อบกพร่องของเรา ซึ่งช่วยให้มั่นใจว่าจะไม่มีเบราว์เซอร์ใดทำผิดพลาดแบบเดิมอีก
ใช้งานไม่ได้
หากคุณเคยพบข้อบกพร่องลึกลับที่การปรับขนาดหน้าต่างเบราว์เซอร์หรือการสลับพร็อพเพอร์ตี้ CSS ทำให้ข้อบกพร่องหายไปอย่างน่าอัศจรรย์ แสดงว่าคุณพบปัญหาการลบล้างไม่เพียงพอ ส่วนหนึ่งของต้นไม้ที่เปลี่ยนแปลงได้ถือว่าสะอาดแล้ว แต่เนื่องจากการเปลี่ยนแปลงบางอย่างในข้อจำกัดของรายการหลัก ต้นไม้จึงไม่ได้แสดงผลลัพธ์ที่ถูกต้อง
ปัญหานี้พบได้บ่อยมากในโหมดเลย์เอาต์แบบ 2 ผ่าน (การเรียกใช้ต้นไม้เลย์เอาต์ 2 ครั้งเพื่อกำหนดสถานะเลย์เอาต์สุดท้าย) ที่อธิบายไว้ด้านล่าง ก่อนหน้านี้โค้ดของเรามีลักษณะดังนี้
if (/* some very complicated statement */) {
child->ForceLayout();
}
การแก้ไขข้อบกพร่องประเภทนี้มักจะมีลักษณะดังนี้
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
โดยทั่วไป การแก้ไขปัญหาประเภทนี้จะทำให้ประสิทธิภาพลดลงอย่างมาก (ดูการลบล้างมากเกินไปด้านล่าง) และแก้ไขได้ยากมาก
ปัจจุบัน (ตามที่อธิบายไว้ข้างต้น) เรามีออบเจ็กต์ข้อจำกัดของรายการหลักแบบคงที่ ซึ่งอธิบายอินพุตทั้งหมดจากเลย์เอาต์หลักไปยังรายการย่อย เราจะจัดเก็บข้อมูลนี้ไว้กับข้อมูลโค้ดที่แก้ไขไม่ได้ ด้วยเหตุนี้ เราจึงมีศูนย์กลางที่diffอินพุต 2 รายการนี้เพื่อพิจารณาว่าเด็กจำเป็นต้องทำผ่านการจัดวางอีกครั้งหรือไม่ ตรรกะการเปรียบเทียบนี้มีความซับซ้อน แต่ควบคุมได้ดี การแก้ไขข้อบกพร่องของปัญหาการลบล้างไม่เพียงพอประเภทนี้มักจะส่งผลให้ต้องตรวจสอบอินพุต 2 รายการด้วยตนเอง และตัดสินใจว่าอินพุตใดมีการเปลี่ยนแปลงจึงจำเป็นต้องใช้การวางเลย์เอาต์อีกครั้ง
การแก้ไขโค้ดการเปรียบเทียบนี้มักจะทำได้ง่าย และทดสอบหน่วยได้ง่ายๆ เนื่องจากการสร้างออบเจ็กต์อิสระเหล่านี้นั้นง่าย
โค้ดการเปรียบเทียบสำหรับตัวอย่างข้างต้นคือ
if (width.IsPercent()) {
if (old_constraints.WidthPercentageSize()
!= new_constraints.WidthPercentageSize())
return kNeedsLayout;
}
if (height.IsPercent()) {
if (old_constraints.HeightPercentageSize()
!= new_constraints.HeightPercentageSize())
return kNeedsLayout;
}
ฮิสเทรีซิส
ข้อบกพร่องประเภทนี้คล้ายกับ "การลบล้างไม่เพียงพอ" โดยพื้นฐานแล้ว ในระบบก่อนหน้านี้ การตรวจสอบว่าเลย์เอาต์เป็นแบบ idempotent นั้นทำได้ยากมาก ซึ่งก็คือ การดำเนินการเลย์เอาต์อีกครั้งโดยใช้อินพุตเดียวกันจะให้ผลลัพธ์เดียวกัน
ในตัวอย่างด้านล่าง เราเพียงแค่สลับคุณสมบัติ CSS ระหว่าง 2 ค่า แต่วิธีนี้จะทำให้สี่เหลี่ยมผืนผ้า "ขยายขนาดอย่างไม่จำกัด"
ก่อนหน้านี้ ต้นไม้แบบปรับเปลี่ยนได้ของเราทำให้เกิดความผิดพลาดเช่นนี้ได้ง่ายมาก หากโค้ดอ่านขนาดหรือตําแหน่งของวัตถุผิดเวลาหรือผิดระยะ (เนื่องจากไม่ได้ "ล้าง" ขนาดหรือตําแหน่งก่อนหน้า เช่น) เราจะเพิ่มข้อบกพร่องฮีสเตรีซิสเล็กน้อยทันที โดยปกติแล้วข้อบกพร่องเหล่านี้จะไม่ปรากฏในการทดสอบ เนื่องจากการทดสอบส่วนใหญ่มุ่งเน้นที่เลย์เอาต์และการแสดงผลแบบเดียว สิ่งที่น่ากังวลยิ่งกว่านั้นคือ เราทราบดีว่าจำเป็นต้องใช้ฮีสเตรีซิสบางส่วนเพื่อให้โหมดเลย์เอาต์บางโหมดทำงานได้อย่างถูกต้อง เราพบข้อบกพร่องเมื่อทำการเพิ่มประสิทธิภาพเพื่อนำการผ่านเลย์เอาต์ออก แต่กลับทำให้เกิด "ข้อบกพร่อง" เนื่องจากโหมดเลย์เอาต์ต้องใช้ 2 ผ่านเพื่อให้ได้ผลลัพธ์ที่ถูกต้อง
เมื่อใช้ LayoutNG เนื่องจากเรามีโครงสร้างข้อมูลอินพุตและเอาต์พุตที่ชัดเจน และระบบไม่อนุญาตให้เข้าถึงสถานะก่อนหน้า เราจึงลดข้อบกพร่องประเภทนี้จากระบบเลย์เอาต์ได้ในระดับกว้าง
การลบล้างมากเกินไปและประสิทธิภาพ
ซึ่งตรงข้ามกับข้อบกพร่องประเภท "การลบล้างไม่เพียงพอ" บ่อยครั้งที่การแก้ไขข้อบกพร่องการลบล้างไม่สมบูรณ์จะทําให้ประสิทธิภาพลดลงอย่างมาก
เรามักต้องเลือกอย่างยากลำบากระหว่างความถูกต้องกับประสิทธิภาพ ในส่วนถัดไป เราจะเจาะลึกวิธีที่เราบรรเทาปัญหาด้านประสิทธิภาพประเภทเหล่านี้
เลย์เอาต์แบบ 2 ผ่านและประสิทธิภาพที่ลดลง
เลย์เอาต์ Flex และตารางกริดแสดงถึงการเปลี่ยนแปลงการแสดงออกของเลย์เอาต์บนเว็บ อย่างไรก็ตาม อัลกอริทึมเหล่านี้แตกต่างจากอัลกอริทึมเลย์เอาต์บล็อกที่ใช้ก่อนหน้านี้โดยพื้นฐาน
เลย์เอาต์บล็อก (เกือบทุกกรณี) กำหนดให้เอนจินจัดวางเลย์เอาต์กับองค์ประกอบย่อยทั้งหมดเพียงครั้งเดียวเท่านั้น ซึ่งวิธีนี้ส่งผลดีต่อประสิทธิภาพ แต่สุดท้ายแล้วกลับไม่แสดงผลอย่างที่ต้องการนักพัฒนาเว็บ
ตัวอย่างเช่น บางครั้งคุณต้องการขยายขนาดของรายการย่อยทั้งหมดให้เท่ากับขนาดของรายการย่อยที่ใหญ่ที่สุด เลย์เอาต์หลัก (Flex หรือตารางกริด) จะดำเนินการวัดเพื่อกำหนดขนาดขององค์ประกอบย่อยแต่ละรายการ จากนั้นจะดำเนินการจัดวางเพื่อยืดองค์ประกอบย่อยทั้งหมดให้มีขนาดเท่านี้ ลักษณะการทำงานนี้เป็นค่าเริ่มต้นสำหรับทั้งเลย์เอาต์ Flex และตารางกริด
เลย์เอาต์แบบ 2 ผ่านเหล่านี้มีประสิทธิภาพที่ยอมรับได้ในช่วงแรก เนื่องจากผู้ใช้มักไม่ได้ฝังเลย์เอาต์เหล่านี้ไว้ลึกมาก อย่างไรก็ตาม เราเริ่มพบปัญหาด้านประสิทธิภาพที่สำคัญเมื่อเนื้อหามีความซับซ้อนมากขึ้น หากคุณไม่แคชผลลัพธ์ของระยะการวัด ต้นไม้เลย์เอาต์จะสลับไปมาระหว่างสถานะการวัดกับสถานะเลย์เอาต์สุดท้าย
ก่อนหน้านี้เราพยายามเพิ่มแคชที่เฉพาะเจาะจงมากลงในเลย์เอาต์แบบยืดหยุ่นและตารางกริดเพื่อต่อสู้กับปัญหาประสิทธิภาพที่ลดลงอย่างรวดเร็วประเภทนี้ วิธีนี้ได้ผล (และเราทํางานกับ Flex ได้ไกลมาก) แต่ต้องต่อสู้กับข้อบกพร่องของการทำให้ข้อมูลไม่ถูกต้องทั้งน้อยและมากอยู่เสมอ
LayoutNG ช่วยให้เราสร้างโครงสร้างข้อมูลที่ชัดเจนสำหรับทั้งอินพุตและเอาต์พุตของเลย์เอาต์ และนอกจากนี้ เรายังได้สร้างแคชของค่าการวัดและเลย์เอาต์ด้วย ซึ่งทำให้ความซับซ้อนกลับมาเป็น O(n) อีกครั้ง ส่งผลให้นักพัฒนาเว็บคาดการณ์ประสิทธิภาพแบบเชิงเส้นได้ หากมีกรณีที่เลย์เอาต์ทำเลย์เอาต์ 3 ผ่าน เราจะแคชผ่านนั้นด้วย ซึ่งอาจเปิดโอกาสให้นําเสนอโหมดเลย์เอาต์ขั้นสูงอื่นๆ ได้อย่างปลอดภัยในอนาคต ซึ่งเป็นตัวอย่างที่แสดงให้เห็นว่า RenderingNG ปลดล็อกความสามารถในการขยายได้ในทุกด้าน ในบางกรณี เลย์เอาต์ตารางกริดอาจต้องใช้เลย์เอาต์ 3 ผ่าน แต่ปัจจุบันพบได้น้อยมาก
เราพบว่าเมื่อนักพัฒนาซอฟต์แวร์พบปัญหาด้านประสิทธิภาพเกี่ยวกับเลย์เอาต์โดยเฉพาะ ปัญหานั้นมักเกิดจากข้อบกพร่องเกี่ยวกับเวลาในการแสดงผลแบบทวีคูณ ไม่ใช่ปริมาณข้อมูลที่ส่งผ่านขั้นต้นของระยะการแสดงผลของไปป์ไลน์ หากการเปลี่ยนแปลงเล็กๆ น้อยๆ (องค์ประกอบหนึ่งๆ เปลี่ยนแปลงพร็อพเพอร์ตี้ CSS รายการเดียว) ส่งผลให้เลย์เอาต์ใช้เวลา 50-100 มิลลิวินาที แสดงว่านี่อาจเป็นข้อบกพร่องของเลย์เอาต์แบบทวีคูณ
ข้อมูลสรุป
เลย์เอาต์เป็นพื้นที่ที่มีความซับซ้อนอย่างมาก และเราไม่ได้กล่าวถึงรายละเอียดที่น่าสนใจทุกประเภท เช่น การเพิ่มประสิทธิภาพเลย์เอาต์ในบรรทัด (วิธีการทำงานของระบบย่อยทั้งหมดในบรรทัดและข้อความ) และแม้แต่แนวคิดที่พูดถึงที่นี่ก็เป็นเพียงข้อมูลเบื้องต้นเท่านั้น และไม่ได้กล่าวถึงรายละเอียดมากมาย อย่างไรก็ตาม เราหวังว่าได้แสดงให้เห็นว่าการปรับปรุงสถาปัตยกรรมของระบบอย่างเป็นระบบจะทําให้เกิดผลกำไรที่เหนือกว่าในระยะยาวได้อย่างไร
อย่างไรก็ตาม เรารู้ว่ายังมีงานอีกมากที่ต้องทำ เราทราบถึงปัญหาหลายประเภท (ทั้งด้านประสิทธิภาพและความถูกต้อง) ที่เรากำลังพยายามแก้ไขอยู่ และตื่นเต้นกับฟีเจอร์การจัดวางใหม่ที่จะเข้ามาใน CSS เราเชื่อว่าสถาปัตยกรรมของ LayoutNG จะช่วยแก้ปัญหาเหล่านี้ได้อย่างปลอดภัยและจัดการได้
รูปภาพ 1 รูป (คุณรู้อยู่แล้วว่ารูปภาพไหน) โดย Una Kravets