1. مقدمة
نسخة تجريبية تفاعلية ودرس تطبيقي حول الترميز للتعرّف على مدى استجابة الصفحة لتفاعلات المستخدم (INP).
المتطلبات الأساسية
- معرفة بتطوير HTML وJavaScript
- ننصحك بقراءة المستندات حول مقياس INP.
ما ستتعلّمه
- كيفية تأثير التفاعل بين تفاعلات المستخدمين ومعالجتك لهذه التفاعلات في سرعة استجابة الصفحة
- كيفية تقليل حالات التأخير والقضاء عليها لتقديم تجربة سلسة للمستخدمين
ما تحتاج إليه
- جهاز كمبيوتر يمكنه استنساخ الرموز البرمجية من GitHub وتنفيذ أوامر npm
- محرِّر نصوص
- إصدار حديث من Chrome لكي تعمل جميع قياسات التفاعل
2. طريقة الإعداد
الحصول على الرمز وتشغيله
يمكن العثور على الرمز في مستودع web-vitals-codelabs.
- استنسِخ المستودع في الوحدة الطرفية:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git - انتقِل إلى الدليل المستنسَخ:
cd web-vitals-codelabs/understanding-inp - تثبيت التبعيات:
npm ci - ابدأ خادم الويب:
npm run start - انتقِل إلى http://localhost:5173/understanding-inp/ في المتصفّح.
نظرة عامة على التطبيق
في أعلى الصفحة، يظهر عدّاد النتيجة وزر الزيادة. هذا عرض توضيحي كلاسيكي للتفاعل والاستجابة.

أسفل الزر، هناك أربعة قياسات:
- INP: نتيجة INP الحالية، والتي تكون عادةً أسوأ تفاعل
- التفاعل: نتيجة أحدث تفاعل
- عدد اللقطات في الثانية: عدد اللقطات في الثانية في سلسلة التعليمات الرئيسية للصفحة
- الموقّت: صورة متحركة لموقّت قيد التشغيل للمساعدة في رصد أي تأخير في عرض الإطارات
لا حاجة على الإطلاق إلى إدخالات FPS وTimer لقياس التفاعلات. تتم إضافتها فقط لتسهيل تصور الاستجابة قليلاً.
للتجربة:
حاوِل التفاعل مع الزر Increment (زيادة) وشاهِد زيادة النتيجة. هل تتغيّر قيمتَي مدى استجابة الصفحة لتفاعلات المستخدم والتفاعل مع كل زيادة؟
يقيس INP المدة التي تستغرقها الصفحة من لحظة تفاعل المستخدم معها إلى أن تعرض له التعديل الذي تم إجراؤه.
3- قياس التفاعلات باستخدام "أدوات مطوّري البرامج في Chrome"
افتح "أدوات مطوّري البرامج" من قائمة مزيد من الأدوات > أدوات مطوّري البرامج، من خلال النقر بزر الماوس الأيمن على الصفحة واختيار فحص، أو من خلال استخدام اختصار لوحة مفاتيح.
انتقِل إلى لوحة الأداء التي ستستخدمها لقياس التفاعلات.

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

يظهر تفاعلان. كبِّر الصورة الثانية من خلال الانتقال للأعلى أو الضغط مع الاستمرار على مفتاح 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():
الاطّلاع على الرمز الكامل: 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 يؤدي إلى تفاعل طويل الأمد، حتى في حال عدم تنفيذ أي عمل حظر في التفاعل نفسه.
غالبًا ما يُطلق على هذه الفترات الطويلة اسم "مهمة يستغرق تنفيذها وقتًا طويلاً".
عند تمرير مؤشر الماوس فوق التفاعل في "أدوات مطوري البرامج في Chrome"، ستتمكّن من رؤية أنّ وقت التفاعل يُنسب الآن بشكل أساسي إلى تأخير الإدخال، وليس إلى مدة المعالجة.

يُرجى العِلم أنّ ذلك لا يؤثر دائمًا في التفاعلات. إذا لم تنقر أثناء تشغيل المهمة، فقد يحالفك الحظ. قد يكون من الصعب تصحيح أخطاء هذه العطسات "العشوائية" عندما تسبّب مشاكل في بعض الأحيان فقط.
ويمكن تتبُّع هذه المشاكل من خلال قياس المهام التي يستغرق تنفيذها وقتًا طويلاً (أو الصور المتحركة التي يستغرق عرضها وقتًا طويلاً) وإجمالي وقت الحظر.
9. عرض بطيء
حتى الآن، تناولنا أداء JavaScript من خلال تأخير الإدخال أو متتبِّعات الأحداث، ولكن ما الذي يؤثر أيضًا في عرض المحتوى التالي؟
حسنًا، تحديث الصفحة بتأثيرات باهظة الثمن!
حتى إذا تم تحديث الصفحة بسرعة، قد يظل على المتصفّح العمل بجد لعرضها!
في سلسلة التعليمات الرئيسية:
- أُطر عمل واجهة المستخدم التي تحتاج إلى عرض التعديلات بعد تغييرات الحالة
- يمكن أن تؤدي تغييرات DOM أو تبديل العديد من أدوات اختيار طلبات البحث في CSS إلى تشغيل الكثير من الأنماط والتنسيقات والرسومات.
خارج سلسلة التعليمات الرئيسية:
- استخدام CSS لتشغيل تأثيرات وحدة معالجة الرسومات
- إضافة صور كبيرة جدًا عالية الدقة
- استخدام SVG/Canvas لرسم مشاهد معقّدة

في ما يلي بعض الأمثلة الشائعة على الويب:
- موقع إلكتروني يعتمد على 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 في حظر العرض التالي لمدة ثانية كاملة.

ومع ذلك، لاحظ شيئين:
- عند تمرير مؤشر الماوس فوقه، ستلاحظ أنّ كل وقت التفاعل يتم إنفاقه الآن في "تأخير العرض" لأنّ حظر سلسلة التعليمات الرئيسية يحدث بعد عرض متتبِّع الأحداث.
- لم يعُد جذر نشاط سلسلة التعليمات الرئيسية هو الحدث الناتج عن النقر، بل "تم تنشيط إطار الصورة المتحركة".
12. تشخيص التفاعلات
في صفحة الاختبار هذه، تكون الاستجابة مرئية للغاية، مع النتائج والمؤقتات وواجهة مستخدم العداد...ولكن عند اختبار الصفحة العادية، تكون أكثر دقة.
عندما تستغرق التفاعلات وقتًا طويلاً، لا يكون السبب واضحًا دائمًا. هل هو:
- هل هناك تأخير في عملية الإدخال؟
- ما هي مدة معالجة الحدث؟
- ما هي مهلة العرض؟
في أي صفحة تريدها، يمكنك استخدام "أدوات مطوّري البرامج في Chrome" للمساعدة في قياس مدى الاستجابة. لاكتساب هذه العادة، جرِّب الخطوات التالية:
- تصفَّح الويب كالمعتاد.
- راقِب سجلّ "التفاعلات" في عرض المقاييس المباشرة ضمن لوحة "الأداء" في "أدوات مطوّري البرامج في Chrome".
- إذا لاحظت تفاعلاً ضعيف الأداء، حاوِل تكراره:
- إذا لم تتمكّن من تكرارها، استخدِم سجلّ التفاعل للحصول على إحصاءات.
- إذا كان بإمكانك تكرارها، سجِّل تتبُّعًا في لوحة "الأداء".
جميع التأخيرات
حاول إضافة القليل من كل هذه المشكلات إلى الصفحة:
الاطّلاع على الرمز الكامل: 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);
});

أصبح التفاعل الآن قصيرًا لأنّ سلسلة التعليمات الرئيسية تكون متاحة فورًا بعد تعديل واجهة المستخدم. تستمر مهمة الحظر الطويلة في العمل، ولكنها تعمل في وقت ما بعد العرض، وبالتالي سيتلقّى المستخدم ملاحظات فورية حول واجهة المستخدم.
الدرس المستفاد: إذا لم تتمكن من إزالته، على الأقل انقله!
الطُرق
هل يمكننا تحسين setTimeout الثابتة التي تبلغ 100 ملي ثانية؟ من المرجّح أنّنا نريد أن يتم تنفيذ الرمز في أسرع وقت ممكن، وإلا كان علينا إزالته.
الهدف:
- سيتم تشغيل التفاعل في
incrementAndUpdateUI(). - سيتم تشغيل
blockFor()في أقرب وقت ممكن، ولكن لن يتم حظر العرض التالي. - يؤدي ذلك إلى سلوك يمكن توقّعه بدون "مهلات سحرية".
تشمل بعض الطرق لتحقيق ذلك ما يلي:
setTimeout(0)Promise.then()requestAnimationFramerequestIdleCallbackscheduler.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);
});
ماذا يحدث إذا نقرت عدة مرات بسرعة؟
تتبُّع الأداء
لكل نقرة، يتم وضع مهمة مدتها ثانية واحدة في قائمة انتظار المهام، ما يضمن حظر سلسلة التعليمات الرئيسية لفترة زمنية طويلة.

عندما تتداخل هذه المهام التي يستغرق تنفيذها وقتًا طويلاً مع النقرات الجديدة الواردة، يؤدي ذلك إلى بطء التفاعلات على الرغم من أنّ متتبِّع الأحداث نفسه يعرض النتائج على الفور تقريبًا. لقد أنشأنا الموقف نفسه كما في التجربة السابقة مع تأخيرات في الإدخال. ولكن في هذه الحالة، لا يكون تأخير الإدخال ناتجًا عن 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" بسهولة.
الاستراتيجيات
- تجنَّب استخدام الرموز البرمجية التي يستغرق تنفيذها وقتًا طويلاً (المهام التي يستغرق تنفيذها وقتًا طويلاً) في صفحاتك.
- انقل الرموز البرمجية غير الضرورية من متتبِّعات الأحداث إلى ما بعد العرض التالي.
- تأكَّد من أنّ عملية تحديث العرض نفسها فعّالة للمتصفّح.