تترك مقاييس العرض والاستجابة العادية فجوة في القياس بعد تحميل الصفحة الأوّلي:
LargestContentfulPaint(largest-contentful-paint) يقيس فقط التحميل الأولي ("الكامل") للمستند ويتوقف عن إرسال الإدخالات بمجرد تفاعل المستخدم مع الصفحة لأول مرة.PerformanceEventTimingوevent/ مدى استجابة الصفحة لتفاعلات المستخدم (INP) يقيس مدى استجابة الإدخال حتى الإطار التالي مباشرةً (مثل الحالة النشطة للزر أو مؤشر التحميل)، ولكنّه لا يتتبّع عمليات العرض غير المتزامنة اللاحقة، مثل المحتوى الجديد الذي يتم عرضه بعد حلّfetch()أو اكتمال انتقال المسار من جهة العميل.- قد يكون من الصعب تتبُّع تغييرات المسار في تطبيق من صفحة واحدة (SPA) خارج رمز إطار العمل. إنّ تتبُّع تعديلات عناوين URL فقط قد يؤدي إلى الإفراط في تسجيل تغييرات عناوين URL غير المرتبطة بتغييرات المسارات (على سبيل المثال، تغييرات الروابط إلى مواضع ثابتة، و "عمليات إعادة التوجيه" المتعدّدة لتغيير مسار واحد).
يملأ نوعا إدخال الأداء InteractionContentfulPaint (interaction-contentful-paint) وPerformanceSoftNavigation (soft-navigation) هذه الفجوة. وهي تنشر سببية التفاعل عبر المهام غير المتزامنة (fetch وawait وsetTimeout) لتحديد مصدر عمليات عرض المحتوى الجديدة وعمليات الانتقال في مسار التطبيق من صفحة واحدة (SPA) مباشرةً إلى تفاعل المستخدم الذي أدّى إلى حدوثها.
كيفية الإعداد
1. مراقبة عمليات عرض المحتوى التي يتم تشغيلها من خلال التفاعلات
استخدِم PerformanceObserver مع type: 'interaction-contentful-paint' وbuffered: true لملاحظة "سرعة عرض أكبر جزء من المحتوى" بشكل تدريجي، والتي يمكن أن تُعزى إلى أي تفاعل من المستخدم.
يوفّر كل إدخال في InteractionContentfulPaint ما يلي:
-
interactionId: المعرّف الفريد لتفاعل المستخدم الذي بدأ العملية (يطابقPerformanceEventTiming.interactionIdوPerformanceSoftNavigation.interactionId). -
startTime: الطابع الزمني لبدء تفاعل المستخدم الذي بدأ التنقّل، نسبةً إلى نقطة بداية وقت التنقّل الثابت الأولي. -
paintTimeوpresentationTime: وقت انتهاء مرحلة العرض ووقت رسم وحدات البكسل الملوّنة على الشاشة -
duration: الوقت المنقضي من بداية التفاعل إلى عرض المحتوى (presentationTime - startTime). -
largestContentfulPaint: لقطة من عنصرLargestContentfulPaintتحتوي على تفاصيل حول أكبر عنصر تم عرضه في هذا التفاعل حتى الآن (elementوsizeوurlوidوrenderTimeوloadTime).
/**
* Observes contentful paints caused by user interactions and invokes a callback
* whenever an interaction produces a larger contentful paint.
*
* @param {(entry: PerformanceEntry) => void} onInteractionPaint
*/
export function observeInteractionPaints(onInteractionPaint) {
// Feature-detect support before calling observe() so unsupported browsers
// do not throw or log console warnings.
if (
typeof PerformanceObserver === 'undefined' ||
!PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')
) {
return null;
}
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration is already calculated as (entry.presentationTime - entry.startTime),
// giving the total time from the user interaction to this contentful paint.
onInteractionPaint(entry);
}
});
// MANDATORY: Pass buffered: true to capture interaction paints that occurred
// before this observer was registered.
observer.observe({ type: 'interaction-contentful-paint', buffered: true });
return observer;
}
2. قياس سرعة عرض أكبر محتوى مرئي (LCP) لعمليات التنقّل السلس في تطبيقات الصفحة الواحدة
يُصدر المتصفّح عملية تنقّل سلسة عندما يؤدي تفاعل مستخدم موثوق به إلى تغيير عنوان URL مرئي وعرض محتوى. لاحتساب مقياس LCP لكل عملية تنقّل سلسة:
- لاحظ إدخالات كل من
'soft-navigation'و'interaction-contentful-paint'معbuffered: true. - MANDATORY: When a
'soft-navigation'entry is received, seed its initial LCP candidate by callingsoftNavEntry.getLargestInteractionContentfulPaint(). إذا عرض التطبيق المحتوى قبل تعديل عنوان URL (على سبيل المثال، عرض صفحة ثم استدعاءhistory.pushState())، فسيتم إرسال إدخالاتinteraction-contentful-paintالمبكرة قبل إدخالsoft-navigationوستظل تحملnavigationIdالسابق. - إجراء إلزامي: اربط إدخالات
'interaction-contentful-paint'اللاحقة بعملية التنقّل السلس النشطة من خلال مطابقةentry.interactionId === softNavEntry.interactionId، ولا تطابقها حسبentry.navigationId. يضمن استخدامinteractionIdتضمين عمليات العرض التي حدثت قبل تعديل عنوان URL واستبعاد عمليات العرض من التفاعلات اللاحقة غير ذات الصلة على المسار الجديد.
/**
* Tracks FCP and LCP for each client-side soft navigation and reports finalized
* route metrics when the user navigates to a new route or hides the page.
*
* @param {(report: {
* url: string,
* navigationId: number,
* interactionId: number,
* fcpDuration: number,
* lcpDuration: number | null,
* lcpElement: Element | null,
* lcpSize: number | null
* }) => void} onSoftNavReport
*/
export function trackSoftNavigationLCP(onSoftNavReport) {
const supported =
typeof PerformanceObserver !== 'undefined' &&
PerformanceObserver.supportedEntryTypes?.includes('soft-navigation') &&
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint');
if (!supported) {
return null;
}
let activeSoftNav = null;
let activeLcpEntry = null;
function finalizeCurrentSoftNav() {
if (!activeSoftNav) return;
onSoftNavReport({
// activeSoftNav.name holds the destination URL of the soft navigation.
url: activeSoftNav.name,
navigationId: activeSoftNav.navigationId,
interactionId: activeSoftNav.interactionId,
// activeSoftNav.duration is (activeSoftNav.presentationTime - activeSoftNav.startTime),
// which represents First Contentful Paint (FCP) relative to interaction start.
fcpDuration: activeSoftNav.duration,
// activeLcpEntry.duration is (activeLcpEntry.presentationTime - activeSoftNav.startTime),
// representing the soft navigation's LCP duration relative to interaction start.
lcpDuration: activeLcpEntry ? activeLcpEntry.duration : null,
lcpElement: activeLcpEntry?.largestContentfulPaint?.element ?? null,
lcpSize: activeLcpEntry?.largestContentfulPaint?.size ?? null,
});
activeSoftNav = null;
activeLcpEntry = null;
}
const softNavObserver = new PerformanceObserver((list) => {
for (const softNavEntry of list.getEntries()) {
// Finalize the previous route's metrics before resetting state for the new route.
finalizeCurrentSoftNav();
activeSoftNav = softNavEntry;
// MANDATORY: Seed the LCP candidate with getLargestInteractionContentfulPaint()
// to capture any attributed contentful paints that occurred before the URL updated.
activeLcpEntry = softNavEntry.getLargestInteractionContentfulPaint();
}
});
const icpObserver = new PerformanceObserver((list) => {
for (const icpEntry of list.getEntries()) {
// MANDATORY: Match by interactionId (not navigationId) so only paints caused
// by the soft navigation's initiating interaction update the route's LCP.
if (activeSoftNav && icpEntry.interactionId === activeSoftNav.interactionId) {
activeLcpEntry = icpEntry;
}
}
});
softNavObserver.observe({ type: 'soft-navigation', buffered: true });
icpObserver.observe({ type: 'interaction-contentful-paint', buffered: true });
// Finalize the active route's LCP when the page is backgrounded or unloaded.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
finalizeCurrentSoftNav();
}
});
return { softNavObserver, icpObserver, finalizeCurrentSoftNav };
}
3- قياس مؤشرات Core Web Vitals في عمليات التنقّل السلسة باستخدام web-vitals
إذا كان هدفك هو إعداد تقارير عن مقاييس Core Web Vitals العادية (LCP وCLS وINP) في عمليات التنقّل الثابتة والمرنة إلى نقطة نهاية في "إحصاءات Google"، استخدِم مكتبة web-vitals (الإصدار 6.0.0 والإصدارات الأحدث) مع { reportSoftNavs: true } بدلاً من تقسيم كل مخطط زمني للمقاييس يدويًا.
import { onLCP, onFCP, onCLS, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
// Example-only endpoint; replace with your real telemetry URL.
navigator.sendBeacon(
'/analytics',
JSON.stringify({
name: metric.name,
value: metric.value,
navigationType: metric.navigationType,
navigationURL: metric.navigationURL,
}),
);
}
// Pass { reportSoftNavs: true } to report metrics per soft navigation in addition
// to the initial hard navigation.
onLCP(sendToAnalytics, { reportSoftNavs: true });
onFCP(sendToAnalytics, { reportSoftNavs: true });
onCLS(sendToAnalytics, { reportSoftNavs: true });
onINP(sendToAnalytics, { reportSoftNavs: true });
أفضل الممارسات
- يجب رصد ميزتَي
'interaction-contentful-paint'و'soft-navigation'باستخدامPerformanceObserver.supportedEntryTypes?.includes(...)قبل استدعاءobserver.observe(). - يجب تمرير
{ buffered: true }عند رصد'interaction-contentful-paint'و'soft-navigation'لكي لا يتم تفويت الإدخالات المبكرة في حال بدء رمز الإحصاءات بشكل غير متزامن. - يجب استدعاء
softNavEntry.getLargestInteractionContentfulPaint()عند التعامل مع إدخالsoft-navigationجديد لكي يتم تضمين عمليات العرض التي حدثت قبل تعديل عنوان URL في مقياس LCP. - يجب مطابقة إدخالات
interaction-contentful-paintمع عملية تنقّل مرن باستخدامinteractionId، وليسnavigationId. - يجب استخدام
entry.duration(أوentry.presentationTime - entry.startTime) لقياس وقت استجابة التفاعل، بدلاً من إعداد تقارير عنentry.presentationTimeمباشرةً. يتم قياس جميع الطوابع الزمنية لـPerformanceEntryمن نقطة بداية وقت تحميل الصفحة الأولي، وليس من بداية التنقّل المرن. - لا تعتمد على
performance.getEntriesByType('soft-navigation')لمراقبة التطبيقات من صفحة واحدة التي تعمل لفترة طويلة، لأنّ ذاكرة التخزين المؤقت للمتصفّح تقتصر على أول 50 إدخالاً للتنقّل السلس.
استراتيجيات احتياطية
إذا كان هدفك الأساسي يتضمّن متصفّحات لا تتيح استخدام InteractionContentfulPaint أو PerformanceSoftNavigation، يمكنك التعامل مع واجهات برمجة التطبيقات هذه كتحسين تدريجي لميزة "مراقبة المستخدمين الحقيقيين" (RUM):
- رصد الميزات والتكيّف مع الإصدارات الأقدم: يمكنك حماية تسجيل المراقب باستخدام
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')وPerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). في المتصفّحات التي لا تتوفّر فيها هذه الميزة، يمكنك تخطّي تسجيل المراقبين لكي يعمل تطبيقك بدون ظهور أخطاء. - إبقاء أدوات تتبُّع التنقّل الصعب العادية نشطة: واصِل تتبُّع إدخالات
'largest-contentful-paint'و'event'و'layout-shift'العادية في جميع المتصفّحات. بما أنّ مقياسLargestContentfulPaintيتم الانتهاء منه عند أول تفاعل للمستخدم بغض النظر عن إتاحة ميزة التنقّل السلس، فإنّ قياس عمليات التنقّل السلس في المتصفّحات المتوافقة لا يغيّر أو يوقف قياسات LCP الأولية للتحميل الكامل.