ผมชื่อเอียน คิลแพทริก หัวหน้าวิศวกรของทีมเลย์เอาต์ Blink พร้อมด้วย Koji Ishii ก่อนที่จะทำงานในทีม Blink ผมเป็นวิศวกรฟรอนท์เอนด์ (ก่อนที่ Google จะเข้ามามีบทบาทเป็น "วิศวกรฟรอนท์เอนด์") การสร้างฟีเจอร์ภายใน Google เอกสาร, ไดรฟ์ และ Gmail หลังจากทำงานนี้มาได้ประมาณ 5 ปี ผมจึงตัดสินใจ เปลี่ยนไปใช้ทีม Blink การเรียนรู้ C++ อย่างมีประสิทธิภาพในงาน และพยายามเพิ่มฐานของโค้ด Blink ที่มีความซับซ้อนมหาศาล ทุกวันนี้ฉันก็เข้าใจแค่เพียงส่วนเล็กๆ เท่านั้น ขอขอบคุณที่สละเวลาในระหว่างนี้ ผมปลื้มใจมากที่ได้รู้ว่า "การฟื้นตัวของวิศวกรฟรอนท์เอนด์" ได้เปลี่ยนไปเป็น "วิศวกรเบราว์เซอร์" ก่อนผม
ประสบการณ์ก่อนหน้านี้ทำให้ฉันรู้สึกเหมือนได้ร่วมงานกับทีม Blink เป็นการส่วนตัว ในฐานะวิศวกรฟรอนท์เอนด์ ผมต้องพบกับความไม่สอดคล้องของเบราว์เซอร์อยู่เรื่อยๆ ปัญหาเกี่ยวกับประสิทธิภาพ การแสดงข้อบกพร่อง และฟีเจอร์ที่หายไป LayoutNG เป็นโอกาสที่ผมได้ช่วยแก้ปัญหาเหล่านี้อย่างเป็นระบบภายในระบบการจัดวางของ Blink และแสดงถึงผลรวมของวิศวกรจำนวนมาก ในช่วงหลายปีที่ผ่านมา
ในโพสต์นี้ เราจะอธิบายว่าการเปลี่ยนแปลงสถาปัตยกรรมครั้งใหญ่เช่นนี้จะช่วยลดและลดจำนวนข้อบกพร่องประเภทต่างๆ และปัญหาด้านประสิทธิภาพได้อย่างไร
ภาพทิวทัศน์สูง 30,000 ฟุตของสถาปัตยกรรมระบบเลย์เอาต์
ก่อนหน้านี้แผนผังเลย์เอาต์ของ Blink คือสิ่งที่ผมจะเรียกว่า "แผนผังแบบเปลี่ยนแปลงได้"
ออบเจ็กต์แต่ละรายการในแผนผังเลย์เอาต์มีข้อมูล input เช่น ขนาดที่ใช้ได้ซึ่งกำหนดโดยผู้ปกครอง ตำแหน่งของ Float และข้อมูลเอาต์พุต เช่น ความกว้างและความสูงสุดท้ายของวัตถุ หรือตำแหน่ง x และ y ของวัตถุ
มีการเก็บวัตถุเหล่านี้ไว้ระหว่างการแสดงภาพ เมื่อเปลี่ยนรูปแบบแล้ว เราทำเครื่องหมายวัตถุนั้นว่าสกปรก และเช่นเดียวกันพ่อแม่ของวัตถุนั้นในต้นไม้ เมื่อมีการเรียกใช้เฟสเลย์เอาต์ของไปป์ไลน์การแสดงผล เราก็ต้องทำความสะอาดต้นไม้ เดินบนวัตถุสกปรกต่างๆ แล้วเรียกใช้การจัดวางเพื่อให้อยู่ในสภาพสะอาด
เราพบว่าสถาปัตยกรรมนี้ก่อให้เกิดปัญหาหลายคลาส ซึ่งเราจะอธิบายด้านล่าง แต่ก่อนอื่น เรามาลองย้อนกลับไปดูว่าอินพุตและเอาต์พุตของเลย์เอาต์มีอะไรบ้าง
การเรียกใช้การออกแบบบนโหนดในต้นไม้นี้ใช้ "รูปแบบบวก DOM" และข้อจำกัดระดับบนสุดจากระบบการจัดวางหลัก (ตารางกริด บล็อก หรือ Flex) เรียกใช้อัลกอริทึมข้อจำกัดการจัดวางและได้ผลลัพธ์
สถาปัตยกรรมใหม่ของเราทำให้โมเดลแนวคิดนี้ดูเป็นทางการมากขึ้น เรายังคงมีแผนผังเลย์เอาต์อยู่ แต่ใช้แผนผังเพื่อยึดอินพุตและเอาต์พุตของเลย์เอาต์เป็นหลัก สำหรับผลลัพธ์ที่ได้ เราจะสร้างออบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ใหม่ทั้งหมด ซึ่งเรียกว่า Fragment Tree
เราได้พูดถึง ส่วนย่อยที่เปลี่ยนแปลงไม่ได้ก่อนหน้านี้ อธิบายวิธีที่ได้รับการออกแบบให้นําส่วนต่างๆ ของแผนผังก่อนหน้านี้กลับมาใช้ซ้ำในเลย์เอาต์เพิ่มเติม
นอกจากนี้ เรายังจัดเก็บออบเจ็กต์ข้อจำกัดหลักที่สร้างส่วนย่อยนั้น เราใช้คีย์นี้เป็นคีย์แคช ซึ่งจะอธิบายเพิ่มเติมด้านล่าง
นอกจากนี้ อัลกอริทึมการจัดวางแบบอินไลน์ (ข้อความ) ยังได้รับการเขียนใหม่เพื่อให้ตรงกับสถาปัตยกรรมใหม่ที่เปลี่ยนแปลงไม่ได้อีกด้วย โดยไม่เพียงแต่ทำให้ การนำเสนอแบบรายการเดี่ยวที่เปลี่ยนแปลงไม่ได้ สำหรับเลย์เอาต์แบบอินไลน์ แต่ยังมีการแคชระดับย่อหน้าเพื่อการส่งต่อที่เร็วขึ้นด้วย รูปร่างต่อย่อหน้าเพื่อนำฟีเจอร์แบบอักษรไปใช้กับองค์ประกอบและคำต่างๆ อัลกอริทึมแบบ 2 ทิศทางของ Unicode ใหม่โดยใช้ ICU, การแก้ไขความถูกต้องมากมาย และอีกมากมาย
ประเภทของข้อบกพร่องในเลย์เอาต์
ข้อบกพร่องของรูปแบบที่พบโดยทั่วไปแบ่งออกเป็น 4 หมวดหมู่ แต่ละรายการมีสาเหตุต้นตอแตกต่างกัน
ความถูกต้อง
เมื่อเรานึกถึงข้อบกพร่องในระบบการแสดงภาพ เรามักจะนึกถึงความถูกต้อง ตัวอย่างเช่น "เบราว์เซอร์ A มีพฤติกรรม X ในขณะที่เบราว์เซอร์ B มีพฤติกรรม Y", หรือ "เบราว์เซอร์ A และ B ใช้งานไม่ได้ทั้งคู่" ก่อนหน้านี้คือสิ่งที่เราใช้เวลามากมายไปกับ และในกระบวนการนี้ เราได้ต่อสู้กับระบบนี้อย่างต่อเนื่อง โหมดการทำงานล้มเหลวที่พบบ่อยคือใช้การแก้ไขที่ตรงเป้าหมายมากสำหรับข้อบกพร่องหนึ่ง แต่ผ่านไปหลายสัปดาห์ต่อมา เราก็ได้เกิดการถดถอยในอีกระบบหนึ่ง (ซึ่งดูเหมือนจะไม่เกี่ยวข้อง)
ตามที่อธิบายไว้ในโพสต์ก่อนหน้า นี่เป็นสัญญาณของระบบที่เปราะบาง สำหรับเลย์เอาต์โดยเฉพาะ เราไม่ได้มีการทำสัญญาที่ชัดเจนระหว่างคลาสใดๆ ทำให้วิศวกรเบราว์เซอร์ต้องพึ่งสถานะที่ไม่ควร หรือตีความค่าบางส่วนจากส่วนอื่นๆ ของระบบผิด
ตัวอย่างเช่น มีจุดหนึ่งเรามีข้อบกพร่องประมาณ 10 รายการ ในช่วงเวลากว่า 1 ปี ที่เกี่ยวข้องกับเลย์เอาต์ Flex การแก้ไขแต่ละครั้งก่อให้เกิดปัญหาด้านความถูกต้องหรือประสิทธิภาพในส่วนใดระบบหนึ่ง จึงทำให้เกิดข้อบกพร่องขึ้นอีก
ตอนนี้ LayoutNG กำหนดสัญญาระหว่างองค์ประกอบทั้งหมดในระบบการออกแบบอย่างชัดเจนแล้ว ทำให้เราสามารถดำเนินการเปลี่ยนแปลงได้อย่างมั่นใจมากขึ้น เรายังได้ประโยชน์อย่างมากจากโครงการ Web Platform Tests (WPT) ที่ยอดเยี่ยม ซึ่งทำให้หลายฝ่ายสามารถร่วมให้ข้อมูลในชุดทดสอบเว็บทั่วไปได้
ในวันนี้ เราพบว่าถ้าเราเผยแพร่การถดถอยที่แท้จริงในช่องทางที่เสถียร โดยทั่วไปจะไม่มีการทดสอบที่เกี่ยวข้องในที่เก็บ WPT และไม่ได้เกิดจากความเข้าใจผิดเกี่ยวกับสัญญาส่วนประกอบ นอกจากนี้ ตามนโยบายการแก้ไขข้อบกพร่อง เราจะเพิ่มการทดสอบ WPT ใหม่เสมอ เพื่อช่วยให้แน่ใจว่าไม่มีเบราว์เซอร์ใดทำผิดพลาดซ้ำอีก
ไม่ถูกต้อง
ถ้าคุณเคยพบข้อบกพร่องลึกลับที่การปรับขนาดหน้าต่างเบราว์เซอร์ หรือการสลับพร็อพเพอร์ตี้ CSS ทำให้ข้อบกพร่องนั้นหายไปได้อย่างน่าอัศจรรย์ ก็ประสบปัญหาการเสียข้อมูลไม่ถูกต้อง ส่วนหนึ่งของต้นไม้ที่เปลี่ยนแปลงได้นั้น ถือว่าสะอาด แต่เนื่องจากมีการเปลี่ยนแปลงบางอย่างในข้อจำกัดระดับบน นั่นจึงไม่ได้แสดงเอาต์พุตที่ถูกต้อง
ซึ่งพบได้บ่อยมากเมื่อใช้โหมด 2 Pass (เดินโครงสร้างของเลย์เอาต์ 2 ครั้งเพื่อกำหนดสถานะเลย์เอาต์สุดท้าย) โหมดเลย์เอาต์ที่อธิบายไว้ด้านล่าง ก่อนหน้านี้โค้ดของเรามีลักษณะดังนี้
if (/* some very complicated statement */) {
child->ForceLayout();
}
โดยทั่วไปแล้ว การแก้ไขสำหรับข้อบกพร่องประเภทนี้มีดังนี้
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
โดยทั่วไปแล้ว การแก้ไขสำหรับปัญหาประเภทนี้จะทำให้ประสิทธิภาพการถดถอยที่รุนแรง (ดูการระบุว่าไม่ถูกต้องมากเกินไปด้านล่าง) และมีการดำเนินการที่ละเอียดอ่อนมากในการแก้ไข
ปัจจุบัน (ตามที่อธิบายไว้ข้างต้น) เรามีออบเจ็กต์ข้อจำกัดระดับบนสุดที่เปลี่ยนแปลงไม่ได้ซึ่งอธิบายอินพุตทั้งหมดจากเลย์เอาต์หลักไปยังย่อย เราจัดเก็บตัวแปรนี้ไว้กับส่วนย่อยที่เปลี่ยนแปลงไม่ได้ที่เกิดขึ้น ด้วยเหตุนี้ เรามีศูนย์กลางที่จะแยกความแตกต่างของอินพุตทั้ง 2 รายการนี้เพื่อพิจารณาว่าเด็กต้องมี Layout Pass อื่นหรือไม่ ตรรกะที่ต่างกันนี้ซับซ้อนแต่สมบูรณ์แบบ โดยทั่วไปแล้ว การแก้ไขข้อบกพร่องเกี่ยวกับคลาสที่ไม่ถูกต้องเหล่านี้จะทำให้มีการตรวจสอบอินพุต 2 รายการด้วยตนเอง และตัดสินใจว่าอะไรในอินพุตที่เปลี่ยนไป ซึ่งทำให้ต้องใช้เลย์เอาต์อื่น
การแก้ไขโค้ดความแตกต่างนี้มักจะทำได้ง่ายๆ และสามารถ unit-testable ได้ง่าย เนื่องจากความเรียบง่ายในการสร้างออบเจ็กต์อิสระเหล่านี้
![การเปรียบเทียบรูปภาพความกว้างคงที่และเปอร์เซ็นต์ความกว้าง](https://developer.chrome.com/static/docs/chromium/layoutng/image/comparing-fixed-width-p-d298e82695e5e.png?authuser=19&hl=th)
โค้ดสำหรับตัวอย่างข้างต้นมีความแตกต่าง:
if (width.IsPercent()) {
if (old_constraints.WidthPercentageSize()
!= new_constraints.WidthPercentageSize())
return kNeedsLayout;
}
if (height.IsPercent()) {
if (old_constraints.HeightPercentageSize()
!= new_constraints.HeightPercentageSize())
return kNeedsLayout;
}
ไฮสเตอเรซิส
ข้อบกพร่องประเภทนี้คล้ายกับการบิดเบือนข้อมูล โดยพื้นฐานแล้ว ในระบบเดิมนั้น เป็นเรื่องยากที่จะรับประกันว่าเลย์เอาต์จะดำเนินไปอย่างไม่มีแบบแผน นั่นคือ การเรียกใช้เลย์เอาต์อีกครั้งด้วยอินพุตเดิม ซึ่งส่งผลให้ได้เอาต์พุตเดียวกัน
ในตัวอย่างด้านล่าง เราจะสลับใช้คุณสมบัติ CSS กลับไปกลับมาระหว่าง 2 ค่า แต่ผลที่ได้คืออัตรา Conversion สี่เหลี่ยมผืนผ้า
ด้วยโครงสร้างที่เปลี่ยนแปลงไม่ได้ก่อนหน้านี้ การแนะนำข้อบกพร่องแบบนี้ ทำได้ง่ายมาก หากโค้ดทำให้การอ่านขนาดหรือตำแหน่งของวัตถุในเวลาหรือขั้นตอนไม่ถูกต้อง (เนื่องจากเราไม่ได้ "ล้าง" ขนาดหรือตำแหน่งก่อนหน้า) เราก็จะเพิ่มข้อบกพร่องเล็กๆ น้อยๆ ของการไม่ไฮเทอรีซิสในทันที โดยปกติแล้วข้อบกพร่องเหล่านี้จะไม่ปรากฏในการทดสอบ เนื่องจากการทดสอบส่วนใหญ่จะมุ่งเน้นที่เลย์เอาต์และการแสดงภาพแบบเดี่ยว และยิ่งเป็นกังวล เราก็รู้ว่าจำเป็นต้องทำให้โหมดเลย์เอาต์บางโหมดทำงานได้อย่างถูกต้อง เราพบข้อบกพร่องที่จะทำการเพิ่มประสิทธิภาพ เพื่อนำ Layout Pass ออก แต่แนะนำ "ข้อบกพร่อง" เนื่องจากโหมดเลย์เอาต์ต้องใช้บัตร 2 ครั้งเพื่อให้ได้เอาต์พุตที่ถูกต้อง
![ต้นไม้ที่แสดงปัญหาที่อธิบายไว้ในข้อความก่อนหน้า](https://developer.chrome.com/static/docs/chromium/layoutng/image/a-tree-demonstrating-pro-9c56bc875a773.png?authuser=19&hl=th)
ด้วย LayoutNG เนื่องจากมีโครงสร้างข้อมูล อินพุตและเอาต์พุตที่ชัดเจน และเข้าถึงสถานะก่อนหน้าไม่ได้ เราจึงได้ลดข้อบกพร่องประเภทนี้ออกไปจากระบบเลย์เอาต์เป็นวงกว้าง
การใช้ข้อมูลที่ไม่ถูกต้องและประสิทธิภาพมากเกินไป
ซึ่งเป็นสิ่งที่ตรงข้ามกับประเภทข้อบกพร่องที่ไม่ถูกต้องและมีความถูกต้อง บ่อยครั้งที่เมื่อแก้ไขข้อบกพร่องที่ผิดพลาดไม่ถูกต้อง เราจะทริกเกอร์หน้าผาประสิทธิภาพ
เรามักต้องทำการตัดสินใจยากๆ เพื่อให้ได้มาซึ่งความถูกต้องมากกว่าประสิทธิภาพ ในส่วนถัดไป เราจะเจาะลึกวิธีที่เราลดปัญหาด้านประสิทธิภาพประเภทนี้
เพิ่มช่องทางผ่านเลย์เอาต์แบบ 2 จุดและหน้าผาการแสดงผาดโผน
เลย์เอาต์แบบยืดหยุ่นและตารางกริดแสดงถึงการเปลี่ยนแปลงด้านการแสดงออกของเลย์เอาต์บนเว็บ อย่างไรก็ตาม โดยพื้นฐานแล้วอัลกอริทึมเหล่านี้แตกต่างจากอัลกอริทึมเลย์เอาต์แบบบล็อกมาก่อน
เลย์เอาต์แบบบล็อก (ในเกือบทุกกรณี) กำหนดให้เครื่องมือทำเลย์เอาต์กับรายการย่อยทั้งหมดเพียงครั้งเดียวเท่านั้น วิธีนี้ได้ผลดีสำหรับประสิทธิภาพ แต่จบลงแล้วไม่ได้แสดงออกอย่างที่นักพัฒนาเว็บต้องการ
ตัวอย่างเช่น บ่อยครั้งที่คุณต้องการขนาดของเด็ก ให้ใหญ่ขึ้นตามขนาดที่ใหญ่ที่สุด เพื่อรองรับการตั้งค่านี้ เลย์เอาต์หลัก (เฟล็กซ์หรือตารางกริด) จะทำการวัดผลเพื่อตัดสินว่าเด็กแต่ละคนมีขนาดเท่าใด เลย์เอาต์จะผ่านเพื่อขยายองค์ประกอบย่อยทั้งหมดให้เป็นขนาดนี้ ลักษณะการทำงานนี้เป็นค่าเริ่มต้นสำหรับทั้งเลย์เอาต์แบบยืดหยุ่นและตารางกริด
การจัดวางแบบผ่าน 2 แบบ เมื่อมองในแง่ประสิทธิภาพที่ยอมรับได้ เพราะปกติแล้วคน มักจะไม่ได้วางฝังลึก อย่างไรก็ตาม เราเริ่มพบปัญหาด้านประสิทธิภาพที่สำคัญเนื่องจากมีเนื้อหาที่ซับซ้อนมากขึ้นเกิดขึ้น หากไม่ได้แคชผลลัพธ์ของระยะวัดผล แผนผังเลย์เอาต์จะข้ามระหว่างสถานะการวัดกับสถานะเลย์เอาต์สุดท้าย
![เลย์เอาต์ 1, 2 และ 3 Pass อธิบายอยู่ในคำบรรยายภาพ](https://developer.chrome.com/static/docs/chromium/layoutng/image/the-two-three-pass-la-d50559550340a.png?authuser=19&hl=th)
<div>
3 รายการ
เลย์เอาต์แบบ 1 พาสแบบง่าย (เช่น เลย์เอาต์แบบบล็อก) จะไปยังโหนดเลย์เอาต์ 3 โหนด (ความซับซ้อน O(n))
อย่างไรก็ตาม สำหรับเลย์เอาต์แบบ 2-Pass (เช่น Flex หรือ ตารางกริด)
ซึ่งอาจทําให้เกิดความซับซ้อนของการเข้าชม O(2n) สำหรับตัวอย่างนี้
![กราฟแสดงเวลาของเลย์เอาต์ที่เพิ่มขึ้นแบบเอ็กซ์โปเนนเชียล](https://developer.chrome.com/static/docs/chromium/layoutng/image/graph-showing-exponentia-6d6966fa98b0e.png?authuser=19&hl=th)
ก่อนหน้านี้ เราจะพยายามเพิ่มแคชที่เฉพาะเจาะจงลงในเลย์เอาต์แบบยืดหยุ่นและตารางกริดเพื่อต่อสู้กับหน้าผาประสิทธิภาพประเภทนี้ วิธีนี้ได้ผล (และเราก้าวไปไกลมากด้วย Flex) แต่ยังคงต่อสู้กับข้อบกพร่องที่ทำให้เป็นโมฆะอย่างต่อเนื่อง
LayoutNG ช่วยให้เราสร้างโครงสร้างข้อมูลที่ชัดเจนสำหรับทั้งอินพุตและเอาต์พุตของเลย์เอาต์ ยิ่งไปกว่านั้น เรายังได้สร้างแคชของการวัดและผ่านการจัดวาง ซึ่งทำให้ความซับซ้อนกลับมาอยู่ใน O(n) ส่งผลให้เกิดประสิทธิภาพเชิงเส้นที่คาดการณ์ได้สำหรับนักพัฒนาเว็บ หากมีกรณีที่เลย์เอาต์ทำเลย์เอาต์แบบ 3 Pass เราจะแคชบัตรผ่านดังกล่าวไว้เช่นเดียวกัน ซึ่งอาจเปิดโอกาสให้นำเสนอโหมดเลย์เอาต์ขั้นสูงขึ้นได้อย่างปลอดภัยในตัวอย่างของหลักการพื้นฐานเกี่ยวกับ RenderingNG ในอนาคต ช่วยปลดล็อกความสามารถในการขยายการใช้งานทั่วทั้งกระดาน ในบางกรณี เลย์เอาต์แบบตารางกริดอาจต้องใช้เลย์เอาต์แบบ 3 จุด แต่ปัจจุบันไม่ค่อยพบมากนัก
เราพบว่าเมื่อนักพัฒนาแอปประสบปัญหาด้านประสิทธิภาพ โดยเฉพาะกับเลย์เอาต์ ปัญหานี้มักเกิดจากข้อบกพร่องเวลาของเลย์เอาต์เอ็กซ์โปเนนเชียล แทนที่จะเป็นอัตราการส่งข้อมูลดิบของขั้นตอนเลย์เอาต์ของไปป์ไลน์ หากการเปลี่ยนแปลงเพิ่มขึ้นเล็กน้อย (องค์ประกอบ 1 รายการเปลี่ยนพร็อพเพอร์ตี้ CSS เดียว) ส่งผลให้เลย์เอาต์ใช้เวลา 50-100 มิลลิวินาที ก็น่าจะเกิดจากข้อบกพร่องของเลย์เอาต์แบบเอ็กซ์โปเนนเชียล
ข้อมูลสรุป
เค้าโครงคือพื้นที่ที่ซับซ้อนมาก และไม่ได้พูดถึงรายละเอียดที่น่าสนใจทั้งหมด เช่น การเพิ่มประสิทธิภาพการจัดวางแบบอินไลน์ (จริงๆ แล้วระบบย่อยของอินไลน์และข้อความทั้งระบบ) และแม้แต่แนวคิดที่พูดถึงที่นี่ ก็เป็นแค่เพียงผิวเผิน และลงรายละเอียดมากมาย อย่างไรก็ตาม เราหวังว่าจะได้แสดงให้เห็นว่าการปรับปรุงสถาปัตยกรรมของระบบอย่างเป็นระบบจะทำให้เกิดประโยชน์อย่างมากในระยะยาวได้อย่างไร
อย่างไรก็ตาม เรารู้ว่าเรายังมีงานอีกมากมายรออยู่ข้างหน้า เราตระหนักดีถึงปัญหาประเภทต่างๆ (ทั้งประสิทธิภาพและความถูกต้อง) ที่เรากำลังดำเนินการแก้ไข และตื่นเต้นกับฟีเจอร์เลย์เอาต์ใหม่ๆ ที่กำลังจะมีให้ใช้งานใน CSS เราเชื่อว่าสถาปัตยกรรมของ LayoutNG ทำให้การแก้ปัญหาเหล่านี้ปลอดภัยและน่าเชื่อถือ
รูปภาพ 1 รูป (รู้จักรูปไหนด้วย) โดย Una Kravets