1/** 2 * Collecteur de données waterfall pour les sessions réelles 3 * S'exécute sur toutes les pages publiques pour collecter les données de performance 4 */ 5(function() { 6 'use strict'; 7 8 // Vérifie que l'API Performance est disponible 9 if (typeof window.performance === 'undefined' || typeof window.performance.getEntriesByType !== 'function') { 10 return; 11 } 12 13 // URL actuelle (normalisée) 14 const currentUrl = window.location.href.split('?')[0].split('#')[0]; 15 16 // Collecte les informations du navigateur et de l'environnement 17 function collectBrowserInfo() { 18 return { 19 screen: { 20 width: window.screen.width || null, 21 height: window.screen.height || null, 22 availWidth: window.screen.availWidth || null, 23 availHeight: window.screen.availHeight || null, 24 colorDepth: window.screen.colorDepth || null, 25 pixelDepth: window.screen.pixelDepth || null 26 }, 27 viewport: { 28 width: window.innerWidth || document.documentElement.clientWidth || null, 29 height: window.innerHeight || document.documentElement.clientHeight || null 30 }, 31 devicePixelRatio: window.devicePixelRatio || null, 32 language: navigator.language || navigator.userLanguage || null, 33 languages: navigator.languages || null, 34 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null, 35 cookieEnabled: navigator.cookieEnabled !== undefined ? navigator.cookieEnabled : null, 36 onLine: navigator.onLine !== undefined ? navigator.onLine : null 37 }; 38 } 39 40 // Vérifie si une mesure est nécessaire (dernière mesure > 24h) 41 function checkIfMeasurementNeeded(callback) { 42 // Détermine l'URL AJAX 43 let ajaxUrl = (typeof ajaxurl !== 'undefined') ? ajaxurl : 44 (typeof wp !== 'undefined' && wp.ajax && wp.ajax.settings && wp.ajax.settings.url) ? 45 wp.ajax.settings.url : 46 '/wp-admin/admin-ajax.php'; 47 48 if (!ajaxUrl) { 49 return; 50 } 51 52 fetch(ajaxUrl, { 53 method: 'POST', 54 headers: { 55 'Content-Type': 'application/x-www-form-urlencoded', 56 }, 57 body: new URLSearchParams({ 58 action: 'pxn_psi_check_waterfall_needed', 59 url: currentUrl 60 }) 61 }) 62 .then(response => response.json()) 63 .then(data => { 64 if (data && data.success && data.data && data.data.needed === true) { 65 callback(); 66 } 67 }) 68 .catch(err => { 69 console.error('Erreur vérification waterfall:', err); 70 }); 71 } 72 73 // Collecte les métriques Core Web Vitals et navigation 74 function collectWebVitals() { 75 const vitals = { 76 navigation: {}, 77 paint: {}, 78 lcp: null, 79 fid: null, 80 inp: null, 81 cls: 0 82 }; 83 84 // Navigation timing 85 if (performance.timing) { 86 const timing = performance.timing; 87 vitals.navigation = { 88 domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart, 89 loadComplete: timing.loadEventEnd - timing.navigationStart, 90 domInteractive: timing.domInteractive - timing.navigationStart, 91 firstByte: timing.responseStart - timing.navigationStart 92 }; 93 } 94 95 // Paint metrics (FCP) 96 try { 97 const paintEntries = performance.getEntriesByType('paint'); 98 paintEntries.forEach(entry => { 99 if (entry.name === 'first-contentful-paint') { 100 vitals.paint.fcp = Math.round(entry.startTime); 101 } 102 }); 103 } catch(e) {} 104 105 // LCP (Largest Contentful Paint) 106 try { 107 const lcpEntries = performance.getEntriesByType('largest-contentful-paint'); 108 if (lcpEntries.length > 0) { 109 const lastLcp = lcpEntries[lcpEntries.length - 1]; 110 vitals.lcp = { 111 value: Math.round(lastLcp.renderTime || lastLcp.loadTime), 112 size: lastLcp.size || 0, 113 url: lastLcp.url || '' 114 }; 115 } 116 } catch(e) {} 117 118 // FID (First Input Delay) / INP 119 try { 120 const fidEntries = performance.getEntriesByType('first-input'); 121 if (fidEntries.length > 0) { 122 vitals.fid = { 123 delay: Math.round(fidEntries[0].processingStart - fidEntries[0].startTime), 124 duration: Math.round(fidEntries[0].duration || 0) 125 }; 126 } 127 128 // INP peut être calculé via Event Timing API si disponible 129 if (performance.eventCounts) { 130 const eventCounts = performance.eventCounts; 131 vitals.inp = { 132 click: eventCounts.get('click') || 0, 133 keydown: eventCounts.get('keydown') || 0, 134 pointerdown: eventCounts.get('pointerdown') || 0 135 }; 136 } 137 } catch(e) {} 138 139 // CLS (Cumulative Layout Shift) 140 try { 141 const clsEntries = performance.getEntriesByType('layout-shift'); 142 if (clsEntries.length > 0) {
143 clsEntries.forEach(entry => { 144 if (!entry.hadRecentInput) { 145 vitals.cls += entry.value; 146 } 147 }); 148 } 149 } catch(e) {} 150 151 return vitals; 152 } 153 154 // Détecte les opportunités d'optimisation 155 function detectOptimizationOpportunities(entries) { 156 const opportunities = { 157 largeImages: [], 158 unoptimizedImages: [], 159 largeScripts: [], 160 largeCSS: [], 161 unusedCSS: [], 162 thirdPartyHeavy: [], 163 missingCache: [], 164 slowThirdParty: [] 165 }; 166 167 entries.forEach(e => { 168 const size = e.transferSize || e.decodedBodySize || 0; 169 const sizeInKB = size / 1024; 170 const url = e.name.toLowerCase(); 171 172 // Images lourdes ou non optimisées 173 if (e.initiatorType === 'img' || url.match(/\.(jpg|jpeg|png|gif|webp|svg|avif)(\?|$)/i)) { 174 if (sizeInKB > 200) { 175 opportunities.largeImages.push({ 176 url: e.name, 177 size: size, 178 type: url.includes('.webp') || url.includes('.avif') ? 'optimized' : 'unoptimized' 179 }); 180 } 181 if (sizeInKB > 100 && !url.match(/\.(webp|avif)/i)) { 182 opportunities.unoptimizedImages.push({ 183 url: e.name, 184 size: size 185 }); 186 } 187 } 188 189 // Scripts lourds 190 if (e.initiatorType === 'script' || url.match(/\.js(\?|$)/i)) { 191 if (sizeInKB > 100) { 192 opportunities.largeScripts.push({ 193 url: e.name, 194 size: size, 195 duration: e.duration 196 }); 197 } 198 } 199 200 // CSS lourd 201 if (e.initiatorType === 'link' || url.match(/\.css(\?|$)/i)) { 202 if (sizeInKB > 50) { 203 opportunities.largeCSS.push({ 204 url: e.name, 205 size: size 206 }); 207 } 208 } 209 210 // Ressources tierces lourdes 211 try { 212 const resourceDomain = new URL(e.name).hostname; 213 const currentDomain = window.location.hostname; 214 if (resourceDomain !== currentDomain) { 215 if (sizeInKB > 200) { 216 opportunities.thirdPartyHeavy.push({ 217 url: e.name, 218 domain: resourceDomain, 219 size: size 220 }); 221 } 222 if (e.duration > 1000) { 223 opportunities.slowThirdParty.push({ 224 url: e.name, 225 domain: resourceDomain, 226 duration: e.duration 227 }); 228 } 229 } 230 } catch(err) {} 231 232 // Ressources sans cache (transferSize === encodedBodySize et > 0) 233 if (e.transferSize > 0 && e.transferSize === e.encodedBodySize && sizeInKB > 10) { 234 opportunities.missingCache.push({ 235 url: e.name, 236 size: size 237 }); 238 } 239 }); 240 241 return opportunities; 242 } 243 244 // Collecte les données de performance 245 function collectPerformanceData() { 246 try { 247 // Attend que la page soit complètement chargée 248 if (document.readyState === 'loading') { 249 window.addEventListener('load', collectPerformanceData); 250 return; 251 } 252 253 // Attend un peu pour que toutes les ressources soient chargées et LCP soit capturé 254 setTimeout(function() { 255 const entries = performance.getEntriesByType('resource'); 256 const data = entries.map(e => { 257 // Détermine le type de ressource 258 let type = e.initiatorType || 'other'; 259 if (type === 'link' && (e.name.includes('.css') || e.name.match(/\.css\?/))) { 260 type = 'css'; 261 } else if (type === 'link' && !e.name.match(/\.(css|js|png|jpg|gif|svg|woff|woff2|ttf|eot)/)) { 262 type = 'other'; 263 } 264 265 // Calculs réseau avancés 266 const compressionRatio = e.decodedBodySize > 0 ? 267 ((e.decodedBodySize - e.encodedBodySize) / e.decodedBodySize * 100).toFixed(1) : 0; 268 const cacheHit = e.transferSize === 0 && e.decodedBodySize > 0; 269 const priority = e.fetchStart > 0 ? e.fetchStart - e.startTime : null; 270 271 return { 272 name: e.name, 273 type: type, 274 start: parseFloat(e.startTime.toFixed(2)), 275 duration: parseFloat(e.duration.toFixed(2)), 276 dns: parseFloat((e.domainLookupEnd - e.domainLookupStart).toFixed(2)), 277 connect: parseFloat((e.connectEnd - e.connectStart).toFixed(2)), 278 ttfb: parseFloat((e.responseStart - e.requestStart).toFixed(2)), 279 transfer: parseFloat((e.responseEnd - e.responseStart).toFixed(2)), 280 transferSize: e.transferSize || 0, 281 encodedBodySize: e.encodedBodySize || 0, 282 decodedBodySize: e.decodedBodySize || 0, 283 responseStatus: e.responseStatus || 0, 284 nextHopProtocol: e.nextHopProtocol || '', 285 renderBlockingStatus: e.renderBlockingStatus || null, 286 compressionRatio: parseFloat(compressionRatio), 287 cacheHit: cacheHit, 288 priority: priority !== null ? parseFloat(priority.toFixed(2)) : null 289 }; 290 }); 291 292 // Collecte les Web Vitals 293 const webVitals = collectWebVitals(); 294 295 // Détecte les opportunités d'optimisation 296 const opportunities = detectOptimizationOpportunities(entries); 297 298 // Collecte les infos du navigateur 299 const browserInfo = collectBrowserInfo(); 300 301 // Envoie les données via AJAX 302 if (data.length > 0) { 303 sendWaterfallData(data, browserInfo, webVitals, opportunities); 304 } 305 }, 3000); // Attendre 3 secondes pour capturer LCP et autres métriques 306 } catch (err) { 307 console.error('Erreur collecte waterfall:', err); 308 } 309 } 310 311 // Envoie les données waterfall au serveur 312 function sendWaterfallData(data, browserInfo, webVitals, opportunities) { 313 // Détermine l'URL AJAX 314 let ajaxUrl = (typeof ajaxurl !== 'undefined') ? ajaxurl : 315 (typeof wp !== 'undefined' && wp.ajax && wp.ajax.settings && wp.ajax.settings.url) ? 316 wp.ajax.settings.url : 317 '/wp-admin/admin-ajax.php'; 318 319 if (!ajaxUrl) { 320 return; 321 } 322 323 fetch(ajaxUrl, { 324 method: 'POST', 325 headers: { 326 'Content-Type': 'application/x-www-form-urlencoded', 327 }, 328 body: new URLSearchParams({ 329 action: 'pxn_psi_save_waterfall_frontend', 330 url: currentUrl, 331 resources: JSON.stringify(data), 332 browser_info: JSON.stringify(browserInfo), 333 web_vitals: JSON.stringify(webVitals), 334 opportunities: JSON.stringify(opportunities) 335 }) 336 }) 337 .then(response => response.json()) 338 .then(data => { 339 if (data && data.success) { 340 console.log('Waterfall data saved successfully'); 341 } else { 342 console.error('Erreur sauvegarde waterfall:', data); 343 } 344 }) 345 .catch(err => { 346 console.error('Erreur envoi waterfall:', err); 347 });
348 } 349 350 // Démarre le processus 351 checkIfMeasurementNeeded(collectPerformanceData); 352})(); 353
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.