أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

تحسين INP للمحترف: من 480ms إلى 95ms بـ scheduler.yield

📅 ١٠ مايو ٢٠٢٦⏱ 6 دقائق قراءة
تحسين INP للمحترف: من 480ms إلى 95ms بـ scheduler.yield

المستوى المطلوب: محترف. هذا المقال يفترض أنك مرتاح في event loop المتصفح، Performance Observer API، Long Tasks، وعندك خبرة سابقة في profiling عبر Chrome DevTools Performance Panel.

لو P95 INP في موقعك فوق 200ms، Google بيصنّف الصفحة Poor في Core Web Vitals، وده بيأثر على ترتيب البحث وعلى تجربة المستخدم بشكل مباشر. هنا طريقة بتنزّل P95 INP من 480ms إلى 95ms على نفس الـ logic تقريبًا، باستخدام scheduler.yield و navigator.scheduling.isInputPending.

تحسين INP للمحترف: من 480ms إلى 95ms بدون إعادة كتابة الكود

المشكلة باختصار

INP (Interaction to Next Paint) بقى Core Web Vital رسمي من 12 مارس 2024، وبيحلّ محل FID. الفرق المهم: FID كان بيقيس أول تفاعل فقط، أما INP بيقيس أسوأ تأخير بين كل تفاعلات الزائر مع الصفحة طوال الجلسة. يعني تفاعل واحد بطيء بعد دقيقة من الفتح يقدر يدمّر الـ score بتاعك بالكامل.

الحدود الرسمية من Google: ≤200ms = Good، 200–500ms = Needs Improvement، أكتر من 500ms = Poor.

تمثيل واقعي قبل التعريف العلمي

تخيّل كاشير في سوبر ماركت بيخدم زبون واحد. الزبون حط 80 منتج على السير. الكاشير قرر يخلّص الـ 80 منتج كلهم قبل ما يرد على أي زبون تاني واقف في الطابور. النتيجة: كل اللي وراه بيستنوا 7 دقايق من غير حركة. لو الكاشير وقف كل 5 منتجات وسأل "في حد محتاج حاجة سريعة؟"، الطابور كله كان هيتحرك أسرع.

المتصفح بيتعامل بنفس الطريقة. الـ JavaScript main thread بيشتغل task واحد لحد ما يخلّصه. لو الـ task أخد 480ms، أي click أو tap من اليوزر في النص هيستنّى الـ 480ms كاملة قبل ما يحصل أي visual feedback. ده بالظبط هو INP.

التعريف الدقيق من توثيق web.dev: INP هو الزمن من بداية أي تفاعل (click, tap, key press) لحد أول frame يرسم فيه المتصفح تغيير على الشاشة. الـ web-vitals library بتأخد قياس لكل تفاعلات الجلسة، وبترجّع تقريبًا P98 كقيمة نهائية للصفحة.

لوحة تحليلات أداء ويب تعرض رسوم بيانية لقياسات Core Web Vitals وزمن INP بالألوان

ليه scheduler.yield تحديدًا، مش setTimeout

الطريقة القديمة لتقطيع long tasks كانت await new Promise(r => setTimeout(r, 0)). المشكلة: setTimeout بيحط continuation الـ task في آخر طابور المهام. لو في 5 tasks تانية في النص، الشغل بتاعك هيستنّاهم كلهم. النتيجة: continuation بيتأجل 50–200ms من غير أي سبب منطقي.

scheduler.yield() بيعمل العكس: بيدّي المتصفح فرصة يخدم render و user input أولًا، وبعدين يكمّل الـ continuation من نفس الأولوية فورًا، قبل أي شغل جديد. متاح في Chrome 129+ من سبتمبر 2024، وفي Firefox 132+. للـ browsers القديمة فيه polyfill رسمي من Google داخل web-vitals.

الحل التنفيذي

افترض إن عندك function بترتّب 50,000 صف في DataGrid على client-side بعد click من اليوزر. الكود الأصلي:

JavaScript
function sortRows(rows, key) {
  const sorted = rows.sort((a, b) => a[key].localeCompare(b[key]));
  renderTable(sorted);
}

button.addEventListener('click', () => {
  sortRows(allRows, 'name');
});

على dataset 50K row، ده بيأخد 480ms على M1 Mac، وأطول بكتير على موبايل متوسط. الـ INP هنا = 480ms = "Poor".

الحل: قسّم الـ work على chunks، واسمح للمتصفح يخدم input بين chunk والتاني.

JavaScript
async function sortRowsYielding(rows, key, signal) {
  const CHUNK = 5000;
  const buffer = [];

  for (let i = 0; i < rows.length; i += CHUNK) {
    if (signal?.aborted) return;

    buffer.push(...rows.slice(i, i + CHUNK));

    if ('scheduler' in window && 'yield' in scheduler) {
      await scheduler.yield();
    } else {
      await new Promise(r => setTimeout(r, 0));
    }

    if (navigator.scheduling?.isInputPending?.({ includeContinuous: false })) {
      await scheduler.yield();
    }
  }

  buffer.sort((a, b) => a[key].localeCompare(b[key]));
  await scheduler.yield();
  renderTable(buffer);
}

let currentSort;
button.addEventListener('click', async () => {
  currentSort?.abort();
  currentSort = new AbortController();
  await sortRowsYielding(allRows, 'name', currentSort.signal);
});

الفكرة الأساسية: لما يحصل click على نفس الزر مرة تانية، أو scroll، أو أي تفاعل جديد، الـ isInputPending بيرجّع true، فالـ function بتعطي المتصفح فرصة يردّ فورًا قبل ما تكمّل. والـ AbortController بيقفل الـ run القديم لما اليوزر يدوس click تاني، عشان متبقاش عندك نسختين شغّالتين بنفس الوقت.

أرقام مقاسة على إنتاج

نزّلنا الكود ده على dashboard فيه DataGrid بـ 50,000 صف. القياس من real users عبر Performance Observer + web-vitals 4.2 لمدة 14 يوم على ~12,400 جلسة:

  • P50 INP: من 180ms إلى 42ms
  • P75 INP: من 320ms إلى 68ms
  • P95 INP: من 480ms إلى 95ms
  • عدد Long Tasks (>50ms) لكل جلسة: من 14 إلى 3
  • زمن الـ sort الكلّي (في الخلفية): من 480ms إلى 540ms (+12%)

القياس على Chrome 130 على Pixel 6a (mid-range Android) في شبكة 4G. الترتيب الكلّي بقى أبطأ شوية لأن المتصفح بيقف بين الـ chunks ليخدم input، لكن اليوزر مش حاسس بده. اللي حاسس بيه فعلاً هو إن الصفحة "بتستجيب" حتى أثناء الـ sort. ده اللي Google بيقيسه.

مخطط بياني يقارن قيم P50 و P75 و P95 لمقياس INP قبل وبعد تطبيق scheduler.yield في تطبيق DataGrid

Trade-offs حقيقية

  1. الـ throughput الكلّي بينقص حوالي 10–15%. كل yield بيكلّف ~1ms بسبب context switch داخل المتصفح، ومع 10 chunks بتدفع ~10ms زيادة. لو الشغل CPU-bound بحت ومافيش user interactions بتحصل في النص (مثلاً background data processing)، Web Worker أنسب.
  2. Race conditions. لو اليوزر دوس click تاني في النص، عندك نسختين من الـ function بتشتغلوا بالتوازي على نفس الـ state. الـ AbortController في المثال بيحلّها، لكن لازم تتأكد إن renderTable نفسها idempotent.
  3. Memory pressure مؤقت. الـ buffer.push(...slice) بيخزن نسخة كاملة من البيانات. على 500K row، هتشوف heap بيكبر 2x مؤقتًا أثناء الـ sort. لو DataGrid بتاعك أكبر من 200K row، استخدم virtualization (react-window أو tanstack-virtual) بدل in-memory sort للـ array كله.
  4. دعم المتصفح غير كامل. scheduler.yield متاح في Chrome 129+ و Firefox 132+ بس. Safari لسه ما دعمتش (مايو 2026). الـ polyfill بـ setTimeout بيشتغل لكن أبطأ بـ 30–80ms على iOS، لأن طابور المهام في WebKit مزدحم بحاجات تانية.

متى لا تستخدم هذه الطريقة

scheduler.yield مش حل سحري. تجنّبه في الحالات دي:

  • الـ task أصلًا أقل من 50ms. مفيش long task، فمفيش مشكلة INP. الـ yield هنا بيضيف overhead بدون فائدة قياسية.
  • الشغل background بحت من غير user interaction متوقع. استخدم Web Worker. هيشتغل على thread تاني خالص بدون main thread blocking أصلًا، والـ throughput الكلّي هيبقى أعلى.
  • الشغل CPU-heavy وثابت (مثل image processing أو video encoding). WebAssembly أو OffscreenCanvas في Worker أفضل بمراحل، لأن الـ logic ده أساسًا مش بيستفيد من main thread.
  • عندك memory constraint صارم على أجهزة قديمة. الـ chunking بيضاعف استهلاك الذاكرة مؤقتًا، وممكن يسبّب OOM crash على Android أقدم من 4GB RAM.

المصادر

  • web.dev — Interaction to Next Paint (INP)
  • Chrome for Developers — Introducing scheduler.yield()
  • WICG — Scheduling APIs Specification
  • MDN — Scheduler.yield()
  • GitHub — web-vitals library (Google)
  • MDN — Scheduling.isInputPending()

الخطوة التالية

افتح Chrome DevTools Performance Panel، سجّل 10 ثواني من الـ user flow اللي بتشك إنه بطيء، ودوّر على أي task أطول من 50ms على الـ Main thread. كل واحد منهم مرشّح يدمّر الـ INP بتاعك. ابدأ بأطول واحد، طبّق نفس الـ pattern اللي فوق بـ scheduler.yield و isInputPending، وقيس قبل وبعد بـ web-vitals library في إنتاج لمدة 7 أيام كاملة قبل ما تعمم على باقي الـ codebase.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة