أداء الويب وإمكانية الوصول

تعمّق في مقاييس Core Web Vitals

5 دقيقة للقراءة

مقاييس Core Web Vitals هي مقاييس Google الموحدة لقياس تجربة المستخدم الحقيقية. كل مقابلة واجهات أمامية في شركة مهتمة بالأداء ستختبر معرفتك بهذه المقاييس وحدودها وكيفية تحسينها.

مقاييس Core Web Vitals الثلاثة

المقياسجيديحتاج تحسينسيئ
LCP (أكبر عنصر محتوى مرئي)≤ 2.5 ثانية> 2.5 ثانية إلى ≤ 4.0 ثانية> 4.0 ثانية
CLS (إزاحة التخطيط التراكمية)≤ 0.1> 0.1 إلى ≤ 0.25> 0.25
INP (التفاعل حتى الرسم التالي)≤ 200 مللي ثانية> 200 مللي ثانية إلى ≤ 500 مللي ثانية> 500 مللي ثانية

حفظ الأرقام هو النصف السهل. النصف الذي يفوت المرشحين: تُقيَّم الصفحة عند الشريحة المئوية 75 من عمليات التحميل، مفصولةً بين الهاتف وسطح المكتب. "LCP جيد" لا يعني أن متوسطك كان 2.5 ثانية — بل أن ثلاثة أرباع عمليات التحميل الحقيقية تمت عند 2.5 ثانية أو أقل. هذا التمييز هو سبب نجاح موقع في Lighthouse ورسوبه في الميدان، وقوله دون أن يُسأل عنه يساوي أكثر من سرد الجدول.

LCP — أكبر عنصر محتوى مرئي

يقيس LCP مدى سرعة عرض أكبر عنصر محتوى مرئي. هذا عادةً ما يكون صورة البطل أو كتلة العنوان أو فقرة نصية كبيرة.

ما العناصر التي تُحسب لـ LCP

  • عناصر <img>
  • عناصر <image> داخل <svg>
  • صور ملصق <video>
  • عناصر بخلفية background-image محمّلة عبر CSS
  • عناصر نصية على مستوى الكتلة (<h1> و <p> وغيرها)

استراتيجيات تحسين LCP

1. استخراج CSS الحرج

استخرج CSS المطلوب للمحتوى فوق الطي وضمّنه في <head>:

<head>
  <!-- تضمين CSS الحرج للعرض الفوري -->
  <style>
    .hero { display: flex; align-items: center; min-height: 60vh; }
    .hero-title { font-size: 3rem; font-weight: 700; }
  </style>
  <!-- تحميل باقي CSS بشكل غير متزامن -->
  <link rel="preload" href="/styles/main.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
</head>

2. تحسين تحميل الخطوط

الخطوط تحجب العرض افتراضيًا. استخدم font-display: swap لعرض النص البديل فورًا:

@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* عرض الخط البديل حتى يُحمّل المخصص */
}

حمّل ملف الخط مسبقًا ليبدأ التنزيل باكرًا:

<link rel="preload" href="/fonts/custom.woff2" as="font"
      type="font/woff2" crossorigin>

3. تحسين الصور

استخدم التنسيقات الحديثة والأحجام المتجاوبة والتحميل الكسول للصور خارج الشاشة:

<!-- صورة البطل: تحميل مسبق لأنها عنصر LCP -->
<link rel="preload" as="image" href="/hero.avif"
      imagesrcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1200.avif 1200w"
      imagesizes="100vw">

<!-- صورة متجاوبة بتنسيقات حديثة -->
<picture>
  <source srcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1200.avif 1200w"
          sizes="100vw" type="image/avif">
  <source srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
          sizes="100vw" type="image/webp">
  <img src="/hero-800.jpg" alt="وصف صورة البطل"
       width="1200" height="600"
       fetchpriority="high">
</picture>

<!-- صور أسفل الطي: تحميل كسول -->
<img src="/feature.webp" alt="لقطة شاشة الميزة"
     loading="lazy" width="600" height="400">

4. التحميل المسبق للموارد الأساسية

أخبر المتصفح بإعطاء الأولوية للموارد الحرجة لـ LCP:

<link rel="preload" href="/api/hero-data" as="fetch" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.example.com">

CLS — إزاحة التخطيط التراكمية

يقيس CLS الاستقرار البصري. في كل مرة يتحرك فيها عنصر مرئي بشكل غير متوقع، يساهم ذلك في درجة CLS.

الأسباب الشائعة لإزاحات التخطيط

1. صور بدون أبعاد

<!-- سيئ: بدون أبعاد، يسبب إزاحة تخطيط عند تحميل الصورة -->
<img src="/photo.jpg" alt="صورة">

<!-- جيد: أبعاد صريحة تحجز المساحة -->
<img src="/photo.jpg" alt="صورة" width="800" height="600">

<!-- جيد: نسبة العرض إلى الارتفاع بـ CSS للصور المتجاوبة -->
<style>
  .responsive-img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
  }
</style>
<img src="/photo.jpg" alt="صورة" class="responsive-img">

2. محتوى مُدرج ديناميكيًا

/* حجز مساحة للمحتوى الديناميكي مثل الإعلانات أو اللافتات */
.ad-slot {
  min-height: 250px; /* حجز الارتفاع المتوقع للإعلان */
  contain: layout;   /* منع تغييرات التخطيط من التأثير على العناصر المجاورة */
}

.notification-banner {
  /* استخدم transform بدلاً من تغيير الارتفاع/الهامش */
  transform: translateY(-100%);
  transition: transform 0.3s ease;
}
.notification-banner.visible {
  transform: translateY(0);
}

3. خطوط الويب تسبب FOIT/FOUT

  • FOIT (وميض النص غير المرئي): المتصفح يخفي النص حتى يُحمّل الخط
  • FOUT (وميض النص غير المنسق): المتصفح يعرض الخط البديل ثم يبدّل

تحدث الإزاحة في لحظة التبديل نفسها: الخط البديل يختلف حجمًا وشكلًا عن خط الويب، فيُعاد تدفق النص عند وصول الخط الحقيقي. والواصفات التي تعالج ذلك — size-adjust وascent-override وdescent-override — توضع في قاعدة @font-face خاصة بالخط البديل، لا في قاعدة خط الويب نفسه. وضعها على خط الويب يغيّر طريقة عرض ذلك الخط، أي يحرّك النص الذي كنت تحاول تثبيته.

/* خط الويب: لا واصفات مقاييس هنا */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap;
}

/* خط محلي، مُعاد تعريفه باسم خاص به ومضبوط على مقاييس CustomFont.
   هذه النسب تخص هذا الزوج من الخطوط — قِسها ولا تنسخها. */
@font-face {
  font-family: 'CustomFont-fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 90%;
  descent-override: 20%;
}

body {
  font-family: 'CustomFont', 'CustomFont-fallback', sans-serif;
}

لا تتعامل مع هذه النسب الثلاث كثوابت. هي نسبة بين خطين بعينهما، ونسخها من مقال يعطيك خطًا بديلًا يختلف في اتجاه جديد. اشتقّها من مقاييس الخطين نفسيهما — unitsPerEm وascender وdescender ومتوسط عرض الحرف — أو ولّدها بأداة تقرأ ملفات الخطوط. القيمة المطلوبة محسوبة، لذا فالصيغة الباقية من هذه النصيحة هي طريقة الاشتقاق لا الرقم. ومرجع size-adjust على MDN يطبّقه على بديل local() لهذا السبب بالذات.

سؤال المتابعة في المقابلة: «أضفتَ size-adjust — إلى أي قاعدة؟» يفصل بين من نفّذ هذا فعلًا ومن قرأ عنه فقط، ولا يكلّف المُحاوِر أكثر من جملة.

4. خاصية CSS contain

/* أخبر المتصفح أن تخطيط هذا العنصر مستقل */
.card {
  contain: layout style; /* التغييرات الداخلية لن تؤثر على التخطيط الخارجي */
}

INP — التفاعل حتى الرسم التالي

حلّ INP محل FID (تأخر الإدخال الأول) كمقياس Core Web Vital في 12 مارس 2024. هذه حقيقة مهمة في المقابلات.

الفروق بين INP و FID

FID (مُهمل)INP (الحالي)
يقيسالتأخر قبل تشغيل معالج الإدخال الأولالكمون الكامل من الإدخال حتى الرسم التالي
النطاقالتفاعل الأول فقطجميع التفاعلات طوال دورة حياة الصفحة
يشملتأخر الإدخال فقطتأخر الإدخال + وقت المعالجة + تأخر العرض

ماذا يقيس INP

المراحل الثلاث داخل تفاعل واحد

يقيس INP المسافة كاملة من الإدخال حتى الإطار المرسوم التالي، لا مجرد الانتظار قبل تشغيل معالجك. لكل مرحلة إصلاح مختلف، ولهذا فجملة «معالجي سريع» ليست إجابة على درجة INP سيئة.

يبدأ المعالجانتهت المعالجاتأُودع الإطارالمستخدم ينقرأُرسل الحدث. ولم يُنفَّذ شيء بعدالمرحلة 1تأخر الإدخالالخيط الرئيسي كان مشغولًا بشيء …المرحلة 2مدة المعالجةكل معالج مُسجَّل للحدث يعمل حتى…المرحلة 3تأخر العرضالنمط والتخطيط والرسم. يُعالَج …= INPرسم الإطار التاليينتهي القياس هنا. المجموع عبر ا…

هناك شريحتان مئويتان يخلط المرشحون بينهما باستمرار:

  • داخل زيارة صفحة واحدة، يُبلّغ INP عن أطول تفاعل — مع سماح للقيم الشاذة. يُستبعد تفاعل مرتفع واحد مقابل كل 50 تفاعلًا، فعلى صفحة كثيفة التفاعل تقترب القيمة المُبلّغ عنها من الشريحة المئوية 98 لتفاعلات تلك الصفحة بدل أسوئها الحرفي.
  • عبر مستخدميك، الدرجة التي تُقيَّم عليها هي الشريحة المئوية 75 من عمليات تحميل الصفحة، كما في كل مقياس من Core Web Vitals.

القاعدة الأولى هي سبب ألا تُغرقك عثرة واحدة سيئة الحظ على صفحة مزدحمة. والثانية هي سبب ألا يُحرّك الرقمَ إصلاحُ تفاعل لا يصادفه إلا أبطأ 10% من مستخدميك.

استراتيجيات تحسين INP

1. تقسيم المهام الطويلة

الخيط الرئيسي لا يمكنه فعل شيء واحد فقط في المرة. المهام الطويلة (> 50 مللي ثانية) تحجب التفاعلات:

// سيئ: مهمة طويلة واحدة تحجب الخيط الرئيسي
function processLargeList(items) {
  items.forEach(item => {
    expensiveOperation(item); // تحجب لمدة 200 مللي ثانية إجمالًا
  });
}

// جيد: التنازل للخيط الرئيسي بين الدفعات
async function processLargeList(items) {
  const CHUNK_SIZE = 50;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    chunk.forEach(item => expensiveOperation(item));

    // التنازل للسماح للمتصفح بمعالجة التفاعلات المعلقة
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

// الأفضل: استخدام scheduler.yield() عند توفره
async function processLargeList(items) {
  const CHUNK_SIZE = 50;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    chunk.forEach(item => expensiveOperation(item));

    // scheduler.yield() يحافظ على أولوية المهمة
    if ('scheduler' in globalThis && 'yield' in scheduler) {
      await scheduler.yield();
    } else {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
}

2. اقرأ قبل أن تكتب

النمط المكلف ليس «كتابة الأنماط» بل قراءة خاصية تخطيط بعد كتابة واحدة. الكتابة تُبطل التخطيط، والقراءة تُجبر المتصفح على إعادة حسابه فورًا وبشكل متزامن وسط معالجك. كرّر ذلك داخل حلقة وستحصل على «خبط التخطيط» (layout thrashing).

المعالج نفسه، سطر واحد انتقل مكانه

javascript
يُجبر تخطيطًا
1button.addEventListener('click', () => {
2 element.style.width = '200px';
3
4 // الكتابة أعلاه أبطلت التخطيط، فهذه القراءة
5 // مضطرة لإعادة حسابه بشكل متزامن، الآن.
6 const height = element.offsetHeight;
7
8 element.style.height = height + 'px';
9});
بلا تخطيط قسري
1button.addEventListener('click', () => {
2 // تقرأ التخطيط الذي يملكه المتصفح أصلًا من
3 // الإطار السابق. لا شيء يُعاد حسابه.
4 const height = element.offsetHeight;
5
6 requestAnimationFrame(() => {
7 element.style.width = '200px';
8 element.style.height = height + 'px';
9 });
10});

دور requestAnimationFrame هنا واحد: ينقل الكتابات إلى الإطار الذي كان المتصفح سيرسمه أصلًا. وهو ليس ما يزيل التخطيط القسري — الترتيب هو ما يزيله. فالقراءة بعد الكتابة داخل ردّ نداء rAF تُجبر التخطيط تمامًا كما تفعل خارجه، وهذه هي التفصيلة التي سيضغط عليها المُحاوِر إن قدّمت «سألفّها في rAF» كإجابة كاملة. وإرشادات Chrome تنصّ على القاعدة مباشرة: اجمع قراءاتك ونفّذها أولًا ثم نفّذ الكتابات (تجنّب التخطيطات الكبيرة المعقدة وخبط التخطيط).

3. تأجيل معالجات الإدخال

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// تأجيل إدخال البحث لتجنب المعالجة عند كل ضغطة مفتاح
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
  filterResults(e.target.value);
}, 300));

قياس Core Web Vitals

تبويب الأداء في Chrome DevTools

  1. افتح DevTools → تبويب Performance
  2. فعّل خانة "Web Vitals"
  3. انقر Record، تفاعل مع الصفحة، ثم Stop
  4. يعرض الجدول الزمني LCP وإزاحات CLS وأحداث التفاعل مع مدتها

Lighthouse

يوفر Lighthouse درجة مبنية على بيانات مخبرية. شغّله من DevTools → تبويب Lighthouse، أو عبر سطر الأوامر:

npx lighthouse https://example.com --output=json --output-path=./report.json

مكتبة JavaScript web-vitals

import { onLCP, onCLS, onINP } from 'web-vitals';

onLCP(metric => console.log('LCP:', metric.value));
onCLS(metric => console.log('CLS:', metric.value));
onINP(metric => console.log('INP:', metric.value));

نصيحة للمقابلات: اعلم أن Lighthouse يقيس بيانات مخبرية (ظروف محاكاة)، بينما يقيس تقرير تجربة مستخدم Chrome (CrUX) بيانات ميدانية (مستخدمون حقيقيون). تستخدم Google البيانات الميدانية لترتيب البحث.

التالي: سنستكشف أنماط الأداء المتقدمة بما في ذلك تقسيم الكود والتخزين المؤقت واستراتيجيات التخزين المؤقت. :::

اختبار

الوحدة 5: أداء الويب وإمكانية الوصول

خذ الاختبار
هل كان هذا الدرس مفيدًا؟

سجّل الدخول للتقييم