فهم مدى استجابة الصفحة لتفاعلات المستخدم (INP)

1. مقدمة

عرض توضيحي تفاعلي ودرس تطبيقي حول الترميز للتعرّف على مدى استجابة الصفحة لتفاعلات المستخدم (INP)

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

المتطلبات الأساسية

المعلومات التي تطّلع عليها

  • مدى تأثير التفاعل بين تفاعلات المستخدمين وطريقة تعاملك مع هذه التفاعلات في مدى استجابة الصفحة
  • كيفية تقليل التأخيرات وإزالتها لتوفير تجربة مستخدم سلسة

ما تحتاج إليه

  • جهاز كمبيوتر لديه القدرة على استنساخ التعليمات البرمجية من GitHub وتنفيذ أوامر npm.
  • محرِّر نصوص
  • إصدار حديث من Chrome لكي تعمل جميع قياسات التفاعل.

2. الإعداد

الحصول على الرمز وتشغيله

يمكنك العثور على الرمز في مستودع web-vitals-codelabs.

  1. استنسِخ المستودع في الوحدة الطرفية: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. الانتقال إلى الدليل المنسوخ: cd web-vitals-codelabs/understanding-inp
  3. تثبيت الملحقات: npm ci
  4. بدء خادم الويب: npm run start
  5. انتقِل إلى http://localhost:5173/understanding-inp/ في المتصفّح.

نظرة عامة على التطبيق

يوجد في أعلى الصفحة عدّاد الدرجة وزر الزيادة. هذا العرض التوضيحي الكلاسيكي يتميز بأسلوب التفاعل والاستجابة.

لقطة شاشة للتطبيق التجريبي الخاص بهذا الدرس التطبيقي حول الترميز

توجد أربعة قياسات أسفل الزر:

  • INP: يشير إلى نتيجة INP الحالية، وهي عادةً أسوأ تفاعل.
  • التفاعل: درجة أحدث تفاعل.
  • عدد اللقطات في الثانية: عدد اللقطات في الثانية لسلسلة التعليمات الرئيسية في الصفحة
  • المؤقت: رسم متحرك قيد التشغيل للمساعدة في تصور البيانات غير المحتملة.

إدخالات عدد اللقطات في الثانية والموقّت غير ضرورية على الإطلاق لقياس التفاعلات. تتم إضافتها فقط لتسهيل تصور الاستجابة.

جرّبه الآن

جرِّب التفاعل مع زر الزيادة ومراقبة الزيادة في النتيجة. هل تتغير قيمتا INP وInteract مع كل زيادة؟

يقيس INP الوقت الذي يستغرقه المستخدم منذ لحظة تفاعل المستخدم إلى أن تعرض الصفحة التعديل المعروض للمستخدم.

3- قياس التفاعلات مع "أدوات مطوري البرامج في Chrome"

افتح أدوات مطوري البرامج من المزيد من الأدوات > قائمة أدوات المطوّرين من خلال النقر بزر الماوس الأيمن على الصفحة واختيار فحص أو باستخدام اختصار لوحة مفاتيح

انتقِل إلى لوحة الأداء التي ستستخدمها لقياس التفاعلات.

لقطة شاشة للوحة "أداء أدوات مطوّري البرامج" بجانب التطبيق

بعد ذلك، التقط تفاعلاً في لوحة الأداء.

  1. اضغط على "تسجيل".
  2. التفاعل مع الصفحة (اضغط على زر الزيادة).
  3. أوقِف التسجيل.

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

عرض توضيحي متحرك لتسجيل تفاعل باستخدام لوحة أداء "أدوات مطوري البرامج"

يظهر تفاعلان. يمكنك تكبير الصورة الثانية من خلال التمرير أو الضغط مع الاستمرار على المفتاح W.

لقطة شاشة للوحة أداء "أدوات مطوّري البرامج"، ومؤشر تمرير مؤشر الماوس فوق التفاعل في اللوحة، وتلميح يعرض التوقيت القصير للتفاعل

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

4. أدوات معالجة الأحداث منذ فترة طويلة

افتح ملف index.js، وألغِ تعليق الوظيفة blockFor داخل أداة معالجة الحدث.

الاطّلاع على الرمز الكامل: click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

احفظ الملف. وسيلاحظ الخادم التغيير ويعيد تحميل الصفحة.

حاوِل التفاعل مع الصفحة مرة أخرى. ستصبح التفاعلات الآن أبطأ بشكل ملحوظ.

تتبُّع الأداء

يُرجى تسجيل فيديو آخر في لوحة "الأداء" لمعرفة كيف يبدو ذلك هناك.

تفاعل لمدة ثانية واحدة في لوحة الأداء

ما كان في السابق تفاعلاً قصيرًا الآن يستغرق ثانية كاملة.

عند تمرير مؤشّر الماوس فوق التفاعل، لاحِظ أنّ الوقت يُقضي بالكامل تقريبًا في "مدة المعالجة"، وهي مقدار الوقت المستغرق لتنفيذ عمليات استدعاء أداة معالجة الحدث. بما أنّ طلب الحظر blockFor تتم بالكامل داخل أداة معالجة الحدث، هذا هو الوقت المستغرَق.

5- التجربة: مدة المعالجة

جرب طرقًا لإعادة ترتيب عمل المستمعين إلى الحدث لمعرفة تأثيره على INP.

تحديث واجهة المستخدم أولاً

ماذا يحدث في حالة تبديل ترتيب استدعاءات js - تحديث واجهة المستخدم أولاً، ثم حظرها؟

اطّلِع على الرمز الكامل: ui_first.html.

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

هل لاحظت أن واجهة المستخدم تظهر في وقت سابق؟ هل يؤثر الترتيب في نتائج INP؟

حاول إجراء التتبع وفحص التفاعل لمعرفة ما إذا كانت هناك أي اختلافات.

فصل المستمعين

ماذا لو نقلت العمل إلى أداة معالجة حدث منفصلة؟ عدِّل واجهة المستخدم في أداة معالجة حدث واحدة، واحظر الوصول إلى الصفحة من أداة معالجة منفصلة.

اطّلِع على الرمز الكامل: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

كيف يظهر المحتوى في لوحة الأداء الآن؟

أنواع الأحداث المختلفة

ستؤدي معظم التفاعلات إلى تنشيط أنواع عديدة من الأحداث، بدءًا من مؤشر الماوس أو الأحداث الرئيسية، للانتقال إلى التمرير والتركيز/التمويه والأحداث الاصطناعية، مثل beforechange وbeforeinput.

فالعديد من الصفحات الحقيقية تستعين بأدوات الاستماع إلى العديد من الأحداث المختلفة.

ماذا يحدث في حال تغيير أنواع الأحداث الخاصة بأدوات معالجة الأحداث؟ على سبيل المثال، هل تريد استبدال إحدى أدوات معالجة أحداث click بـ pointerup أو mouseup؟

اطّلع على الرمز الكامل: diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

لا يتوفر تحديث لواجهة المستخدم

ماذا يحدث إذا أزلت الطلب لتعديل واجهة المستخدم من أداة معالجة الحدث؟

اطّلع على الرمز الكامل: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

6- نتائج التجربة المتعلّقة بمدة المعالجة

تتبُّع الأداء: تحديث واجهة المستخدم أولاً

اطّلِع على الرمز الكامل: ui_first.html.

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

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

تفاعل ثابت لمدة ثانية واحدة في لوحة الأداء

تتبُّع الأداء: مجموعة مستمعين منفصلة

اطّلِع على الرمز الكامل: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

مرة أخرى، لا يوجد فرق وظيفي. يستغرق التفاعل ثانية كاملة.

عند التركيز على تفاعل النقر، سترى أنّ هناك بالفعل دالتَين مختلفتَين يتم استدعاءهما نتيجة لحدث click.

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

نظرة مصغَّرة على التفاعل الذي تبلغ مدته ثانية واحدة في هذا المثال، مع توضيح أنّ اكتمال استدعاء الدالة الأول يستغرق أقل من مللي ثانية

تتبُّع الأداء: أنواع الأحداث المختلفة

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

هذه النتائج متشابهة جدًا. لا يزال التفاعل ثانية كاملة؛ الاختلاف الوحيد هو أنّ مستمع click الأقصر لاستخدام واجهة المستخدم للتحديث فقط يتم تشغيله الآن بعد مستمع pointerup الذي يحظر الوصول.

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

تتبُّع الأداء: ما مِن تحديث لواجهة المستخدم

اطّلع على الرمز الكامل: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • لا يتم تعديل النتيجة، ولكن الصفحة ما زالت تعمل.
  • يستمر تحديث الصور المتحركة وتأثيرات CSS وإجراءات مكوِّنات الويب التلقائية (إدخال النموذج) وإدخال النص والنص الذي يسلط الضوء على كل شيء.

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

لأنّ أداة معالجة الحدث حظرت سلسلة التعليمات الرئيسية لمدة ثانية تمنع عرض الصفحة، سيظل التفاعل يستغرق ثانية كاملة.

يؤدي تسجيل لوحة الأداء إلى عرض التفاعل مطابقًا تقريبًا للتفاعلات السابقة.

تفاعل ثابت لمدة ثانية واحدة في لوحة الأداء

طعام سفري

سيؤدي أي رمز يتم تشغيله في أي أداة معالجة للحدث إلى تأخير التفاعل.

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

إنها مشكلة شائعة!

أخيرًا: لا يعني مجرد عدم قيام التعليمة البرمجية الخاصة بك باكتمال عملية عرض النتائج بأن المسار لن ينتظره مستمعي الأحداث البطيئة لإكماله.

7. التجربة: تأخير الإدخال

ماذا عن الرمز البرمجي الذي يعمل لفترة طويلة خارج أدوات معالجة الأحداث؟ على سبيل المثال:

  • إذا كانت لديك عملية تحميل متأخرة للسمة <script> أدت إلى حظر الصفحة بشكل عشوائي أثناء التحميل.
  • هل هناك طلب بيانات من واجهة برمجة التطبيقات، مثل setInterval، الذي يحظر الصفحة بشكل دوري؟

جرِّب إزالة blockFor من أداة معالجة الحدث وإضافتها إلى setInterval():

اطّلِع على الرمز الكامل: enter_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

الإجراء

8. نتائج تجربة تأخير الإدخال

اطّلِع على الرمز الكامل: enter_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

يؤدي تسجيل نقرة على زر تحدث أثناء تنفيذ مهمة الحظر "setInterval" إلى تفاعل طويل الأمد، حتى مع عدم تنفيذ عمل الحظر خلال التفاعل نفسه.

غالبًا ما تسمى هذه الفترات الطويلة المدى بالمهام الطويلة.

عند تمرير مؤشّر الماوس فوق التفاعل في "أدوات مطوري البرامج"، ستظهر لك بيانات وقت التفاعل التي يعود مصدرها بشكلٍ أساسي إلى تأخّر الإدخال، وليس إلى مدة المعالجة.

لوحة أداء في &quot;أدوات مطوري البرامج&quot; تعرض مهمة حظر مدتها ثانية واحدة، وتفاعل جزئي خلال هذه المهمة، وتفاعل بمقدار 642 ملّي ثانية، ويعود سبب ذلك غالبًا إلى تأخير الإدخال

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

ويمكنك تتبُّع هذه المهام من خلال قياس المهام الطويلة (أو إطارات الصور المتحركة الطويلة) وإجمالي وقت الحظر.

9. عرض بطيء

تناولنا حتى الآن أداء JavaScript من خلال تأخير الإدخال أو أدوات معالجة الأحداث، ولكن ما هي العوامل الأخرى التي تؤثر في عرض سرعة عرض المحتوى بعد ذلك؟

حسنًا، جارٍ تحديث الصفحة بتأثيرات باهظة الثمن!

حتى إذا تمت عملية تحديث الصفحة بسرعة، قد يحتاج المتصفح إلى بذل مجهود كبير لعرضها.

في سلسلة المحادثات الرئيسية:

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

خارج سلسلة التعليمات الرئيسية:

  • استخدام CSS لتعزيز تأثيرات وحدة معالجة الرسومات
  • إضافة صور كبيرة جدًا عالية الدقة
  • استخدام الرسومات الموجّهة التي يمكن تغيير حجمها (SVG) أو لوحة الرسم "لوحة الرسم" لرسم مشاهد معقّدة

رسم تخطيطي للعناصر المختلفة للعرض على الويب

RenderingNG

في ما يلي بعض الأمثلة الشائعة على الويب:

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

10. التجربة: تأخير العرض التقديمي

requestAnimationFrame بطيئة

لنحاكي حدوث تأخير طويل في العرض التقديمي باستخدام requestAnimationFrame() API.

يمكنك نقل استدعاء blockFor إلى استدعاء requestAnimationFrame حتى يتم تشغيله بعد أن يعود مستمع الحدث إلى الحدث:

الاطّلاع على الرمز البرمجي بالكامل: presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

الإجراء

11. نتائج تجربة تأخير العرض التقديمي

الاطّلاع على الرمز البرمجي بالكامل: presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

يظل التفاعل لفترة طويلة، فماذا حدث؟

تطلب "requestAnimationFrame" معاودة الاتصال قبل سرعة العرض التالية. بما أنّ مقياس INP يقيس الوقت من التفاعل إلى سرعة عرض الصفحة التالية، سيستمر مقياس blockFor(1000) في requestAnimationFrame في حجب مدى العرض التالي لمدة ثانية كاملة.

تفاعل ثابت لمدة ثانية واحدة في لوحة الأداء

ومع ذلك، لاحظ شيئين:

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

12. تشخيص التفاعلات

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

عندما تستمر التفاعلات لفترة طويلة، ليس من الواضح دائمًا سبب ذلك. هل هي:

  • هل تأخير الإدخال؟
  • مدة معالجة الحدث؟
  • هل هناك تأخير في العرض التقديمي؟

يمكنك استخدام "أدوات مطوري البرامج" في أي صفحة تريدها لقياس مدى الاستجابة لطلبات البحث. للاعتياد على استخدامها، اتّبِع الخطوات التالية:

  1. تنقَّل على الويب كالمعتاد.
  2. اختياري: اترك وحدة تحكّم "أدوات مطوري البرامج" مفتوحة بينما تسجِّل إضافة "مؤشرات أداء الويب" التفاعلات.
  3. إذا لاحظت تفاعلاً ضعيفًا، حاوِل تكراره:
  • وإذا لم تتمكّن من تكراره، استخدِم سجلّات وحدة التحكّم للحصول على إحصاءات.
  • إذا تمكّنت من تكرارها، سجِّل الفيديو في لوحة الأداء.

كل التأخيرات

حاول إضافة بعض من كل هذه المشكلات إلى الصفحة:

الاطّلاع على الرمز الكامل: all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

بعد ذلك، استخدِم وحدة التحكّم ولوحة الأداء لتشخيص المشاكل.

13. التجربة: العمل غير المتزامن

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

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

لتجربة ذلك، واصِل تعديل واجهة المستخدم من أداة معالجة النقرات، ولكن نفِّذ عملية الحظر بعد انتهاء المهلة.

اطّلِع على الرمز الكامل: المهلة_100.html.

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

ما الخطوة التالية؟

14. نتائج تجربة العمل غير المتزامنة

اطّلِع على الرمز الكامل: المهلة_100.html.

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

تفاعل 27 مللي ثانية مع مهمة طويلة مدتها ثانية واحدة يحدث الآن في وقت التتبع

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

الدرس: إذا لم تتمكن من إزالته، حرّكه على الأقل!

الطُرق

هل يمكننا تحقيق نتائج أفضل من قيمة setTimeout الثابتة التي تبلغ مدّتها 100 ملي ثانية؟ من المحتمل ما زلنا نريد تشغيل الرمز في أسرع وقت ممكن، وإلا كان يجب علينا إزالته.

الهدف:

  • سيتم تنفيذ التفاعل incrementAndUpdateUI().
  • سيتم تشغيل blockFor() في أقرب وقت ممكن، ولكن لن يحظر سرعة العرض التالية.
  • وينتج عن ذلك سلوك يمكن التنبؤ به بدون "مهلات سحرية".

تتضمن بعض الطرق لتحقيق هذا ما يلي:

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

"requestPostAnimationFrame"

على عكس requestAnimationFrame بشكل منفرد (والتي ستحاول التشغيل قبل سرعة عرض البيانات التالية والتي ستظل عادةً تؤدي إلى تفاعل بطيء)، ينشئ requestAnimationFrame + setTimeout رمز polyfill بسيط لـ requestPostAnimationFrame، ويتم تشغيل طلب الاستدعاء بعد سرعة عرض البيانات التالية.

اطّلع على الرمز الكامل: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

بالنسبة إلى علم بيئة العمل، يمكنك حتى التفافها في وعد:

الاطّلاع على الرمز الكامل: raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  await nextPaint();
  blockFor(1000);
});

15. التفاعلات المتعددة (والنقرات الغضب)

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

جرِّب إصدار العمل غير المتزامن من الصفحة مرة أخرى (أو الإصدار الخاص بك إذا توصلت إلى الشكل الخاص بك بشأن تأجيل العمل في الخطوة الأخيرة):

اطّلِع على الرمز الكامل: المهلة_100.html.

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

ماذا يحدث في حال النقر عدّة مرات بسرعة؟

تتبُّع الأداء

مع كل نقرة، تتم إضافة مهمة تبلغ مدتها ثانية واحدة إلى قائمة الانتظار، ما يضمن حظر سلسلة المحادثات الرئيسية لفترة كبيرة من الوقت.

تنفيذ مهام متعددة تستغرق ثانية واحدة في سلسلة التعليمات الرئيسية، ما يؤدي إلى بطء التفاعلات في 800 ملي ثانية

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

الاستراتيجيات

من الناحية المثالية، نريد إزالة المهام الطويلة تمامًا!

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

16. الاستراتيجية 1: الارتداد

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

  • يمكنك استخدام setTimeout لتأجيل بدء العمل المكلف، باستخدام موقِّت يتراوح بين 500 و1,000 ملّي ثانية.
  • احفظ معرّف المؤقت عند إجراء ذلك.
  • في حال وصول تفاعل جديد، يمكنك إلغاء الموقّت السابق باستخدام clearTimeout.

اطّلِع على الرمز الكامل: debounce.html.

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

تتبُّع الأداء

تفاعلات متعددة، ولكنها مهمة واحدة طويلة فقط من العمل نتيجة لجميعها

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

17. الاستراتيجية 2: مقاطعة العمل الطويل الأجل

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

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

هناك بعض واجهات برمجة التطبيقات مثل isInputPending، ولكن من الأفضل عمومًا تقسيم المهام الطويلة إلى أجزاء.

setTimeout كثيرة

المحاولة الأولى: افعل شيئًا بسيطًا.

اطّلِع على الرمز الكامل: mini_tasks.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
  });
});

يعمل ذلك من خلال السماح للمتصفح بجدولة كل مهمة على حدة، ويمكن أن يكون للإدخال أولوية أعلى!

تفاعلات متعددة، ولكن تم تقسيم كل العمل المجدوَل إلى العديد من المهام الأصغر

لقد عدنا إلى خمس ثوانٍ كاملة من العمل مقابل خمس نقرات، ولكن كل مهمة مدتها ثانية واحدة لكل نقرة تم تقسيمها إلى عشر مهام تبلغ مدتها 100 ملّي ثانية. نتيجة لذلك - حتى مع تداخل التفاعلات المتعددة مع هذه المهام - لا يحدث أي تأخير في الإدخال لأكثر من 100 مللي ثانية! يمنح المتصفّح الأولوية لأدوات معالجة الأحداث الواردة على عملية setTimeout، وتظل التفاعلات متجاوبة.

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

ولا تعمل هذه الاستراتيجية بشكل جيد في فصل الرموز البرمجية المقترنة بشكل وثيق، مثل التكرار الحلقي for الذي يستخدم الحالة المشتركة.

الآن مع "yield()"

في المقابل، يمكننا الاستفادة من الإصدارَين الحديثَين async وawait لإضافة "نقاط الأرباح" بسهولة. مع أي وظيفة من وظائف JavaScript.

على سبيل المثال:

اطّلِع على الرمز الكامل: arrangey.html.

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

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

الآن مع "AbortContoller()"

لقد نجح ذلك، لكن كل تفاعل يقوم بجدولة المزيد من العمل، حتى إذا ظهرت تفاعلات جديدة وربما غيرت العمل الذي يجب القيام به.

باستخدام استراتيجية الارتداد، ألغينا المهلة السابقة مع كل تفاعل جديد. هل يمكننا تنفيذ شيء مماثل هنا؟ لإجراء ذلك، استخدِم AbortController():

اطّلع على الرمز الكامل: aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

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

وعندما تأتي نقرة ثانية، يتم وضع علامة على الحلقة الأولى باعتبارها ملغاة باستخدام AbortController وتبدأ تكرار حلقة blockInPiecesYieldyAborty جديدة. وفي المرة التالية التي تتم فيها جدولة تشغيل التكرار الأول مرة أخرى، تلاحظ أن حلقة signal.aborted أصبحت الآن true وتعود على الفور بدون إجراء مزيد من الإجراءات.

أصبحت سلسلة التعليمات الرئيسية الآن متوفرة في العديد من الأجزاء الصغيرة، والتفاعلات قصيرة، ولا تستمر إلا ما دامت بحاجة إلى

18 الخاتمة

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

ملاحظة

  • يقيس INP جميع التفاعلات.
  • ويتم قياس كل تفاعل بدءًا من الإدخال ووصولاً إلى سرعة عرض البيانات التالية، وهي الطريقة التي يرى بها مدى استجابة المستخدم.
  • يؤثر تأخير الإدخال ومدة معالجة الأحداث وتأخير العرض كل في سرعة استجابة التفاعل.
  • يمكنك بسهولة قياس مدى استجابة الصفحة لتفاعلات المستخدم (INP) وتفاصيل التفاعل باستخدام "أدوات مطوري البرامج".

الاستراتيجيات

  • عدم اشتمال صفحاتك على رمز برمجي طويل الأمد (مهام طويلة)
  • نقل الرمز غير الضروري من أدوات معالجة الأحداث إلى ما بعد عملية العرض التالية
  • تأكَّد من أنّ تحديث العرض فعال للمتصفّح.

مزيد من المعلومات