أنا إيان كيلباتريك، قائدًا هندسيًا في فريق تخطيط Blink، جنبًا إلى جنب مع كوجي إيشي. قبل العمل ضمن فريق Blink، كنت مهندس واجهة أمامية (قبل أن يصبح لدى Google دور "مهندس الواجهة الأمامية")، إنشاء ميزات في "مستندات Google" وDrive وGmail. وبعد حوالي خمس سنوات في هذا الدور، قمت بمقامرة كبيرة والتحول إلى فريق Blink، تعلم لغة C++ بفعالية أثناء العمل، ومحاولة استخدام قاعدة رموز Blink المعقدة على نطاق واسع. حتى اليوم، ما زلت أفهم جزءًا صغيرًا نسبيًا منها. أنا ممتنّ جدًا على الوقت الذي خصّصتُه خلال هذه الفترة. شعرت بالارتياح لحقيقة أن الكثير من "استعادة مهندسي الواجهة الأمامية" تحولت إلى منصب "مهندس متصفح" قبلي.
لقد وجّهتني تجربتي السابقة شخصيًا أثناء العمل في فريق Blink. بصفتي مهندس واجهة أمامية، واجهت باستمرار تناقضات في المتصفح، ومشاكل في الأداء وأخطاء العرض والميزات المفقودة. كانت LayoutNG فرصة بالنسبة لي للمساعدة في إصلاح هذه المشكلات بشكل منهجي ضمن نظام تخطيط Blink، ويمثل مجموع العديد من المهندسين على مر السنين.
في هذه المشاركة، سأشرح كيف يمكن لتغيير كبير مثل هذا أن يقلل ويخفف من أنواع مختلفة من الأخطاء ومشكلات الأداء.
عرض 30,000 قدم لهياكل محركات التخطيطات
في السابق، كانت شجرة تخطيط Blink هي ما سأشير إليه باسم "شجرة قابلة للتغيير".
احتوى كل كائن في شجرة التخطيط على معلومات الإدخال، مثل الحجم المتاح الذي يفرضه أحد الوالدَين، وموضع أي أعداد عشرية ومعلومات الناتج، على سبيل المثال، العرض النهائي والارتفاع للكائن أو موضعه x وy.
وكان يتم الاحتفاظ بهذه العناصر بين عمليات العرض. فعند حدوث تغيير في النمط، وضعنا علامة على هذا الكائن على أنه غير نظيف وكذلك جميع والديه في الشجرة. وعندما تم تشغيل مرحلة التخطيط لمسار العرض، وسنقوم بعد ذلك بتنظيف الشجرة، ونسير أي أجسام متسخة، ثم تشغيل التخطيط لإخراجها إلى حالة نظيفة.
وجدنا أن هذه البنية قد نتج عنها فئات عديدة من المشاكل، والتي سنقوم بوصفها أدناه. لكن أولاً، لنعد إلى الوراء ونفكر في مدخلات ومخرجات التخطيط.
نظريًا يؤدي تشغيل التنسيق على عقدة في هذه الشجرة إلى الحصول على "Style plus DOM". وأي قيود رئيسية من نظام التخطيط الأصلي (الشبكة أو الكتلة أو المرونة)، تقوم بتشغيل خوارزمية قيد التخطيط، وينتج عنها نتيجة.
تضفي بنيتنا الأساسية الجديدة طابعًا رسميًا على هذا النموذج المفاهيمي. لا يزال لدينا شجرة التخطيط، ولكننا نستخدمها بشكل أساسي للاحتفاظ بمدخلات ومخرجات التخطيط. بالنسبة إلى الناتج، ننشئ كائنًا جديدًا تمامًا immutable يُسمى immutable.
لقد تناولت شجرة أجزاء غير قابلة للتغيير في السابق، ويصف كيف تم تصميمها لإعادة استخدام أجزاء كبيرة من الشجرة السابقة في التخطيطات التزايدية.
بالإضافة إلى ذلك، نخزن كائن القيود الأصلي الذي أنشأ هذا الجزء. ونستخدم هذا المفتاح كمفتاح تخزين مؤقت والذي سنناقشه بمزيد من التفصيل في ما يلي.
تتم أيضًا إعادة كتابة خوارزمية التخطيط المضمّن (النصي) لمطابقة البنية الجديدة غير القابلة للتغيير. فهي لا تؤدي فقط إلى إنتاج تمثيل قائمة مسطحة غير قابلة للتغيير للتنسيق المضمّن، ولكنها تتميّز أيضًا بالتخزين المؤقت على مستوى الفقرة لإجراء ترحيل أسرع، والشكل لكل فقرة لتطبيق ميزات الخط على العناصر والكلمات، خوارزمية يونيكود جديدة ثنائية الاتجاه باستخدام وحدة المعالجة المركزية (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();
}
وعادةً ما يؤدي إصلاح هذا النوع من المشكلات إلى حدوث تراجع كبير في الأداء، (انظر أدناه حول التعويض المفرط)، وكان من الصعب جدًا تصحيحه.
لدينا اليوم (كما هو موضح أعلاه) كائن قيود رئيسية غير قابل للتغيير يصف جميع الإدخالات من التنسيق الرئيسي إلى العنصر الثانوي. نخزن هذا مع الجزء غير القابل للتغيير. لهذا السبب، لدينا مكان مركزي حيث نفرق بين هذين المدخلين لتحديد ما إذا كان الطفل بحاجة إلى إجراء تمرير تنسيق آخر أم لا. وهذا المنطق المختلف معقّد، لكنه مكتمِل. يؤدي تصحيح أخطاء هذه الفئة من مشاكل عدم الصلاحية عادةً إلى فحص المُدخلَين يدويًا. وتحديد ما تم تغييره في الإدخال بحيث يكون هناك حاجة إلى تخطيط آخر.
وعادةً ما تكون إصلاحات رمز الاختلاف هذه بسيطة، وقابلة للاختبار بسهولة بسبب بساطة إنشاء هذه العناصر المستقلة.
![مقارنة صورة بعرض ثابت ونسبة مئوية للعرض.](https://developer.chrome.com/static/docs/chromium/layoutng/image/comparing-fixed-width-p-d298e82695e5e.png?authuser=7&hl=ar)
رمز الاختلاف للمثال أعلاه هو:
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 بين قيمتين. ومع ذلك، ينتج عن هذا "النمو اللانهائي" مستطيل.
باستخدام شجرتنا القابلة للتغيير السابقة، كان من السهل للغاية إدخال أخطاء كهذه. إذا أخطأت الكود في قراءة حجم أو موضع كائن في الوقت أو المرحلة غير الصحيحة (لأننا لم "نمحو" الحجم أو الموضع السابق على سبيل المثال)، فإننا نضيف على الفور خطأ تداخل دقيق. لا تظهر هذه الأخطاء عادةً أثناء الاختبار حيث تركّز معظم الاختبارات على تنسيق وعرض واحد. ومما يثير القلق أكثر من ذلك، أننا عرفنا أن بعض هذا التباطؤ مطلوب لكي تعمل بعض أوضاع التخطيط بشكل صحيح. كانت لدينا أخطاء تمكّننا فيها من إجراء تحسين لإزالة بطاقة تخطيط، ولكن أدخل "خطأ" لأن وضع التخطيط يتطلب عمليتي تمرير للحصول على الإخراج الصحيح.
![شجرة توضح المشكلات الموضحة في النص السابق.](https://developer.chrome.com/static/docs/chromium/layoutng/image/a-tree-demonstrating-pro-9c56bc875a773.png?authuser=7&hl=ar)
باستخدام LayoutNG، نظرًا لأن لدينا هياكل بيانات المدخلات والمخرجات الصريحة، غير مسموح بالوصول إلى الحالة السابقة، فقد خفّفنا على نطاق واسع من فئة الخطأ هذه من نظام التخطيط.
الإفراط في الصلاحية والأداء
وهذا هو العكس المباشر لفئة الأخطاء غير الصالحة. في كثير من الأحيان، عندما نصلح خطأ يظهر فيه خطأ عدم صلاحية الرمز، نطرح حافة في الأداء.
وكثيرًا ما كان علينا اتخاذ خيارات صعبة لصالح الصواب على الأداء. في القسم التالي، سنشرح كيفية تخفيف هذه الأنواع من مشاكل الأداء بشكل أكثر تفصيلاً.
ارتفاع مستويات التصميم المزدوج ومنحدرات الأداء
يمثل التخطيط المرن والشبكي تحولاً في التعبير عن التخطيطات على الويب. ومع ذلك، كانت هذه الخوارزميات مختلفة بشكل أساسي عن خوارزمية تخطيط الحظر التي سبقتها.
يتطلب تخطيط الحظر (في جميع الحالات تقريبًا) أن يقوم المحرك بتنفيذ التخطيط على جميع عناصره الثانوية مرة واحدة فقط. وهذه طريقة رائعة لتحسين الأداء، لكنّها في النهاية لا تكون معبّرة بالقدر الذي يريده مطوّرو البرامج على الويب.
على سبيل المثال: أنت تريد في كثير من الأحيان أن يتسع حجم جميع الأطفال إلى حجم الأكبر. لدعم ذلك، يجب استخدام التنسيق الرئيسي (مرن أو شبكي) ويقوم بتنفيذ ممر قياس لتحديد حجم كل من الأطفال، ثم ممر تخطيط لتوسيع جميع الأطفال إلى هذا الحجم. هذا السلوك هو الإعداد التلقائي لكل من التنسيق المرن والتنسيق الشبكي.
كانت هذه التخطيطات المكوّنة من تمريرتين مقبولة في البداية من حيث الأداء، لأن الناس عادة لم يدمجونها بعمق. ومع ذلك، بدأنا نشهد مشاكل كبيرة في الأداء مع ظهور محتوى أكثر تعقيدًا. إذا لم تقم بتخزين نتيجة مرحلة القياس مؤقتًا، ستمزج شجرة التنسيق بين حالة القياس وحالة التنسيق النهائية.
![التخطيطات واحد واثنين وثلاث تمريرات موضحة في التسمية التوضيحية.](https://developer.chrome.com/static/docs/chromium/layoutng/image/the-two-three-pass-la-d50559550340a.png?authuser=7&hl=ar)
<div>
.
سيزور التخطيط البسيط أحادي التمرير (مثل تخطيط الكتلة) ثلاث عقد تخطيط (تعقيد O(n)).
ومع ذلك بالنسبة للتخطيط ثنائي المرور (مثل المرن أو الشبكة)،
وقد يؤدي ذلك إلى تعقيد زيارة (ن) لهذا المثال.
![رسم بياني يوضح الزيادة الأسّية في وقت التنسيق](https://developer.chrome.com/static/docs/chromium/layoutng/image/graph-showing-exponentia-6d6966fa98b0e.png?authuser=7&hl=ar)
سنحاول سابقًا إضافة ذاكرات تخزين مؤقتة محددة للغاية إلى التنسيق المرن والشبكي لمواجهة هذا النوع من مخاطر الأداء. وقد نجح هذا الأمر (وقد حققنا نجاحًا كبيرًا باستخدام Flex)، ولكنهم كانوا يكافحون باستمرار مع أخطاء عدم صلاحية متخلفة.
يتيح لنا LayoutNG إنشاء هياكل بيانات واضحة لكل من مدخل ومخرجات التخطيط، وفوق كل ذلك أنشأنا ذاكرات تخزين مؤقتة لبطاقات القياس والمخططات. يؤدي هذا إلى إعادة التعقيد إلى O(n)، ما ينتج عنه أداء خطي متوقع لمطوّري البرامج على الويب. إذا كانت هناك حالة يقوم فيها التخطيط بعمل تخطيط ثلاثي التمرير، فسنقوم ببساطة بتخزين هذا المرور مؤقتًا أيضًا. قد يتيح ذلك فرصًا لتقديم أوضاع تخطيط أكثر تقدّمًا بأمان في المستقبل، كمثال على كيفية استخدام RenderingNG بشكل أساسي إمكانية التوسُّع على جميع الأجهزة. في بعض الحالات، يمكن أن يتطلب تخطيط الشبكة تخطيطات لثلاث تمريرات، ولكنها نادر جدًا في الوقت الحالي.
نجد أنه عندما يواجه المطورون مشكلات في الأداء تحديدًا مع التخطيط، عادة ما يرجع ذلك إلى خطأ وقت تخطيطي أسي بدلاً من سرعة معالجة البيانات الأولية لمرحلة التخطيط لمسار العملية. إذا أدى تغيير تدريجي صغير (أحد العناصر التي تغيِّر خاصية css واحدة) إلى تنسيق يتراوح بين 50 و100 ملي ثانية، ومن المحتمل أن يكون هذا خطأ تخطيطيًا أسيًا.
الملخّص
يُعد التخطيط منطقة معقدة للغاية، ولم نتناول جميع أنواع التفاصيل المهمة مثل تحسينات التنسيق المضمّن (في الواقع، كيف يعمل النظام الفرعي المضمّن والنصي بالكامل)، وحتى المفاهيم التي تم التحدث عنها هنا تخطت فقط تقريبًا، وإبرازه على العديد من التفاصيل. مع ذلك، نأمل أن نكون قد أظهرنا كيف يمكن أن يؤدي التحسين المنتظم لبنية النظام إلى مكاسب ضخمة على المدى الطويل.
ومع ذلك، نعلم أنّ أمامنا الكثير من العمل. نحن على دراية بفئات المشكلات (كل من الأداء والدقة) التي نعمل على حلها، ونحن متحمّسون بشأن ميزات التنسيق الجديدة التي سنطرحها في CSS. ونعتقد أنّ بنية LayoutNG تجعل حل هذه المشكلات آمنًا وسهلاً.
صورة واحدة (لا أحد يعرف الصورة) من تصميم "أونا كرافيتس"