تعقيدات تمرير لا نهائي

ملخّص: أعِد استخدام عناصر DOM وأزِل العناصر البعيدة عن إطار العرض. استخدِم عناصر نائبة لتفسير البيانات المتأخّرة. إليك عرضًا توضيحيًا ورمزًا لأداة التمرير اللانهائي.

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

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

The right thing™

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

سنستخدم 3 تقنيات لتحقيق هدفنا، وهي: إعادة استخدام DOM، و"أحجار القبر"، وتثبيت موضع التمرير.

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

لقطة شاشة لتطبيق محادثات

إعادة تدوير DOM

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

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

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

Runway Sentinel Viewport

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

ملفات Tombstone

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

Such
tomb. حجر جدًا يا للروعة

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

تثبيت التمرير

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

مخطّط تثبيت موضع التمرير

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

التنسيق

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

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

تعديلات تجريبية

أضاف متصفّح Chrome مؤخرًا ميزة CSS Containment التي تتيح للمطوّرين إخبار المتصفّح بأنّ أحد العناصر يشكّل حدودًا لعمليات التنسيق والرسم. بما أنّنا نُجري التخطيط بأنفسنا هنا، فإنّ هذا التطبيق هو تطبيق أساسي للاحتواء. عندما نضيف عنصرًا إلى شريط العرض، نعرف أنّ العناصر الأخرى لا تحتاج إلى إعادة التخطيط. لذلك يجب أن يحصل كل منتج على contain: layout. ولا نريد أيضًا أن يؤثر ذلك في بقية موقعنا الإلكتروني، لذا يجب أن يتلقّى المدرج نفسه توجيهات النمط هذه أيضًا.

فكرنا أيضًا في استخدام IntersectionObservers كآلية لرصد الوقت الذي يمرر فيه المستخدم الشاشة إلى الأسفل بما يكفي لنبدأ في إعادة استخدام العناصر وتحميل بيانات جديدة. ومع ذلك، تم تحديد IntersectionObserver على أنّه يتسبب في تأخير كبير (كما لو كان يستخدم requestIdleCallback)، لذا قد نشعر بأنّ الاستجابة أقل عند استخدام IntersectionObserver مقارنةً بعدم استخدامه. حتى أنّ عملية التنفيذ الحالية التي تستخدم الحدث scroll تعاني من هذه المشكلة، لأنّه يتم إرسال أحداث التمرير استنادًا إلى مبدأ "بذل قصارى الجهد". في النهاية، سيصبح Houdini’s Compositor Worklet الحلّ عالي الدقة لهذه المشكلة.

لا يزال غير مثالي

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

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

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