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