اسمي "إيان كيلباتريك"، وأنا رئيس قسم الهندسة في فريق تنسيق Blink، إلى جانب "كوجي إيشي". قبل العمل في فريق Blink، كنت مهندسًا في واجهة المستخدم (قبل أن تضيف Google دور "مهندس واجهة المستخدم")، وكنت أطوّر ميزات في "مستندات Google" وDrive وGmail. بعد حوالي خمس سنوات في هذا الدور، خضتُ مغامرة كبيرة من خلال الانتقال إلى فريق Blink، وتعلمتُ لغة C++ بشكل فعّال أثناء العمل، وحاولتُ تسريع وتيرة العمل على قاعدة بيانات Blink المعقدة للغاية. وحتى اليوم، لا أفهم سوى جزء صغير نسبيًا منه. أُقدّر الوقت الذي قضيته في هذه الفترة. لقد ساعدني كثيرًا معرفة أنّ الكثير من "مهندسي الواجهة الأمامية الذين يحاولون تغيير مجال عملهم" قد انتقلوا إلى مجال "مهندسي المتصفّحات" قبلي.
لقد ساعدتني تجربتي السابقة شخصيًا أثناء عملي في فريق Blink. بصفتي مهندسًا في واجهة المستخدم، كنت أواجه باستمرار مشاكل في المتصفحات و مشاكل في الأداء وأخطاء في العرض وميزات غير متوفّرة. وقد وفّرت لي أداة LayoutNG فرصة للمساعدة في حلّ هذه المشاكل بشكل منهجي ضمن نظام تنسيق Blink، وهي تمثّل مجموع جهود العديد من المهندسين على مرّ السنين.
في هذه المشاركة، سأوضّح كيف يمكن أن يؤدي تغيير كبير في البنية مثل هذا إلى تقليل أنواع مختلفة من الأخطاء ومشاكل الأداء والحدّ منها.
عرض من ارتفاع 30,000 قدم لتصاميم محرّكات التنسيق
في السابق، كانت شجرة تنسيق Blink تُعرف باسم "الشجرة القابلة للتغيير".
يحتوي كل عنصر في شجرة التنسيق على معلومات إدخال، مثل الحجم المتاح الذي يفرضّه العنصر الرئيسي، وموضع أيّ عناصر تطفو على سطح الصفحة، ومعلومات الإخراج، مثل العرض والارتفاع النهائيَين للعنصر أو موضعَي x وy.
تم الاحتفاظ بهذه العناصر بين عمليات التصدير. عند حدوث تغيير في النمط، وضعنا علامة على هذا العنصر بأنّه غير محدَّث، وكذلك على جميع العناصر الرئيسية له في الشجرة. عند تشغيل مرحلة التنسيق في مسار العرض، كنا نُجري عملية تنظيف للشجرة، ونزيل أي عناصر غير مرتبة، ثم نُجري عملية تنسيق لترتيبها.
تبيّن لنا أنّ هذه البنية أدّت إلى ظهور العديد من فئات المشاكل، وسنوضّح ذلك أدناه. أولاً، لنلقِ نظرة على مدخلات التنسيق ومخارجه.
عند تنفيذ التنسيق على عقدة في هذه الشجرة، يتمّ بشكلٍ مفاهيمي استخدام "الأسلوب بالإضافة إلى نموذج DOM"، وأيّ قيود رئيسية من نظام التنسيق الرئيسي (شبكة أو كتلة أو مرونة)، وتنفيذ خوارزمية قيود التنسيق، وتحقيق نتيجة.
وتُضفي بنيتنا الجديدة الطابع الرسمي على هذا النموذج المفاهيمي. لا تزال لدينا شجرة التنسيق، ولكنّنا نستخدمها في المقام الأول للاحتفاظ بمدخلات التنسيق ومخارجه. بالنسبة إلى الإخراج، ننشئ عنصرًا جديدًا تمامًا غير قابل للتغيير يُعرف باسم شجرة الأجزاء.
لقد غطّيت شجرة الأجزاء الثابتة سابقًا، ووصفت كيفية تصميمها لإعادة استخدام أجزاء كبيرة من الشجرة السابقة لتنسيقات متزايدة.
بالإضافة إلى ذلك، نخزّن عنصر القيود الرئيسية الذي أنشأ هذا المقتطف. نستخدم هذا الرمز كمفتاح لذاكرة التخزين المؤقت، وسنناقش المزيد حول ذلك أدناه.
تمت إعادة كتابة خوارزمية تنسيق النص المضمّن أيضًا لتتلاءم مع البنية الجديدة غير القابلة للتغيير. ولا ينتج هذا الأسلوب فقط تمثيلًا ثابتًا لقائمة مسطّحة للتنسيق المضمّن، بل يقدّم أيضًا ميزة التخزين المؤقت على مستوى الفقرة لإعادة التنسيق بشكل أسرع، وميزة تحديد شكل لكل فقرة لتطبيق ميزات الخط على العناصر والكلمات، وميزة خوارزمية جديدة ثنائية الاتجاه لـ Unicode باستخدام ICU، والعديد من الإصلاحات المتعلقة بالصحة، وغير ذلك.
أنواع أخطاء التنسيق
تندرج أخطاء التنسيق بشكل عام ضمن أربع فئات مختلفة، لكل منها أسباب أساسية مختلفة.
الصحة
عندما نفكر في الأخطاء في نظام العرض، نفكر عادةً في صحته، على سبيل المثال: "المتصفح "أ" يعرض السلوك "س"، بينما يعرض المتصفح "ب" السلوك "ص""، أو "المتصفحان "أ" و"ب" معطّلان". في السابق، كان هذا هو ما كنا نقضي الكثير من وقتنا عليه، وخلال هذه العملية، كنا نواجه مشاكل باستمرار مع النظام. كان أحد أسباب الأعطال الشائعة هو تطبيق حلّ مستهدف للغاية لخطأ معيّن، ولكن تبيّن لنا بعد أسابيع أنّنا تسببنا في حدوث تراجع في جزء آخر (يبدو غير مرتبط) من النظام.
كما هو موضّح في المشاركات السابقة، يشير ذلك إلى أنّ النظام غير متّسق. بالنسبة إلى التنسيق على وجه التحديد، لم يكن لدينا اتّفاقية واضحة بين أيّ فئات، مما جعل مهندسي المتصفّحات يعتمدون على حالة لا يُفترَض أن يعتمدوا عليها، أو يخطئون في تفسير بعض القيم من جزء آخر من النظام.
على سبيل المثال، في مرحلة ما، واجهنا سلسلة من 10 أخطاء تقريبًا على مدار أكثر من عام، كانت مرتبطة بتنسيق Flex. وقد أدّى كلّ حلّ إلى حدوث مشكلة في صحة البيانات أو الأداء في جزء من النظام، مما أدّى إلى ظهور خطأ آخر.
الآن بعد أن حدّد LayoutNG بوضوح العلاقة بين جميع المكوّنات في نظام التنسيق، تبيّن لنا أنّه يمكننا تطبيق التغييرات بثقة أكبر بكثير. نستفيد أيضًا بشكل كبير من مشروع اختبارات منصة الويب (WPT) الرائع، الذي يتيح لعدة جهات المساهمة في مجموعة اختبارات ويب مشتركة.
تبيّن لنا اليوم أنّه في حال طرحنا إصدارًا يتضمّن تراجعًا في الأداء على قناتنا الثابتة، لا يكون عادةً مرتبطًا باختبارات في مستودع WPT، ولا يكون ناتجًا عن سوء فهم لعقود المكوّنات. بالإضافة إلى ذلك، كجزء من سياسة إصلاح الأخطاء، نضيف دائمًا اختبار WPT جديدًا، ما يساعد في ضمان عدم ارتكاب أي متصفّح للخطأ نفسه مرة أخرى.
القيمة غير الصالحة
إذا واجهت خطأً غامضًا يؤدي فيه تغيير حجم نافذة المتصفّح أو تبديل إحدى سمات CSS إلى اختفاء الخطأ بشكلٍ سحري، هذا يعني أنّك واجهت مشكلة في عدم إبطال القيمة. تم اعتبار جزء من الشجرة القابلة للتغيير نظيفًا بشكلٍ فعّال، ولكن بسبب بعض التغييرات في قيود العنصر الرئيسي، لم يمثّل ذلك الجزء الإخراج الصحيح.
وهذا شائع جدًا في أوضاع التنسيق التي تستخدِم تمريرةَين (التنقّل في شجرة التنسيق مرّتين لتحديد حالة التنسيق النهائية) والموضّحة أدناه. في السابق، كانت التعليمة البرمجية تظهر على النحو التالي:
if (/* some very complicated statement */) {
child->ForceLayout();
}
عادةً ما يكون حلّ هذا النوع من الأخطاء على النحو التالي:
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
سيؤدي حلّ هذا النوع من المشاكل عادةً إلى تراجع كبير في الأداء، (راجِع القسم "إبطال البيانات بشكل مفرط" أدناه)، وكان من الصعب جدًا تصحيحه.
في الوقت الحالي (كما هو موضّح أعلاه)، لدينا عنصر قيود رئيسية غير قابل للتغيير يصف جميع الإدخالات من تنسيق العنصر الرئيسي إلى العنصر الفرعي. نخزّن هذا مع المقتطف الثابت الناتج. ولهذا السبب، لدينا مكان مركزي نُجري فيه diff بين هذين الإدخالَين لتحديد ما إذا كان الطفل بحاجة إلى إجراء جولة أخرى لتنسيق المحتوى. إنّ منطق المقارنة هذا معقّد، ولكنه مُحاط بشكل جيد. يؤدي تصحيح أخطاء هذه الفئة من مشاكل عدم الإبطال إلى فحص الإدخالَين يدويًا وتحديد ما تغيّر في الإدخال بحيث يلزم إجراء جولة أخرى من التنسيق.
عادةً ما تكون الإصلاحات التي يتم إجراؤها على رمز المقارنة بسيطة، ويمكن اختبارها على مستوى الوحدة بسهولة بسبب بساطة إنشاء هذه العناصر المستقلة.
رمز المقارنة للمثال أعلاه هو:
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 بين قيمتَين. ويؤدي ذلك إلى إنشاء مستطيل "ينمو بلا حدود".
باستخدام الشجرة القابلة للتغيير السابقة، كان من السهل جدًا إدخال أخطاء مثل هذه. إذا ارتكب الرمز الخطأ في قراءة حجم عنصر أو موضعه في الوقت أو المرحلة غير الصحيحَين (لأنّنا لم "نُفِّذ" الحجم أو الموضع السابقَين على سبيل المثال)، سنضيف على الفور خطأً بسيطًا في الاستجابة التفاضلية. لا تظهر هذه الأخطاء عادةً في الاختبارات لأنّ معظم الاختبارات تركّز على تنسيق وعرض واحدَين. والأمر الأكثر قلقًا هو أنّنا علمنا أنّ بعضًا من هذا التباطؤ كان مطلوبًا لتشغيل بعض أوضاع التنسيق بشكل صحيح. كانت لدينا أخطاء نُجري فيها عملية تحسين لإزالة خطوة تنسيق، ولكنّنا أدخلنا "خطأ" لأنّ وضع التنسيق يتطلّب خطوتَين للحصول على النتيجة الصحيحة.
باستخدام LayoutNG، نظرًا لأنّنا نمتلك بنى بيانات واضحة للدخل والخرج، ولا يُسمح بالوصول إلى الحالة السابقة، خفّضنا بشكلٍ كبير من هذه الفئة من الأخطاء في نظام التنسيق.
الأداء وعمليات الإبطال المفرطة
وهذا هو العكس المباشر لفئة الأخطاء المتعلّقة بعدم الإبطال. غالبًا ما يؤدّي إصلاح خطأ في عملية الإبطال إلى حدوث انخفاض حاد في الأداء.
وكثيرًا ما اضطررنا إلى اتخاذ خيارات صعبة تفضّل الدقة على الأداء. في القسم التالي، سنوضّح بالتفصيل كيف خفّضنا هذه الأنواع من مشاكل الأداء.
زيادة استخدام التنسيقات ذات المرورَين وانخفاض الأداء
شكّل تنسيق Flex وGrid Layout نقلة نوعية في التعبير عن التنسيقات على الويب. ومع ذلك، كانت هذه الخوارزميات مختلفة بشكل أساسي عن خوارزمية تنسيق الكتل التي سبقتها.
لا يتطلّب تنسيق الكتل (في جميع الحالات تقريبًا) من المحرّك تنفيذ التنسيق على جميع العناصر الثانوية مرة واحدة فقط. وهذا أمر رائع للأداء، ولكنّه لا يعبّر عن المعنى بقدر ما يريده مطوّرو الويب.
على سبيل المثال، غالبًا ما تريد توسيع حجم جميع الأطفال إلى حجم أكبر طفل. لدعم ذلك، سيُجري تنسيق العنصر الرئيسي (مرن أو شبكة) عملية قياس لتحديد حجم كل عنصر فرعي، ثم عملية تنسيق لتمديد جميع العناصر الفرعية إلى هذا الحجم. هذا السلوك هو الإعداد التلقائي لكل من تنسيقات التخطيط المرن والشبكة.
كانت تنسيقات المرورَين هذه مقبولةً في البداية من حيث الأداء، لأنّ المستخدمين لم يدمجوا هذه التنسيقات عادةً بشكلٍ عميق. ومع ذلك، بدأنا نلاحظ مشاكل كبيرة في الأداء مع ظهور محتوى أكثر تعقيدًا. إذا لم تُخزِّن نتيجة مرحلة القياس، ستتأرجح شجرة التنسيق بين حالتها القياس وحالتها النهائية التنسيق.
في السابق، كنا نحاول إضافة ذاكرات تخزين مؤقتة محدّدة جدًا إلى تنسيقات الشبكة المرنة والشبكة من أجل معالجة هذا النوع من الانخفاض المفاجئ في الأداء. وقد نجحت هذه الطريقة (وحققنا تقدمًا كبيرًا باستخدام Flex)، ولكننا كنا نواجه باستمرار أخطاء في عمليات الإبطال الناقصة أو الزائدة.
يتيح لنا LayoutNG إنشاء بنى بيانات صريحة لكلّ من إدخال التنسيق وإخراجه، بالإضافة إلى ذلك، أنشأنا ذاكرات تخزين مؤقتة لمرات قياس الأداء ومرات تطبيق التنسيق. ويؤدي ذلك إلى إعادة التعقيد إلى O(n)، مما يؤدي إلى تحقيق أداء خطي متوقّع لمطوّري الويب. إذا حدثت أي حالة يُجري فيها التنسيق ثلاث عمليات مرور، سنخزّن هذه العملية أيضًا في ذاكرة التخزين المؤقت. وقد يؤدي ذلك إلى توفير فرص لإدخال أوضاع تنسيق أكثر تقدمًا بأمان في المستقبل، ما يمثّل مثالاً على كيفية إتاحة إمكانات التوسّع بشكل أساسي في RenderingNG على جميع المستويات. في بعض الحالات، قد يتطلّب تنسيق الشبكة استخدام تنسيقات ثلاثية الخطوات، ولكن هذا نادر جدًا في الوقت الحالي.
تبيّن لنا أنّه عندما يواجه المطوّرون مشاكل في الأداء تتعلّق بالتصميم على وجه التحديد، يعود ذلك عادةً إلى خطأ تصاعدي في وقت التصميم بدلاً من معدل نقل البيانات الأوّلي لمرحلة التصميم في مسار الإحالة الناجحة. إذا كان تغييرًا متزايدًا صغيرًا (عنصر واحد يغيّر خاصيّة css واحدة) يؤدّي إلى عرض التنسيق في غضون 50 إلى 100 ملي ثانية، من المرجّح أنّ هذا خطأ تصاعدي في التنسيق.
الملخّص
يُعدّ التنسيق مجالًا معقّدًا للغاية، ولم نتطرّق إلى جميع أنواع التفاصيل المثيرة للاهتمام، مثل تحسينات التنسيق المضمّن (أي آلية عمل النظام الفرعي المضمّن والنصي بالكامل)، وحتى المفاهيم التي تمّ التحدّث عنها هنا لم تتناول سوى الأساسيات، وتجاهلت العديد من التفاصيل. نأمل أن نكون قد أوضحنا كيف يمكن أن يؤدي تحسين بنية النظام بشكل منهجي إلى تحقيق مكاسب كبيرة على المدى الطويل.
مع ذلك، ندرك أنّنا ما زلنا بحاجة إلى بذل الكثير من الجهد. نحن على دراية بفئات من المشاكل (في كلّ من الأداء وصحة الرمز) نعمل على حلّها، ونتطلّع إلى ميزات التنسيق الجديدة التي ستتوفّر في CSS. نعتقد أنّ بنية LayoutNG تجعل حلّ هذه المشاكل آمنًا وسهلاً.
صورة واحدة (أنت تعرفها) من تصميم "أونا كرافيتس"