البحث التفصيلي عن RenderingNG: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

اسمي "إيان كيلباتريك"، وأنا رئيس قسم الهندسة في فريق تنسيق Blink، إلى جانب "كوجي إيشي". قبل العمل في فريق Blink، كنت مهندسًا في واجهة المستخدم (قبل أن تضيف Google دور "مهندس واجهة المستخدم")، وكنت أطوّر ميزات في "مستندات Google" وDrive وGmail. وبعد حوالي خمس سنوات في هذا الدور، انطلقت مقامرة كبيرة إلى فريق Blink، وتعلمت C++ بشكل فعّال أثناء العمل، وأحاول استخدام قاعدة رموز Blink المعقدة على نطاق واسع. وحتى اليوم، لا أفهم سوى جزء صغير نسبيًا منه. أُقدّر الوقت الذي تم منحه لي خلال هذه الفترة. شعرت بالرضا عن حقيقة انتقال الكثير من "مهندسي الواجهة الأمامية" الذين تحولوا إلى أن أصبح "مهندس متصفح" قبلي.

لقد ساعدتني تجربتي السابقة شخصيًا أثناء عملي في فريق Blink. بصفتي مهندسًا في واجهة المستخدم، كنت أواجه باستمرار مشاكل في المتصفحات و مشاكل في الأداء وأخطاء في العرض وميزات غير متوفّرة. وقد وفّرت لي أداة LayoutNG فرصة للمساعدة في حلّ هذه المشاكل بشكل منهجي ضمن نظام تنسيق Blink، وهي تمثّل مجموع جهود العديد من المهندسين على مرّ السنين.

في هذه المشاركة، سأوضّح كيف يمكن أن يؤدي تغيير كبير في البنية مثل هذا إلى تقليل أنواع مختلفة من الأخطاء ومشاكل الأداء والحدّ منها.

عرض من ارتفاع 30,000 قدم لتصاميم محرّكات التنسيق

في السابق، كانت شجرة تنسيق Blink تُعرف باسم "الشجرة القابلة للتغيير".

تعرِض هذه الطريقة العرض التدرّجي كما هو موضّح في النص التالي.

يحتوي كل عنصر في شجرة التنسيق على معلومات إدخال، مثل الحجم المتاح الذي يفرضّه العنصر الرئيسي، وموضع أيّ عناصر تطفو على سطح الصفحة، ومعلومات الإخراج، مثل العرض والارتفاع النهائيَين للعنصر أو موضعَي x وy.

تم الاحتفاظ بهذه العناصر بين عمليات التصدير. عند حدوث تغيير في النمط، وضعنا علامة على هذا العنصر بأنّه غير محدَّث، وكذلك على جميع العناصر الرئيسية له في الشجرة. عند تشغيل مرحلة التنسيق في مسار العرض، كنا نُجري عملية تنظيف للشجرة، ونزيل أي عناصر غير مرتبة، ثم نُجري عملية تنسيق لترتيبها.

وقد وجدنا أن هذه البنية قد نتج عنها فئات عديدة من المشاكل، وسنوضحها أدناه. لكن أولاً، لنعد إلى الوراء ونفكر في مدخلات ومخرجات التخطيط.

عند تنفيذ التنسيق على عقدة في هذه الشجرة، يتمّ بشكلٍ مفاهيمي استخدام "الأسلوب بالإضافة إلى نموذج DOM"، وأيّ قيود رئيسية من نظام التنسيق الرئيسي (شبكة أو كتلة أو مرونة)، وتنفيذ خوارزمية قيود التنسيق، وتحقيق نتيجة.

النموذج المفاهيمي الموضّح سابقًا

وتُضفي البنية الجديدة طابعًا رسميًا على هذا النموذج المفاهيمي. لا يزال لدينا شجرة التخطيط، ولكننا نستخدمها بشكل أساسي للاحتفاظ بمدخلات ومخرجات التخطيط. بالنسبة إلى الإخراج، ننشئ عنصرًا جديدًا تمامًا غير قابل للتغيير يُعرف باسم شجرة الأجزاء.

شجرة الأجزاء

لقد تناولتُ سابقًا شجرة الأجزاء غير القابلة للتغيير، وأوضّح كيفية تصميمها لإعادة استخدام أجزاء كبيرة من الشجرة السابقة للحصول على تنسيقات إضافية.

بالإضافة إلى ذلك، نخزّن كائن قيود العنصر الرئيسي الذي أنشأ هذا المقتطف. نستخدم هذا الرمز كمفتاح لذاكرة التخزين المؤقت، وسنناقش المزيد حول ذلك أدناه.

تمت إعادة كتابة خوارزمية تنسيق النص المضمّن أيضًا لتتلاءم مع البنية الجديدة غير القابلة للتغيير. فهو لا يوفّر تمثيل قائمة مسطحة غير قابل للتغيير فقط للتنسيق المضمّن، بل يتضمّن أيضًا التخزين المؤقت على مستوى الفقرة لنقل البيانات بشكل أسرع، وأشكالاً للفقرة لتطبيق ميزات الخط على العناصر والكلمات، وخوارزمية جديدة ثنائية الاتجاه يونيكود تستخدم وحدة المعالجة المركزية (ICU)، والكثير من إصلاحات الصحة، وغير ذلك الكثير.

أنواع أخطاء التنسيق

تنقسم أخطاء التخطيط على نطاق واسع إلى أربع فئات مختلفة، لكل منها أسبابها الأساسية المختلفة.

الصحة

عندما نفكر في الأخطاء في نظام العرض، نفكر عادةً في صحته، على سبيل المثال: "المتصفح "أ" يعرض السلوك "س"، بينما يعرض المتصفح "ب" السلوك "ص""، أو "المتصفحان "أ" و"ب" معطّلان". في السابق، كنا نقضي الكثير من الوقت في العمل، وفي هذه العملية كنا نتصدّى باستمرار لهذا النظام. كان أحد أسباب الأعطال الشائعة هو تطبيق حلّ مستهدف للغاية لخطأ معيّن، ولكن تبيّن لنا بعد أسابيع أنّنا تسببنا في حدوث تراجع في جزء آخر (يبدو أنّه غير مرتبط) من النظام.

كما هو موضّح في المشاركات السابقة، يشير ذلك إلى أنّ النظام غير متّسق. وبالنسبة إلى التصميم تحديدًا، لم يكن لدينا عقد موحّد بين جميع الفئات، ما جعل مهندسي المتصفّح يعتمدون على تحديد الخيار الذي لا ينبغي لهم إدراجه، أو يسيءون تفسير قيمة معيّنة من جزء آخر من النظام.

على سبيل المثال، واجهنا في مرحلة معيّنة سلسلة مؤلفة من 10 أخطاء تقريبًا على مدار أكثر من عام مرتبط بالتصميم المرن. وقد تسبّب كل إصلاح في حدوث مشكلة في الصحة أو الأداء في جزء من النظام، ما أدى إلى حدوث خطأ آخر.

الآن بعد أن حدّد LayoutNG بوضوح العلاقة بين جميع المكوّنات في نظام التنسيق، تبيّن لنا أنّه يمكننا تطبيق التغييرات بثقة أكبر بكثير. كما نستفيد أيضًا بشكل كبير من المشروع الممتاز Web Platform Tests (WPT) الذي يسمح لعدة أطراف بالمساهمة في مجموعة مشتركة من أدوات اختبار الويب.

تبيّن لنا اليوم أنّه في حال طرحنا إصدارًا يتضمّن تراجعًا في الأداء على قناتنا الثابتة، لا يكون عادةً مرتبطًا باختبارات في مستودع WPT، ولا يكون ناتجًا عن سوء فهم لعقود المكوّنات. علاوةً على ذلك، وكجزء من سياسة إصلاح الأخطاء، نضيف دائمًا اختبار WPT جديدًا، لضمان عدم ارتكاب أي متصفح للخطأ نفسه مرة أخرى.

القيمة غير الصالحة

إذا واجهت خطأً غامضًا يؤدّي فيه تغيير حجم نافذة المتصفّح أو تبديل إحدى خصائص CSS إلى التخلص من الخطأ بشكل سحري، تكون قد واجهت مشكلة عدم إلغاء الصلاحية. تم اعتبار جزء من الشجرة القابلة للتغيير نظيفًا بشكلٍ فعّال، ولكن بسبب بعض التغييرات في قيود العنصر الرئيسي، لم يمثّل ذلك الجزء الناتج الصحيح.

وهذا شائع جدًا في أوضاع التنسيق التي تستخدِم تمريرةَين (التنقّل في شجرة التنسيق مرّتين لتحديد حالة التنسيق النهائية) والموضّحة أدناه. في السابق، كانت التعليمة البرمجية تظهر على النحو التالي:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

عادة ما يكون إصلاح هذا النوع من الأخطاء:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

سيؤدي حلّ هذا النوع من المشاكل عادةً إلى تراجع كبير في الأداء، (راجِع القسم "إبطال البيانات بشكل مفرط" أدناه)، وكان من الصعب جدًا تصحيحه.

في الوقت الحالي (كما هو موضّح أعلاه)، لدينا عنصر قيود رئيسية غير قابل للتغيير يصف جميع المدخلات من تنسيق العنصر الرئيسي إلى العنصر الفرعي. نخزّن هذا مع المقتطف الثابت الناتج. ولهذا السبب، لدينا مكان مركزي نُجري فيه مقارنة بين هذين الإدخالَين لتحديد ما إذا كان الطفل بحاجة إلى إجراء جولة أخرى لتنسيق المحتوى. إنّ منطق المقارنة هذا معقّد، ولكنه مُحاط بشكل جيد. يؤدي تصحيح أخطاء هذه الفئة من مشاكل عدم الإبطال إلى فحص الإدخالَين يدويًا وتحديد ما تغيّر في الإدخال بحيث يلزم إجراء جولة أخرى من التنسيق.

تكون الإصلاحات التي يتم إجراؤها على رمز التفريق هذا عادةً بسيطة، ويمكن اختبارها على مستوى الوحدة بسهولة بسبب بساطة إنشاء هذه العناصر المستقلة.

مقارنة صورة بعرض ثابت وصورة بعرض نسبة مئوية
لا يهتم عنصر العرض/الارتفاع الثابت إذا كان الحجم المتاح الذي تم منحه له يزداد، ولكن يهتم العرض/الارتفاع المستند إلى النسبة المئوية. يتم تمثيل سمة available-size في عنصر قيود العنصر الرئيسي، وسيتم إجراء هذا التحسين كجزء من خوارزمية المقارنة.

رمز الاختلاف للمثال أعلاه هو:

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 بين قيمتَين. ويؤدي ذلك إلى إنشاء مستطيل "ينمو بلا حدود".

يعرض الفيديو والالعرض التجريبي خطأ في الاستجابة في Chrome 92 والإصدارات الأقدم. ويتم إصلاحها في الإصدار 93 من Chrome.

مع شجرتنا السابقة القابلة للتغيير، كان من السهل جدًا إدخال أخطاء كهذه. فإذا أخطأ الرمز في قراءة حجم كائن أو موضعه في الوقت أو المرحلة غير الصحيحة (لأننا لم "نمحو" الحجم أو الموضع السابق على سبيل المثال)، فسنضيف على الفور خطأ تداخل دقيق. لا تظهر هذه الأخطاء عادةً في الاختبارات لأنّ معظم الاختبارات تركّز على تنسيق وعرض واحدَين. ومما يثير القلق أكثر من ذلك، أننا عرفنا أن بعض هذا التباطؤ مطلوب لكي تعمل بعض أوضاع التخطيط بشكل صحيح. كنّا نستخدم أخطاءً أثناء عملية التحسين من أجل إزالة تذكرة تصميم، ولكننا أشرنا إلى "خطأ" لأنّ وضع التنسيق يتطلّب عمليتي تمرير للحصول على النتيجة الصحيحة.

شجرة توضّح المشاكل الموضّحة في النص السابق
استنادًا إلى معلومات نتيجة التنسيق السابق، تؤدي إلى تنسيقات غير متماثلة

باستخدام LayoutNG، لدينا هياكل واضحة للبيانات والمخرجات، ومن خلال الوصول إلى الحالة السابقة، خفّفنا من حدة هذه الفئة من الأخطاء من نظام التخطيط.

الأداء وعمليات الإبطال المفرطة

وهذا هو العكس المباشر لفئة الأخطاء المتعلّقة بعدم الإبطال. غالبًا ما يؤدي إصلاح خطأ في عملية الإبطال إلى حدوث انخفاض حاد في الأداء.

وكثيرًا ما كان علينا اتخاذ خيارات صعبة لصالح الصواب على الأداء. في القسم التالي، سنوضّح بالتفصيل كيف خفّضنا هذه الأنواع من مشاكل الأداء.

زيادة استخدام التنسيقات ذات المرورَين وانخفاض الأداء

شكّل تنسيق Flex وGrid Layout نقلة نوعية في التعبير عن التنسيقات على الويب. ومع ذلك، كانت هذه الخوارزميات مختلفة بشكل أساسي عن خوارزمية تنسيق الكتل التي سبقتها.

لا يتطلّب تنسيق الكتل (في جميع الحالات تقريبًا) من المحرّك سوى تنفيذ التنسيق على جميع العناصر الثانوية مرة واحدة فقط. وهذا أمر رائع للأداء، ولكنّه لا يعبّر عن المعنى بقدر ما يريده مطوّرو الويب.

على سبيل المثال، غالبًا ما تريد توسيع حجم جميع الأطفال إلى حجم أكبر طفل. لدعم ذلك، سيُجري تنسيق العنصر الرئيسي (مرن أو شبكة) عملية قياس لتحديد حجم كل عنصر فرعي، ثم عملية تنسيق لتمديد جميع العناصر الفرعية إلى هذا الحجم. وهذا السلوك هو الإعداد التلقائي لكل من تنسيقات التخطيط المرن والشبكة.

مجموعتان من المربّعات، الأولى تعرض الحجم الأساسي للمربّعات في عملية القياس، والثانية في التنسيق تكون جميعها متساوية الارتفاع.

كانت تنسيقات المرورَين هذه مقبولةً في البداية من حيث الأداء، لأنّ المستخدمين لم يدمجوا هذه التنسيقات عادةً بشكل عميق. ومع ذلك، بدأنا نلاحظ مشاكل كبيرة في الأداء مع ظهور محتوى أكثر تعقيدًا. إذا لم تُخزِّن نتيجة مرحلة القياس، ستتأرجح شجرة التنسيق بين حالتها القياس وحالتها النهائية التنسيق.

توضيح التنسيقات ذات المرور الواحد والمرورَين والمرورَين الثلاث
في الصورة أعلاه، لدينا ثلاثة عناصر <div>. سينتقل تنسيق بسيط من خطوة واحدة (مثل تنسيق الكتل) إلى ثلاث عقد تنسيق (معيار التعقيد O(n)). في المقابل، بالنسبة إلى التنسيق الذي يتطلب مرورًا مرتين (مثل التنسيق المرن أو الشبكي)، قد يؤدي ذلك إلى تعقيد زيارات O(2n) في هذا المثال.
رسم بياني يعرض الزيادة المتسارعة في وقت التنسيق
تعرض هذه الصورة والالعرض التجريبي تنسيقًا متزايدًا مع تنسيق الشبكة. تم إصلاح هذه المشكلة في الإصدار 93 من Chrome نتيجة نقل Grid إلى البنية الجديدة.

سنحاول سابقًا إضافة ذاكرات تخزين مؤقتة محددة للغاية إلى التنسيق المرن والشبكي لمواجهة هذا النوع من مخاطر الأداء. وقد نجحت هذه الطريقة (وحققنا تقدمًا كبيرًا باستخدام Flex)، ولكننا كنا نواجه باستمرار أخطاء في عمليات الإبطال الناقصة أو الزائدة.

يتيح لنا LayoutNG إنشاء بنى بيانات صريحة لكلّ من إدخال التنسيق وإخراجه، بالإضافة إلى ذلك، أنشأنا ذاكرات تخزين مؤقتة لمرات قياس الأداء ومرات تطبيق التنسيق. يؤدي ذلك إلى إعادة التعقيد إلى O(n)، مما يؤدي إلى أداء خطي متوقّع لمطوّري الويب. إذا كانت هناك حالة يقوم فيها التخطيط بتخطيط ثلاثي التمرير، فسنقوم ببساطة بتخزين هذه الحالة مؤقتًا أيضًا. وقد يؤدي ذلك إلى توفير فرص لإدخال أوضاع تنسيق أكثر تقدمًا بأمان في المستقبل، ما يمثّل مثالاً على كيفية إتاحة إمكانيات التوسّع بشكل أساسي في RenderingNG على جميع المستويات. في بعض الحالات، قد يتطلّب تنسيق الشبكة استخدام تنسيقات ثلاثية المرور، ولكن هذا نادر جدًا في الوقت الحالي.

تبيّن لنا أنّه عندما يواجه المطوّرون مشاكل في الأداء في ما يتعلّق بالتنسيق على وجه التحديد، يعود ذلك عادةً إلى خطأ تصاعدي في وقت التنسيق بدلاً من معدل نقل البيانات الأوّلي لمرحلة التنسيق في مسار الإحالة الناجحة. إذا أدى تغيير تدريجي صغير (أحد العناصر التي تغيّر خاصية css واحدة) إلى تنسيق يتراوح بين 50 و100 ملي ثانية، فمن المحتمل أن يكون هذا خطأ تنسيقًا كبيرًا.

الملخّص

يُعدّ التنسيق مجالًا معقّدًا للغاية، ولم نتطرّق إلى جميع أنواع التفاصيل المثيرة للاهتمام، مثل تحسينات التنسيق المضمّن (أي آلية عمل النظام الفرعي المضمّن والنصي بالكامل)، وحتى المفاهيم التي تمّ التحدّث عنها هنا لم تتناول سوى الأساسيات، وتجاهلت العديد من التفاصيل. نأمل أن نكون قد أوضحنا كيف يمكن أن يؤدي تحسين بنية النظام بشكل منهجي إلى تحقيق مكاسب كبيرة على المدى الطويل.

مع ذلك، ندرك أنّنا ما زلنا بحاجة إلى بذل الكثير من الجهد. نحن على دراية بفئات من المشاكل (في كلّ من الأداء وصحة الرمز) نعمل على حلّها، ونتطلّع إلى ميزات التنسيق الجديدة التي ستتوفّر في CSS. نعتقد أنّ بنية LayoutNG تجعل حلّ هذه المشاكل آمنًا وسهلاً.

صورة واحدة (أنت تعرفها) من تصميم "أونا كرافيتس"