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

إيان كيلباتريك
إيان كيلباتريك
كوجي إيشي
كوجي إيشي

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

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

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

عرض 30,000 قدم لبنية المحركات ذات التصميم

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

تعرض الشجرة كما هو موضح في النص التالي.

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

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

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

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

يشير ذلك المصطلح إلى النموذج المفاهيمي الذي تم وصفه سابقًا.

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

شجرة الكسر.

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

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

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

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

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

التصحيح

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

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

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

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

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

عدم صحة الصلاحية

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

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

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

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

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

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

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

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

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

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

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، ونظرًا لوجود هياكل بيانات واضحة للإدخالات والمخرجات، ولا يُسمح بالوصول إلى الحالة السابقة، فقد خفّفنا على نطاق واسع هذه الفئة من الأخطاء من نظام التخطيط.

الاستخدام الزائد عن الحد والأداء

وهذا هو عكس فئة الأخطاء التي لا يُسمَح باستخدامها بشكل كافٍ. غالبًا ما يؤدي إصلاح خطأ عدم وجود أسماء إلى حدوث خفض في الأداء.

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

ارتفاع مستويات التصميمات ذات الممرَّين والمنحدرات الخاصة بالأداء

يمثل التخطيط المرن والشبكة تغييرًا في طريقة التعبير عن التخطيطات على الويب. ومع ذلك، كانت هذه الخوارزميات مختلفة جذريًا عن خوارزمية تخطيط الكتل التي جاءت قبلها.

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

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

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

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

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

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

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

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

الملخّص

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

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

صورة واحدة (لا شكّ في ذلك) من إبداع "أونا كرافيتس"