تخطَّ إلى المحتوى

مؤشرات Core Web Vitals في Next.js: ما الذي يحرّك الأرقام فعلًا

لمؤشرات LCP وINP وCLS أسباب محدّدة في تطبيق Next.js. هذه هي القائمة المرتّبة التي نمرّ بها في مشاريع الأداء، مع التغييرات التي تستحقّ العناء.

4 دقيقة قراءة

درجة Lighthouse قياس مخبري لتحميل صفحة واحدة على جهاز محاكى. أما بيانات الميدان فهي ما عاشه زوّارك فعلًا، وهي ما تستخدمه Google. ابدأ من هناك دائمًا:

# p75 field data for an origin, from the Chrome UX Report
curl -s "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"origin":"https://example.com","formFactor":"PHONE"}'

إذا كانت درجتك المخبرية 95 ومؤشر LCP الميداني 3.8 ثانية، فالمختبر يكذب عليك في شيء ما — غالبًا خطّ، أو سكربت طرف ثالث، أو شبكة واقعية لا تحاكيها شبكتك المحاكاة.

LCP: الصورة أو الخط في الغالب الأعمّ

يقيس Largest Contentful Paint اللحظة التي ينتهي فيها رسم أكبر عنصر ظاهر دون تمرير. في تطبيق Next.js، تغطّي أربعة أسباب الغالبية العظمى من حالات الفشل.

1. صورة الواجهة غير مُعطاة أولوية

import Image from 'next/image';
 
<Image
  src="/hero.jpg"
  alt=""
  width={1200}
  height={630}
  priority          // preloads it; without this it waits in the lazy queue
  sizes="100vw"
  quality={80}
/>

بدون priority، يقف عنصر LCP خلف كل مورد آخر في طابور المتصفّح. تغيير من سطر واحد يقتطع عادةً نصف ثانية من LCP على الهاتف.

وبالمثل: وضع priority على أكثر من صورة أو صورتين هو الخطأ نفسه معكوسًا — حين يُحمَّل كل شيء مسبقًا، لا شيء له أولوية.

2. المسار ديناميكي دون حاجة

المسار المعروض ثابتًا يقدّم HTML من أقرب عقدة حافة. أما الديناميكي فيُشغّل شجرة مكوّناتك وقاعدة بياناتك أولًا. وإذا تجاوز زمن أول بايت 600 مللي ثانية، فلن ينقذ LCP أي قدر من تحسين الصور — إذ لم يستلم المتصفّح بعد شيئًا ليرسمه.

هذه هي الصلة بين استراتيجية العرض ومؤشرات Core Web Vitals التي تغفلها معظم قوائم تدقيق الأداء تمامًا.

3. الخطوط تحجب الرسم

import { Inter } from 'next/font/google';
 
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',      // paint immediately with the fallback
  preload: true,
  adjustFontFallback: true,  // reduces the shift when the real font arrives
});

تستضيف next/font الملف ذاتيًا، وهو ما يزيل من مسارك الحرج استعلام DNS ومصافحة TLS ورحلة إلى نطاق طرف ثالث. وإن كنت ما زلت تحمّل الخطوط عبر <link> إلى شبكة خطوط، فثمّة تحسين مجاني ينتظرك.

4. سكربت طرف ثالث في الترويسة

مديرو الوسوم، وأدوات الدردشة، ولافتات الموافقة، ومسجّلات الجلسات. كلٌّ منها طلب حاجب في المسار الحرج ما لم يُؤجَّل صراحةً:

import Script from 'next/script';
 
<Script src="https://example.com/widget.js" strategy="lazyOnload" />

في التدقيقات، يكون سكربت طرف ثالث أكبر مساهم منفرد في LCP نحو ثلث الوقت. ويستحقّ الأمر القياس قبل أن تنفق دورة عمل كاملة على شيفرتك أنت.

INP: كلفة الترطيب

حلّ مؤشر Interaction to Next Paint محلّ First Input Delay لأن الأخير كان يقيس الشيء الخطأ — إذ كان يوقّت التأخير قبل بدء المعالج فقط، لا المدة التي انتظرها الزائر ليرى نتيجة.

مشكلات INP في Next.js هي مشكلات حزمة. والأسباب، بترتيب ورودها عندنا:

  • حدّ عميل مرتفع أكثر من اللازم في الشجرة. تناولناه بالتفصيل في مقالتنا عن أين يكلّفك use client فعلًا.
  • اعتمادية ثقيلة مستوردة على نطاق الوحدة. مكتبات الرسوم البيانية والتواريخ والمحرّرات. حمّلها بـnext/dynamic مع ssr: false حين يكون المكوّن فعلًا خارج الشاشة الأولى.
  • عمل مكلف داخل معالج حدث. ترشيح عشرة آلاف صف داخل onChange. انقله إلى الخادم، أو استخدم debounce والتمرير الافتراضي.
  • جذور ترطيب أكثر من اللازم. مئة بطاقة تفاعلية مستقلّة في صفحة قائمة تعني مئة وحدة ترطيب تتزاحم على الخيط الرئيسي.
import dynamic from 'next/dynamic';
 
const Chart = dynamic(() => import('@/components/Chart'), {
  ssr: false,
  loading: () => <ChartSkeleton />,
});

CLS: احجز المساحة

إزاحة التخطيط التراكمية أسهل الثلاثة إصلاحًا وأكثرها إحراجًا عند النشر.

  • امنح الصور دائمًا width وheight صريحين، أو غلّفها بحاوية ذات aspect-ratio ثابتة. تفرض next/image ذلك، وهو من أفضل أسباب استخدامها.
  • احجز مساحة لكل ما يصل متأخّرًا — مساحات الإعلانات والتضمينات ولافتات الموافقة. هيكل عظمي بأبعاد العنصر النهائي لا يكلّف شيئًا ويزيل الإزاحة تمامًا.
  • استخدم font-display: swap مع adjustFontFallback كي تقارب مقاييس الخط الاحتياطي مقاييس الخط الحقيقي.
  • لا تحقن لافتة فوق محتوى قائم بعد التحميل أبدًا. ضعها طبقةً فوقه، أو احجز مساحتها في HTML الأوّلي.

الدفاع عن النتيجة

كل إصلاح أعلاه يتآكل. أحدهم يضيف اعتمادية، وآخر يضيف سكربتًا، وثالث ينقل حدًّا. وبلا آلية إنفاذ ستُعيد هذا العمل نفسه العام المقبل.

# .github/workflows/performance.yml
- name: Lighthouse CI
  uses: treosh/lighthouse-ci-action@v12
  with:
    urls: |
      https://staging.example.com/
      https://staging.example.com/pricing
    budgetPath: ./lighthouse-budget.json
    uploadArtifacts: true
[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 180 },
      { "resourceType": "total", "budget": 600 }
    ],
    "timings": [{ "metric": "interactive", "budget": 3000 }]
  }
]

طلب الدمج الذي يدفع ميزانية جافاسكريبت فوق 180 كيلوبايت صار يُفشل فحصًا بدل أن يُنشر بصمت. ملف الميزانية ذاك هو المُخرَج الحقيقي لمشروع الأداء. أما تحسّن الدرجة فهو مجرّد ما يحدث في الطريق إليه.

الترتيب الذي نعمل به

  1. احصل على بيانات الميدان. جزّئها حسب فئة الجهاز وقالب المسار.
  2. أصلح استراتيجية العرض — مسار ديناميكي كان يجب أن يكون ثابتًا يتقدّم على كل ما عداه في هذه القائمة.
  3. أصلح عنصر LCP: الأولوية والأبعاد والصيغة.
  4. انقل الخطوط إلى next/font، وأخرج سكربتات الطرف الثالث من المسار الحرج.
  5. اسحب حدّ العميل إلى الأسفل، وقسّم ما تبقّى إلى أجزاء.
  6. احجز مساحة لكل ما يصل متأخّرًا.
  7. ضع ميزانية في التكامل المستمرّ كي لا يتراجع أيٌّ مما سبق.

الخطوتان الثانية والثالثة هما عادةً معظم المكسب. وما تبقّى هو الحفاظ عليه.

العودة إلى كل المقالات