@@ -6,3 +6,124 @@ export function getJsPerformanceMetrics() {
66 const firstPaint = paintResources . find ( ( entry ) => entry . name === 'first-contentful-paint' ) ;
77 return firstPaint ? [ firstPaint . startTime ] : [ ] ;
88}
9+
10+ /** @typedef {{error: string, success: false} } ErrorObject */
11+ /** @typedef {{success: true, metrics: any} } PerformanceMetricsResponse */
12+
13+ /**
14+ * Convenience function to return an error object
15+ * @param {string } errorMessage
16+ * @returns {ErrorObject }
17+ */
18+ function returnError ( errorMessage ) {
19+ return { error : errorMessage , success : false } ;
20+ }
21+
22+ /**
23+ * @returns {Promise<number | null> }
24+ */
25+ function waitForLCP ( timeoutMs = 500 ) {
26+ return new Promise ( ( resolve ) => {
27+ // eslint-disable-next-line prefer-const
28+ let timeoutId ;
29+ // eslint-disable-next-line prefer-const
30+ let observer ;
31+
32+ const cleanup = ( ) => {
33+ if ( observer ) observer . disconnect ( ) ;
34+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
35+ } ;
36+
37+ // Set timeout
38+ timeoutId = setTimeout ( ( ) => {
39+ cleanup ( ) ;
40+ resolve ( null ) ; // Resolve with null instead of hanging
41+ } , timeoutMs ) ;
42+
43+ // Try to get existing LCP
44+ observer = new PerformanceObserver ( ( list ) => {
45+ const entries = list . getEntries ( ) ;
46+ const lastEntry = entries [ entries . length - 1 ] ;
47+ if ( lastEntry ) {
48+ cleanup ( ) ;
49+ resolve ( lastEntry . startTime ) ;
50+ }
51+ } ) ;
52+
53+ try {
54+ observer . observe ( { type : 'largest-contentful-paint' , buffered : true } ) ;
55+ } catch ( error ) {
56+ // Handle browser compatibility issues
57+ cleanup ( ) ;
58+ resolve ( null ) ;
59+ }
60+ } ) ;
61+ }
62+
63+ /**
64+ * Get the expanded performance metrics
65+ * @returns {Promise<ErrorObject | PerformanceMetricsResponse> }
66+ */
67+ export async function getExpandedPerformanceMetrics ( ) {
68+ try {
69+ if ( document . readyState !== 'complete' ) {
70+ return returnError ( 'Document not ready' ) ;
71+ }
72+
73+ const navigation = /** @type {PerformanceNavigationTiming } */ ( performance . getEntriesByType ( 'navigation' ) [ 0 ] ) ;
74+ const paint = performance . getEntriesByType ( 'paint' ) ;
75+ const resources = /** @type {PerformanceResourceTiming[] } */ ( performance . getEntriesByType ( 'resource' ) ) ;
76+
77+ // Find FCP
78+ const fcp = paint . find ( ( p ) => p . name === 'first-contentful-paint' ) ;
79+
80+ // Get largest contentful paint if available
81+ let largestContentfulPaint = null ;
82+ if ( PerformanceObserver . supportedEntryTypes . includes ( 'largest-contentful-paint' ) ) {
83+ largestContentfulPaint = await waitForLCP ( ) ;
84+ }
85+
86+ // Calculate total resource sizes
87+ const totalResourceSize = resources . reduce ( ( sum , r ) => sum + ( r . transferSize || 0 ) , 0 ) ;
88+
89+ if ( navigation ) {
90+ return {
91+ success : true ,
92+ metrics : {
93+ // Core timing metrics (in milliseconds)
94+ loadComplete : navigation . loadEventEnd - navigation . fetchStart ,
95+ domComplete : navigation . domComplete - navigation . fetchStart ,
96+ domContentLoaded : navigation . domContentLoadedEventEnd - navigation . fetchStart ,
97+ domInteractive : navigation . domInteractive - navigation . fetchStart ,
98+
99+ // Paint metrics
100+ firstContentfulPaint : fcp ? fcp . startTime : null ,
101+ largestContentfulPaint,
102+
103+ // Network metrics
104+ timeToFirstByte : navigation . responseStart - navigation . fetchStart ,
105+ responseTime : navigation . responseEnd - navigation . responseStart ,
106+ serverTime : navigation . responseStart - navigation . requestStart ,
107+
108+ // Size metrics (in octets)
109+ transferSize : navigation . transferSize ,
110+ encodedBodySize : navigation . encodedBodySize ,
111+ decodedBodySize : navigation . decodedBodySize ,
112+
113+ // Resource metrics
114+ resourceCount : resources . length ,
115+ totalResourcesSize : totalResourceSize ,
116+
117+ // Additional metadata
118+ protocol : navigation . nextHopProtocol ,
119+ redirectCount : navigation . redirectCount ,
120+ navigationType : navigation . type ,
121+ } ,
122+ } ;
123+ }
124+
125+ return returnError ( 'No navigation timing found' ) ;
126+ } catch ( e ) {
127+ return returnError ( 'JavaScript execution error: ' + e . message ) ;
128+ }
129+ }
0 commit comments