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