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

1. مقدمة

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

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

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

ما ستتعلمه

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

ما تحتاج إليه

  • جهاز كمبيوتر يمكنه استنساخ الرمز البرمجي من 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) والتفاعل مع كل زيادة؟

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

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

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

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

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

بعد ذلك، سجِّل تفاعلاً في لوحة "الأداء".

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

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

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

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

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

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

4. أدوات معالجة الأحداث التي تستغرق وقتًا طويلاً

افتح ملف index.js، وأزِل التعليق من الدالة blockFor داخل متتبِّع الأحداث.

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

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

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

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

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

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

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

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

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

5- Experiment: processing duration

جرِّب طرقًا لإعادة ترتيب عمل معالج الأحداث لمعرفة تأثير ذلك في مقياس 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 التي تحظر التنفيذ.

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

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

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

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

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

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

يُظهر تسجيل "لوحة الأداء" التفاعل بشكل مطابق تقريبًا للتفاعلات السابقة.

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

الخلاصة

سيؤدي تشغيل أي رمز في أي متتبِّع أحداث إلى تأخير التفاعل.

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

هذه مشكلة شائعة.

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

7. Experiment: input delay

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

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

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

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

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


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

الإجراء

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

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

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


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

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

وغالبًا ما يُطلق على هذه الفترات الطويلة اسم "المهام الطويلة".

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

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

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

إحدى طرق تتبُّع هذه المشاكل هي قياس المهام التي تستغرق وقتًا طويلاً (أو إطارات الصور المتحركة التي تستغرق وقتًا طويلاً) وإجمالي وقت الحظر.

9- عرض تقديمي بطيء

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

حسنًا، تحديث الصفحة بتأثيرات مكلفة!

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

في سلسلة التعليمات الرئيسية:

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

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

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

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

RenderingNG

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

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

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

requestAnimationFrame بطيئة

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

انقل طلب 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 في حظر عملية الطلاء التالية لمدة ثانية كاملة.

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

ومع ذلك، يُرجى ملاحظة ما يلي:

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

12. جارٍ تشخيص التفاعلات

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

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

  • تأخير عملية الإدخال؟
  • مدة معالجة الحدث؟
  • تأخير العرض؟

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

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

كل التأخيرات

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

الاطّلاع على الرمز الكامل: timeout_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. تفاعلات متعدّدة (ونقرات غاضبة)

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

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

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

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

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

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

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

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

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

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

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

في الواقع، نريد إزالة المهام الطويلة تمامًا.

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

16. الاستراتيجية 1: تقليل عدد مرات التنفيذ

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

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

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

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

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

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

تفاعلات متعدّدة، ولكن مهمة يستغرق تنفيذها وقتًا طويلاً واحدة فقط نتيجةً لكل هذه التفاعلات

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

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

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

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

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

الكثير من setTimeout

المحاولة الأولى: تنفيذ إجراء بسيط

الاطّلاع على الرمز الكامل: small_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.

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

الاطّلاع على الرمز الكامل: yieldy.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 الخاتمة

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

ملاحظة

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

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

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

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