@@ -6,3 +6,124 @@ export function getJsPerformanceMetrics() {
6
6
const firstPaint = paintResources . find ( ( entry ) => entry . name === 'first-contentful-paint' ) ;
7
7
return firstPaint ? [ firstPaint . startTime ] : [ ] ;
8
8
}
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