เมตริกการแสดงผลและการตอบสนองมาตรฐานจะทำให้เกิดช่องว่างในการวัดหลังจากโหลดหน้าเว็บครั้งแรก ดังนี้
LargestContentfulPaint(largest-contentful-paint) จะวัดเฉพาะการโหลดเอกสารครั้งแรก ("ฮาร์ด") และหยุดแสดงรายการทันทีที่ผู้ใช้โต้ตอบกับหน้าเว็บเป็นครั้งแรกPerformanceEventTimingและevent/ Interaction to Next Paint (INP) วัดการตอบสนองต่ออินพุตจนถึงเฟรมถัดไป (เช่น สถานะการทำงานของปุ่มหรือภาพหมุนขณะโหลด) แต่จะไม่ติดตามการแสดงผลแบบอะซิงโครนัสที่ตามมา เช่น เนื้อหาใหม่ที่แสดงผลหลังจากfetch()ได้รับการแก้ไข หรือการเปลี่ยนเส้นทางฝั่งไคลเอ็นต์เสร็จสมบูรณ์- การสังเกตการเปลี่ยนแปลงเส้นทางในแอปพลิเคชันหน้าเว็บเดียว (SPA) อาจทำได้ยากนอกโค้ดเฟรมเวิร์ก การตรวจสอบการอัปเดต URL เพียงอย่างเดียวอาจรายงานการเปลี่ยนแปลง URL ที่ไม่ได้ลิงก์กับการเปลี่ยนแปลงเส้นทางมากเกินไป (เช่น การเปลี่ยนแปลงลิงก์ตำแหน่งเฉพาะ, "การเปลี่ยนเส้นทาง" หลายรายการสำหรับการเปลี่ยนแปลงเส้นทางเดียว)
ประเภทรายการประสิทธิภาพ InteractionContentfulPaint (interaction-contentful-paint) และ PerformanceSoftNavigation (soft-navigation) จะช่วยเติมเต็มช่องว่างนี้ โดยจะเผยแพร่สาเหตุของการโต้ตอบในงานแบบอะซิงโครนัส (fetch, await, setTimeout) เพื่อระบุแหล่งที่มาของ Contentful Paint ใหม่และการเปลี่ยนเส้นทางแอปพลิเคชันหน้าเว็บเดียว (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. วัด Largest Contentful Paint (LCP) สำหรับการนำทางแบบไม่เข้มงวดของ SPA
เบราว์เซอร์จะส่งการนําทางแบบซอฟต์เมื่อการโต้ตอบของผู้ใช้ที่เชื่อถือได้ทําให้เกิดทั้งการเปลี่ยนแปลง URL ที่มองเห็นได้และการแสดงผลเนื้อหา วิธีคำนวณ LCP สำหรับการนำทางแบบซอฟต์แต่ละครั้ง
- สังเกตทั้งรายการ
'soft-navigation'และ'interaction-contentful-paint'ด้วยbuffered: true - บังคับ: เมื่อได้รับรายการ
'soft-navigation'ให้สร้างผู้สมัคร LCP เริ่มต้นโดยเรียกใช้softNavEntry.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) ทั้งในการนําทางแบบฮาร์ดและซอฟต์ไปยังปลายทางการวิเคราะห์ ให้ใช้ไลบรารี web-vitals (v6.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'เพื่อไม่ให้พลาดรายการแรกๆ หากโค้ด Analytics เริ่มต้นแบบอะซิงโครนัส - ควรเรียกใช้
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 รายการแรก
กลยุทธ์สำรอง
หากเป้าหมาย Baseline ของคุณรวมเบราว์เซอร์ที่ไม่รองรับ InteractionContentfulPaint หรือ PerformanceSoftNavigation ให้ถือว่า API เหล่านี้เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่องสำหรับการตรวจสอบผู้ใช้จริง (RUM) ดังนี้
- การตรวจหาฟีเจอร์และการลดลงอย่างค่อยเป็นค่อยไป: ป้องกันการลงทะเบียน Observer ด้วย
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')และPerformanceObserver.supportedEntryTypes?.includes('soft-navigation')ในเบราว์เซอร์ที่ไม่มีการรองรับ ให้ข้ามการลงทะเบียน Observer เพื่อให้แอปพลิเคชันทำงานโดยไม่แสดงข้อผิดพลาด - เปิดใช้ Hard Navigation Observer มาตรฐานไว้: สังเกตรายการ
'largest-contentful-paint','event'และ'layout-shift'มาตรฐานต่อไปในเบราว์เซอร์ทั้งหมด เนื่องจากLargestContentfulPaintจะสรุปผลในการโต้ตอบของผู้ใช้ครั้งแรกไม่ว่าจะรองรับการนําทางแบบซอฟต์หรือไม่ก็ตาม การวัดการนําทางแบบซอฟต์ในเบราว์เซอร์ที่รองรับจึงไม่เปลี่ยนแปลงหรือทําลายการวัด LCP แบบฮาร์ดโหลดเริ่มต้น