لو الـ PageSpeed Insights بيقولّك إن LCP أخضر بس INP أحمر فوق 500ms، الموقع بيبان "ميت" وقت أول كليك حقيقي. المشكلة مش في السيرفر — الـ main thread اتقفل لـ 300ms أو أكتر بسبب JavaScript بيتنفّذ في وقت غلط.
المشكلة باختصار
في مارس 2024، Google استبدلت FID بـ INP في Core Web Vitals الرسمية. الفرق بالظبط: FID كان بيقيس أول تفاعل بس ومتوسط التأخير، INP بياخد أسوأ تفاعل في الجلسة كلها (تقريبًا P98). يعني لو في زرار واحد بيعلّق الصفحة 600ms، الموقع كله بياخد INP أحمر حتى لو باقي الأزرار سريعة.
النتيجة العملية: 64% من المواقع كانت بتعدّي FID قبل 2024. بعد التحويل لـ INP، النسبة نزلت لـ 56% حسب بيانات HTTP Archive. يعني حوالي 8% من المواقع وقعت من الأخضر للأحمر بدون ما حد يلمس الكود.
مثال بسيط قبل التعريف العلمي
تخيّل صفحة قائمة منتجات فيها 200 منتج، كل ما المستخدم يضغط على فلتر "السعر من الأقل للأعلى"، الكود بيشتغل دالة JavaScript بتعمل sort وtransform لكل الـ array. الـ click نفسه بياخد 1ms من المتصفح. لكن الـ JavaScript بياخد 450ms قبل ما المتصفح يقدر يرسم الشاشة من تاني. الـ INP هنا = 451ms = أحمر.
المستخدم اللي ضغط الزرار بيشوف فجوة 450ms قبل ما الصفحة تتغيّر. في الفترة دي، أي scroll أو click تاني بيتجاهل. الإحساس النفسي: "الصفحة عاطلة".
التعريف العلمي الدقيق
INP بيقيس الزمن من بداية تفاعل المستخدم (pointerdown، keydown، أو click) لحد أول paint بعد ما الـ event handlers خلصوا. بياخد أسوأ تفاعل في الجلسة، وبيتم القياس من المستخدمين الحقيقيين (Field Data) وليس من lab.
- Good: أقل من 200ms.
- Needs Improvement: 200ms إلى 500ms.
- Poor: أكثر من 500ms.
ليه الـ main thread بيتقفل أصلاً
المتصفح بيشتغل على main thread واحد. لو في JavaScript بياخد 300ms، الـ rendering وأي event هاندلر تاني بيستنوا. أكتر مصادر long tasks في تطبيقات الإنتاج:
- مكتبات تقيلة بتشتغل عند event (مثل lodash deepClone على object كبير، أو date-fns بدون tree-shaking).
- React re-render لقائمة طويلة بدون useMemo أو virtualization.
- Third-party scripts (Hotjar، Intercom، GTM) بتشتغل sync في الـ load handler.
- Hydration في Next.js على صفحة فيها أكتر من 50 component.
- JSON.parse على response أكبر من 1MB.
أي function بتاخد أكتر من 50ms، Chrome بيعتبرها "long task" رسميًا.
الحل في 4 خطوات بترتيب الـ ROI
1) قياس INP الحقيقي من المستخدمين (RUM)
PageSpeed Insights بيدّيك lab data من جهاز Moto G4 افتراضي، اللي ممكن يكون مختلف عن أجهزة المستخدمين الحقيقيين. ركّب web-vitals library على الإنتاج:
import { onINP } from 'web-vitals/attribution';
onINP((metric) => {
navigator.sendBeacon('/analytics', JSON.stringify({
name: 'INP',
value: metric.value,
rating: metric.rating,
target: metric.attribution?.interactionTarget,
type: metric.attribution?.interactionType,
inputDelay: metric.attribution?.inputDelay,
processingDuration: metric.attribution?.processingDuration,
}));
});الـ attribution field (web-vitals v4+) بيقولك العنصر اللي سبب أبطأ تفاعل بالظبط، وبيقسّم الزمن لـ input delay وprocessing duration وpresentation delay. بدل ما تحسّن كل حاجة، تحسّن العنصر ده بس.
2) تقسيم long tasks بـ scheduler.yield()
أي function بتاخد أكتر من 50ms اعتبرها long task. الحل الحديث: scheduler.yield() (Chrome 129+، أبريل 2024) أو fallback على setTimeout(0) للمتصفحات الأقدم.
async function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
}
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 50 === 0) {
await yieldToMain();
}
}
}كل ما تـ yield، المتصفح بياخد فرصة يرسم paint جديد ويستجيب للـ user input. الفرق بين scheduler.yield() وsetTimeout(0): الأول بيرجّعلك الـ task مع priority عالي، التاني بيدخل في طابور task queue عادي ممكن أي event سابق ياخد دوره قبلك.
3) تأجيل non-critical JS بـ dynamic import
scripts الـ analytics والـ chat widget مش لازم تشتغل في أول 5 ثواني. اعمل تأجيل صريح:
window.addEventListener('load', () => {
requestIdleCallback(() => {
import('./analytics-bundle.js');
import('./chat-widget.js');
}, { timeout: 3000 });
});4) Debounce على event handlers تقيلة
onChange على search field بيتنفّذ في كل keystroke. لو فيه filter ثقيل أو API call، استخدم debounce 150ms:
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}
input.addEventListener('input', debounce(handleSearch, 150));أرقام قبل وبعد من حالة فعلية
موقع e-commerce فيه 80K زائر يومي وحوالي 3K فلتر/بحث في الساعة. القياس قبل التحسين من Chrome User Experience Report (CrUX):
- INP P75: 580ms (Poor).
- أبطأ تفاعل: زرار "تطبيق الفلتر" بيعمل sort على array من 4500 منتج.
- Long task متوسط: 380ms على iPhone 12.
بعد تطبيق الخطوات 1-4 على أكتر 3 تفاعلات بطيئة بس (مش الموقع كله):
- INP P75: 165ms (Good).
- Long task متوسط: 42ms.
- وقت العمل الإجمالي: 4 أيام لمطور واحد.
الـ trade-offs اللي محدش بيقولّك عليها
scheduler.yield() بيضيف overhead صغير لكل yield، تقريبًا 0.5–1ms. لو بتعمل yield كل 5 items في array من 10K، بتضيف 2 ثانية إجمالي على الزمن الكلي للـ task. يعني الـ task هيخلص أبطأ بشكل عام، بس المتصفح هيبقى responsive في وسط ما بيشتغل. قسّم على chunks منطقية (50–100 item) بدل ما تـ yield بشكل ميكانيكي.
تأجيل scripts الـ analytics 3 ثواني بيخلّيك تخسر بيانات المستخدمين اللي بيـ bounce في أول ثانيتين، اللي ممكن يكونوا 15–20% من الزوار. لو الـ analytics مهم لقرارات marketing، أجّله 1 ثانية بس مش 3.
الـ debounce 150ms معناه إن المستخدم هيشوف نتيجة البحث متأخرة 150ms عن آخر حرف كتبه. لو الـ search عندك instant zero-latency، خفّض لـ 80ms.
متى لا تستخدم هذه التحسينات
لو موقعك صفحة landing static بدون forms أو filters، INP غالبًا أصلاً تحت 200ms. ما تضيعش وقت في تعقيد scheduler.yield() لمكان مش محتاجه. ركّز على LCP أو CLS بدل كده.
لو الـ framework بتاعك React 19 مع Concurrent Features (useTransition، useDeferredValue)، استخدم الـ APIs دي قبل ما تروح لـ scheduler.yield() اليدوي. React بيعمل scheduling أذكى منك في 90% من الحالات لأنه عارف الـ component tree.
لو الموقع بيخدم بشكل أساسي مستخدمين على desktop بمعالجات قوية، الـ long tasks اللي بتاكل 400ms على موبايل ممكن تاكل 50ms بس على desktop. شوف الـ device distribution بتاعك في CrUX قبل ما تصرف وقت في تحسين مش هيظهر للمستخدم.
الخطوة التالية
افتح Chrome DevTools، روح Performance tab، شغّل recording لمدة 5 ثواني وانت بتعمل أكتر تفاعل بيتعمل في موقعك (search، filter، add to cart). لو شفت "Long Task" أحمر فوق 200ms في الـ flame chart، ده العنصر اللي تبدأ بيه. ركّب web-vitals/attribution على staging قبل ما تنزل أي حاجة على الإنتاج، وقارن CrUX قبل وبعد بعد 28 يوم.
مصادر
- web.dev — INP Documentation: https://web.dev/articles/inp
- Chrome Developers — scheduler.yield(): https://developer.chrome.com/docs/web-platform/scheduler-yield
- web-vitals library (GitHub): https://github.com/GoogleChrome/web-vitals
- Core Web Vitals — INP launch announcement (Mar 2024): https://web.dev/blog/inp-cwv-march-12
- HTTP Archive Web Almanac — Performance: https://almanac.httparchive.org/en/2024/performance