ผมชื่อ 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 กำหนดสัญญาระหว่างองค์ประกอบทั้งหมดในระบบการออกแบบอย่างชัดเจนแล้ว เราพบว่าเราสามารถนำการเปลี่ยนแปลงมาใช้ได้อย่างมั่นใจมากขึ้น เรายังได้ประโยชน์อย่างมากจากโครงการ 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 รายการนี้เพื่อพิจารณาว่าเด็กจำเป็นต้องทำผ่านการจัดวางอีกครั้งหรือไม่ ตรรกะการเปรียบเทียบนี้มีความซับซ้อน แต่ควบคุมได้ดี การแก้ไขข้อบกพร่องของปัญหาที่ไม่ถูกต้องระดับนี้มักได้ผลในการตรวจสอบอินพุตทั้ง 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 และตารางกริด
เลย์เอาต์แบบ 2 ผ่านเหล่านี้มีประสิทธิภาพที่ยอมรับได้ในช่วงแรก เนื่องจากโดยทั่วไปแล้วผู้ใช้ไม่ได้ฝังเลย์เอาต์เหล่านี้ไว้ลึกมาก อย่างไรก็ตาม เราเริ่มพบปัญหาด้านประสิทธิภาพที่สำคัญเมื่อเนื้อหามีความซับซ้อนมากขึ้น หากคุณไม่แคชผลลัพธ์ของระยะการวัด ต้นไม้เลย์เอาต์จะสลับไปมาระหว่างสถานะการวัดกับสถานะเลย์เอาต์สุดท้าย
ก่อนหน้านี้ เราพยายามเพิ่มแคชที่เฉพาะเจาะจงมากๆ ในเลย์เอาต์แบบเฟล็กซ์และตารางกริดเพื่อต่อสู้กับหน้าผาด้านประสิทธิภาพประเภทนี้ วิธีนี้ได้ผล (และเราก็ไปได้ไกลมากกับ Flex) แต่ก็ยังคงต่อสู้กับข้อบกพร่องที่ทำให้เป็นโมฆะไม่หยุดนิ่ง
LayoutNG ช่วยให้เราสร้างโครงสร้างข้อมูลที่ชัดเจนสำหรับทั้งอินพุตและเอาต์พุตของเลย์เอาต์ และนอกจากนี้ เรายังได้สร้างแคชของค่าการวัดและเลย์เอาต์ด้วย ซึ่งทำให้ความซับซ้อนกลับมาเป็น O(n) อีกครั้ง ส่งผลให้นักพัฒนาเว็บคาดการณ์ประสิทธิภาพแบบเชิงเส้นได้ หากมีกรณีที่เลย์เอาต์ใช้เลย์เอาต์ 3 ผ่าน เราจะแคชผ่านนั้นด้วย ซึ่งอาจเป็นโอกาสในการนำโหมดเลย์เอาต์ขั้นสูงมาใช้อย่างปลอดภัยในตัวอย่างในอนาคตว่า RenderingNG จะปลดล็อกความสามารถในการขยายการใช้งานอย่างเต็มรูปแบบได้อย่างไร ในบางกรณี เลย์เอาต์แบบตารางกริดอาจต้องใช้เลย์เอาต์แบบ 3 จุด แต่ปัจจุบันไม่ค่อยพบมากนัก
เราพบว่าเมื่อนักพัฒนาซอฟต์แวร์พบปัญหาด้านประสิทธิภาพเกี่ยวกับเลย์เอาต์โดยเฉพาะ ปัญหานั้นมักเกิดจากข้อบกพร่องเกี่ยวกับเวลาในการแสดงผลแบบทวีคูณ ไม่ใช่ปริมาณข้อมูลที่ส่งผ่านขั้นต้นของระยะการแสดงผลของไปป์ไลน์ หากการเปลี่ยนแปลงเล็กๆ น้อยๆ (องค์ประกอบหนึ่งๆ เปลี่ยนแปลงพร็อพเพอร์ตี้ CSS รายการเดียว) ส่งผลให้เลย์เอาต์ใช้เวลา 50-100 มิลลิวินาที แสดงว่านี่อาจเป็นข้อบกพร่องของเลย์เอาต์แบบทวีคูณ
ข้อมูลสรุป
เลย์เอาต์เป็นพื้นที่ที่มีความซับซ้อนอย่างมาก และเราไม่ได้กล่าวถึงรายละเอียดที่น่าสนใจทุกประเภท เช่น การเพิ่มประสิทธิภาพเลย์เอาต์ในบรรทัด (วิธีการทำงานของระบบย่อยแบบอินไลน์และแบบข้อความทั้งหมด) และแม้แต่แนวคิดที่พูดถึงที่นี่ก็เป็นเพียงข้อมูลเบื้องต้นเท่านั้น และไม่ได้กล่าวถึงรายละเอียดมากมาย อย่างไรก็ตาม เราหวังว่าได้แสดงให้เห็นว่าการปรับปรุงสถาปัตยกรรมของระบบอย่างเป็นระบบจะทําให้เกิดผลกำไรที่เหนือกว่าในระยะยาวได้อย่างไร
อย่างไรก็ตาม เรารู้ว่ายังมีงานอีกมากที่ต้องทำ เราทราบถึงปัญหาหลายประเภท (ทั้งด้านประสิทธิภาพและความถูกต้อง) ที่เรากำลังพยายามแก้ไขอยู่ และตื่นเต้นกับฟีเจอร์การจัดวางใหม่ที่จะเข้ามาใน CSS เราเชื่อว่าสถาปัตยกรรมของ LayoutNG จะช่วยแก้ปัญหาเหล่านี้ได้อย่างปลอดภัยและจัดการได้
รูปภาพ 1 รูป (คุณรู้อยู่แล้วว่ารูปภาพไหน) โดย Una Kravets