ผมชื่อ Ian Kilpatrick เป็นหัวหน้าวิศวกร ของทีมเลย์เอาต์ของ Blink ร่วมกับ Koji Ishii ก่อนทำงานในทีม Blink ผมเป็นวิศวกรฟรอนท์เอนด์ (ก่อนที่ Google จะรับบทบาทเป็น "วิศวกรฟรอนท์เอนด์") โดยมี การสร้างฟีเจอร์ต่างๆ ภายใน Google เอกสาร, ไดรฟ์ และ Gmail หลังจากผ่านไปประมาณ 5 ปี ผมก็ลองเดิมพันครั้งใหญ่โดยสลับไปทำงานกับทีม Blink เรียนรู้ C++ เกี่ยวกับงานอย่างมีประสิทธิภาพ และพยายามเพิ่มฐานของโค้ด Blink ที่ซับซ้อนขึ้น ถึงตอนนี้เราเข้าใจเนื้อหาเพียงส่วนน้อยเท่านั้น เรารู้สึกซาบซึ้งเป็นอย่างยิ่งสำหรับช่วงเวลาที่ได้มอบให้แก่เราในช่วงเวลานี้ ผมรู้สึกอุ่นใจมากที่ "การกู้คืนวิศวกรฟรอนท์เอนด์" จำนวนมากทำให้ผมเปลี่ยนมาเป็น "วิศวกรเบราว์เซอร์" ก่อนผม
ประสบการณ์ที่ผ่านมาช่วยแนะนำฉันเป็นการส่วนตัวขณะอยู่ในทีม Blink ในฐานะวิศวกรฟรอนท์เอนด์ ฉันมักจะพบกับความไม่สอดคล้องของเบราว์เซอร์ ปัญหาด้านประสิทธิภาพ ข้อบกพร่องในการแสดงผล และฟีเจอร์ที่ขาดหายไป LayoutNG เป็นโอกาสให้เราช่วยแก้ปัญหาเหล่านี้ในระบบเลย์เอาต์ของ Blink อย่างเป็นระบบ และแสดงถึงความพยายามของวิศวกรหลายคนในช่วงหลายปีที่ผ่านมา
ในโพสต์นี้ เราจะอธิบายว่าการเปลี่ยนแปลงสถาปัตยกรรมครั้งใหญ่เช่นนี้จะช่วยลดและลดข้อบกพร่องและปัญหาด้านประสิทธิภาพประเภทต่างๆ ได้อย่างไร
ภาพมุมมอง 30,000 ฟุตของสถาปัตยกรรมเครื่องมือเลย์เอาต์
ก่อนหน้านี้ แผนผังเลย์เอาต์ของ Blink คือสิ่งที่ฉันจะเรียกว่า "ต้นไม้แบบเปลี่ยนแปลงได้"
แต่ละวัตถุในแผนผังเลย์เอาต์มีข้อมูลอินพุต เช่น ขนาดที่พร้อมใช้งานซึ่งกำหนดโดยผู้ปกครอง ตำแหน่งของแบบลอย และข้อมูลเอาต์พุต ตัวอย่างเช่น ความกว้างและความสูงสุดท้ายของวัตถุ หรือตำแหน่ง x และ y
วัตถุเหล่านี้อยู่รอบๆ ระหว่างการแสดงภาพ เมื่อเปลี่ยนรูปแบบแล้ว เราทำเครื่องหมายวัตถุนั้นว่าสกปรก และต้นกำเนิดทั้งหมดที่อยู่บนต้นไม้ เมื่อช่วงการจัดวางของไปป์ไลน์การแสดงผลทำงาน เราจะทำความสะอาดต้นไม้ เดินบนวัตถุที่สกปรก แล้วเรียกใช้เลย์เอาต์เพื่อให้ได้สถานะที่สะอาด
เราพบว่าสถาปัตยกรรมนี้ทำให้เกิดปัญหาในหลายระดับ ซึ่งเราจะอธิบายต่อไปนี้ แต่ก่อนอื่น เรามาย้อนกลับไปและพิจารณาว่าอินพุตและเอาต์พุตของเลย์เอาต์มีอะไรบ้าง
การเรียกใช้เลย์เอาต์บนโหนดในต้นไม้นี้ใช้ "สไตล์และ DOM" และข้อจำกัดระดับบนสุดจากระบบเลย์เอาต์ระดับบนสุด (ตารางกริด บล็อก หรือ Flex) จะเรียกใช้อัลกอริทึมข้อจำกัดเลย์เอาต์และสร้างผลลัพธ์
สถาปัตยกรรมใหม่ของเราทำให้โมเดลแนวคิดนี้ดูเป็นรูปธรรม เรายังมีแผนผังเลย์เอาต์ แต่ใช้เพื่อยึดอินพุตและเอาต์พุตของเลย์เอาต์เป็นหลัก สำหรับเอาต์พุตนี้ เราจะสร้างออบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ขึ้นมาใหม่ทั้งหมดซึ่งเรียกว่า แฟรกเมนต์ทรี
ผมได้พูดถึงแผนผังส่วนย่อยที่เปลี่ยนแปลงไม่ได้ก่อนหน้านี้ไปแล้ว โดยได้อธิบายถึงวิธีการออกแบบการนำส่วนขนาดใหญ่ของแผนผังก่อนหน้ามาใช้ซ้ำสำหรับเลย์เอาต์ที่เพิ่มขึ้น
นอกจากนี้ เรายังจัดเก็บออบเจ็กต์ข้อจำกัดระดับบนที่สร้างส่วนย่อยดังกล่าวด้วย เราใช้คีย์นี้เป็นคีย์แคช ซึ่งจะพูดถึงข้อมูลเพิ่มเติมด้านล่าง
นอกจากนี้ ระบบยังเขียนอัลกอริทึมเลย์เอาต์ในบรรทัด (แบบข้อความ) ใหม่เพื่อให้ตรงกับสถาปัตยกรรมใหม่ที่เปลี่ยนแปลงไม่ได้ ซึ่งไม่เพียงสร้าง การแสดงรายการแบบเดี่ยวที่เปลี่ยนแปลงไม่ได้ สำหรับเลย์เอาต์แบบอินไลน์ แต่ยังมีการแคชระดับย่อหน้าเพื่อให้รีเลย์เร็วขึ้น รูปร่างต่อย่อหน้าเพื่อใช้คุณลักษณะของแบบอักษรกับองค์ประกอบและคำ อัลกอริทึมแบบใหม่ของ Unicode แบบ 2 ทิศทางที่ใช้ ICU, การแก้ไขความถูกต้องจำนวนมาก และอีกมากมาย
ประเภทของข้อบกพร่องด้านเลย์เอาต์
ข้อบกพร่องด้านเลย์เอาต์ที่พูดถึงอย่างกว้างๆ แบ่งออกเป็น 4 หมวดหมู่ ซึ่งแต่ละประเภทมีสาเหตุที่แท้จริงต่างกัน
ความถูกต้อง
เมื่อนึกถึงข้อบกพร่องในระบบการแสดงผล เรามักจะนึกถึงความถูกต้อง ตัวอย่างเช่น "เบราว์เซอร์ A มีลักษณะการทำงานแบบ X ในขณะที่เบราว์เซอร์ B มีลักษณะการทำงานแบบ Y" หรือ "เบราว์เซอร์ A และ B ไม่ทำงาน" ก่อนหน้านี้เราใช้เวลาอย่างมาก และต้องต่อสู้กับระบบอย่างต่อเนื่อง โหมดการทำงานล้มเหลวโดยทั่วไปคือ การแก้ไขเป้าหมายสำหรับข้อบกพร่องหนึ่ง แต่พบว่าสัปดาห์ต่อมาเราได้ทำให้ส่วนอื่นในระบบเกิดปัญหา (ดูจะไม่เกี่ยวข้อง) ถดถอย
นี่เป็นสัญญาณของระบบที่ยุ่งเหยิงมาก ดังที่ได้อธิบายไว้ในโพสต์ก่อนหน้า สำหรับการจัดวางโดยเฉพาะ เราไม่ได้ทำสัญญาที่ชัดเจนระหว่างคลาสใดๆ ทำให้วิศวกรเบราว์เซอร์ต้องพึ่งพาสถานะที่ไม่ควร หรือตีความค่าบางอย่างผิดจากส่วนอื่นๆ ของระบบ
เช่น ช่วงหนึ่งเรามีข้อบกพร่องประมาณ 10 รายการในช่วงกว่า 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();
}
การแก้ไขปัญหาประเภทนี้มักทำให้ประสิทธิภาพถดถอยอย่างรุนแรง (ดูการแก้ไขมากเกินไปด้านล่าง) และแก้ไขเป็นเรื่องละเอียดอ่อนมาก
ในวันนี้ (ตามที่อธิบายไว้ข้างต้น) เรามีออบเจ็กต์ข้อจำกัดระดับบนที่เปลี่ยนแปลงไม่ได้ ซึ่งอธิบายอินพุตทั้งหมดจากเลย์เอาต์หลักไปยังไปยังระดับล่าง เราจัดเก็บรายการนี้ไว้กับแฟรกเมนต์ที่เปลี่ยนแปลงไม่ได้ที่เกิดขึ้น เราจึงมีส่วนกลางที่เราจะแยกอินพุต 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;
}
ไฮสเตเรซิส
ข้อบกพร่องประเภทนี้คล้ายกับการระบุว่าไม่ถูกต้อง โดยพื้นฐานแล้ว ในระบบก่อนหน้านี้ การทำให้เลย์เอาต์เป็นแบบเดิมเป็นเรื่องที่ทำได้ยากมาก ซึ่งก็คือการเรียกใช้เลย์เอาต์ที่มีอินพุตเดียวกันอีกครั้ง ซึ่งทำให้ได้เอาต์พุตเดียวกัน
ในตัวอย่างด้านล่าง เราเพียงแค่สลับคุณสมบัติ CSS ไปมาระหว่าง 2 ค่า อย่างไรก็ตาม ผลที่ได้คือสี่เหลี่ยมผืนผ้า "เติบโตอย่างไม่มีที่สิ้นสุด"
สำหรับแผนผังที่เปลี่ยนได้ก่อนหน้านี้ ทำให้ข้อบกพร่องแบบนี้เกิดขึ้นได้ง่ายมาก หากโค้ดอ่านขนาดหรือตำแหน่งของวัตถุผิดพลาดในเวลาหรือระยะที่ไม่ถูกต้อง (เช่น เราไม่ได้ "ล้าง" ขนาดหรือตำแหน่งก่อนหน้า) เราจะเพิ่มข้อบกพร่องของการไฮสเตรีสย่อยเล็กๆ น้อยๆ โดยทันที โดยทั่วไปแล้ว ข้อบกพร่องเหล่านี้จะไม่ปรากฏในการทดสอบ เนื่องจากการทดสอบส่วนใหญ่จะมุ่งเน้นที่เลย์เอาต์และการแสดงภาพเดียว ที่น่ากังวลยิ่งขึ้นไปอีกคือ แต่เราก็ตระหนักว่าจำเป็นต้องมีอุปสรรคบางอย่างนี้เพื่อทำให้โหมดเลย์เอาต์บางอย่างทำงานได้อย่างถูกต้อง เราพบข้อบกพร่องที่ต้องเพิ่มประสิทธิภาพเพื่อลบ Layout Pass ออก แต่ให้แนะนำ "ข้อบกพร่อง" เนื่องจากโหมดเลย์เอาต์ต้องใช้การส่ง 2 ครั้งเพื่อให้ได้ผลลัพธ์ที่ถูกต้อง
ด้วย LayoutNG เนื่องจากเรามีโครงสร้างข้อมูลอินพุตและเอาต์พุตที่ชัดเจน และการเข้าถึงสถานะก่อนหน้าไม่ได้รับอนุญาต เราจึงลดข้อบกพร่องประเภทนี้จากระบบการออกแบบในวงกว้าง
การระบุแหล่งที่มาและประสิทธิภาพมากเกินไป
ซึ่งเป็นสิ่งที่ตรงข้ามกับข้อบกพร่องในประเภทที่ไม่ถูกต้องในระดับที่ไม่ถูกต้องโดยตรง การแก้ไขข้อบกพร่องที่ไม่เกี่ยวกับเหตุผลที่ไม่ถูกต้องมักจะทำให้เกิดหน้าผาประสิทธิภาพ
เราต้องตัดสินใจยากๆ เพราะเรามักเลือกความถูกต้องมากกว่าประสิทธิภาพ ในส่วนถัดไป เราจะเจาะลึกวิธีที่เราบรรเทาปัญหาเกี่ยวกับประสิทธิภาพประเภทนี้
การเพิ่มขึ้นของเลย์เอาต์แบบ 2 ทางและหน้าผาประสิทธิภาพสูง
เลย์เอาต์แบบยืดหยุ่นและตารางกริดแสดงถึงการเปลี่ยนแปลงความโดดเด่นของเลย์เอาต์ในเว็บ อย่างไรก็ตาม โดยพื้นฐานแล้ว อัลกอริทึมเหล่านี้แตกต่างจากอัลกอริทึมเลย์เอาต์แบบบล็อกที่อยู่ก่อนหน้า
เลย์เอาต์แบบบล็อก (ในเกือบทุกกรณี) กําหนดให้เครื่องมือใช้เลย์เอาต์สําหรับโฆษณาย่อยทั้งหมดเพียงครั้งเดียวเท่านั้น วิธีนี้เป็นผลดีต่อประสิทธิภาพ แต่สุดท้ายแล้วต้องไม่แสดงออกอย่างที่นักพัฒนาเว็บต้องการ
เช่น คุณต้องการให้เด็กทั้งหมดขยายเป็นขนาดใหญ่สุด เลย์เอาต์หลัก (เฟล็กซ์หรือตารางกริด) จะใช้ Measurement Pass เพื่อระบุว่าเด็กแต่ละคนมีขนาดเท่าใด จากนั้นจึงส่งเลย์เอาต์เพื่อขยายเด็กทั้งหมดให้มีขนาดเท่านี้ ลักษณะการทำงานนี้เป็นค่าเริ่มต้นสำหรับทั้งเลย์เอาต์แบบยืดหยุ่นและตารางกริด
เลย์เอาต์แบบ 2 ช่องสัญญาณนี้เป็นแบบประสิทธิภาพที่ยอมรับได้ เนื่องจากผู้ใช้มักไม่ได้ฝังเลย์เอาต์แบบนี้ไว้ลึกๆ อย่างไรก็ตาม เราเริ่มเห็นปัญหาด้านประสิทธิภาพที่สำคัญเมื่อมีเนื้อหาที่ซับซ้อนเกิดขึ้น ถ้าคุณไม่ได้แคชผลลัพธ์ของระยะการวัดผล แผนผังเลย์เอาต์จะแทรกระหว่างสถานะการวัดกับสถานะเลย์เอาต์สุดท้าย
ก่อนหน้านี้เราจะพยายามเพิ่มแคชที่เจาะจงมากๆ ลงในเลย์เอาต์แบบยืดหยุ่นและแบบตารางกริดเพื่อต่อสู้กับหน้าผาประสิทธิภาพประเภทนี้ วิธีนี้ได้ผล (และเราก้าวมาไกลมากด้วย Flex) แต่ก็ต้องต่อสู้กับข้อบกพร่องอยู่เรื่อยๆ โดยไม่ผ่านการแก้ไข
LayoutNG ช่วยให้เราสร้างโครงสร้างข้อมูลที่ชัดเจนสำหรับทั้งอินพุตและเอาต์พุตของเลย์เอาต์ และนอกเหนือจากนั้น เราได้สร้างแคชของการวัดผลและ Layout Pass แล้ว วิธีนี้กลับทำให้มีความซับซ้อนกลับคืนมา และส่งผลให้นักพัฒนาเว็บมีประสิทธิภาพในเชิงเส้นแบบคาดการณ์ได้ หากมีกรณีที่เลย์เอาต์ทำเลย์เอาต์แบบ 3 พาธ เราจะแคชข้อความนั้นไว้เช่นกัน ซึ่งอาจเปิดโอกาสในการนำโหมดเลย์เอาต์ขั้นสูงขึ้นอย่างปลอดภัยในตัวอย่างในอนาคตของการปลดล็อกการขยายการใช้งานของ RenderingNG อย่างเป็นพื้นฐาน ในบางกรณีเลย์เอาต์แบบตารางกริดอาจต้องใช้เลย์เอาต์แบบ 3 พาส แต่ขณะนี้เกิดขึ้นไม่บ่อยนัก
เราพบว่าเมื่อนักพัฒนาซอฟต์แวร์พบปัญหาด้านประสิทธิภาพโดยเฉพาะกับเลย์เอาต์ โดยปกติแล้วจะเกิดจากข้อบกพร่องด้านเวลาเลย์เอาต์แบบเอ็กซ์โปเนนเชียลมากกว่าอัตราการส่งข้อมูลดิบของขั้นตอนเลย์เอาต์ของไปป์ไลน์ หากการเปลี่ยนแปลงทีละน้อย (องค์ประกอบ 1 รายการที่เปลี่ยนพร็อพเพอร์ตี้ CSS รายการเดียว) ทำให้เลย์เอาต์ยาว 50-100 มิลลิวินาที ก็น่าจะเป็นข้อบกพร่องของเลย์เอาต์แบบเอ็กซ์โปเนนเชียล
ข้อมูลสรุป
เลย์เอาต์เป็นพื้นที่ที่ซับซ้อนมากๆ และเราไม่ได้ครอบคลุมรายละเอียดที่น่าสนใจทุกประเภท เช่น การเพิ่มประสิทธิภาพเลย์เอาต์แบบอินไลน์ (หรือก็คือการทำงานของระบบย่อยแบบแทรกในบรรทัดและข้อความทั้งหมด) และแม้แต่แนวคิดที่พูดถึงในจุดนี้ก็เป็นแค่เพียงการขูดพื้นเท่านั้น อย่างไรก็ตาม หวังว่าเราได้แสดงให้เห็นแล้วว่าการปรับปรุงสถาปัตยกรรมของระบบอย่างเป็นระบบสามารถทำให้เกิดประโยชน์มากกว่าในระยะยาวได้อย่างไร
แต่เรารู้ว่ายังมีงานอีกมากมายที่รออยู่ข้างหน้า เราทราบถึงคลาสของปัญหา (ทั้งด้านประสิทธิภาพและความถูกต้อง) ที่เรากำลังแก้ไขอยู่ และรู้สึกตื่นเต้นกับฟีเจอร์การออกแบบใหม่ๆ ที่จะเพิ่มเข้ามาใน CSS เราเชื่อว่าสถาปัตยกรรมของ LayoutNG จะช่วยให้การแก้ปัญหาเหล่านี้ปลอดภัยและควบคุมได้
รูปภาพ 1 รูป (คุณรู้ไหมว่าอันไหน!) โดย Una Kravets