UNPKG

@casoon/auditmysite

Version:

Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe

319 lines (315 loc) 13.2 kB
"use strict"; /** * Mobile Performance Collector * * Collects performance metrics specifically for mobile devices using mobile viewport * and mobile-specific thresholds. This runs parallel to desktop performance collection. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MobilePerformanceCollector = void 0; class MobilePerformanceCollector { constructor(options = {}) { this.options = options; } /** * Collect mobile performance metrics using mobile viewport */ async collectMobileMetrics(page, url) { const urlString = (typeof url === 'object' && url.loc ? url.loc : url); const startTime = Date.now(); try { if (this.options.verbose) { console.log(`📱 Starting mobile performance analysis for: ${urlString}`); } // Set mobile viewport await page.setViewportSize({ width: 375, height: 812 }); // iPhone 12 Pro size // Simulate mobile device await page.setExtraHTTPHeaders({ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' }); // Navigate to page (if not already loaded) const currentUrl = page.url(); if (currentUrl === 'about:blank' || currentUrl === '') { await page.goto(urlString, { waitUntil: 'networkidle', timeout: this.options.analysisTimeout || 30000 }); } // Wait for mobile-specific loading patterns await page.waitForTimeout(2000); // Mobile networks are slower // Optionally emulate PSI profile let cdpSession = null; try { if (this.options.psiProfile) { // @ts-ignore cdpSession = await page._client?.() || await page.context().newCDPSession(page); await cdpSession.send('Network.enable'); const net = this.options.psiNetwork || { latencyMs: 150, downloadKbps: 1600, uploadKbps: 750 }; await cdpSession.send('Network.emulateNetworkConditions', { offline: false, latency: net.latencyMs, downloadThroughput: Math.floor((net.downloadKbps * 1024) / 8), uploadThroughput: Math.floor((net.uploadKbps * 1024) / 8), connectionType: 'cellular3g' }); const cpuRate = this.options.psiCPUThrottlingRate || 4; await cdpSession.send('Emulation.setCPUThrottlingRate', { rate: cpuRate }); } } catch (e) { console.warn('PSI profile emulation (mobile) failed:', e); } // Collect Core Web Vitals with mobile focus const coreWebVitals = await this.collectMobileCoreWebVitals(page); // Collect additional metrics const additionalMetrics = await this.collectMobileTimingMetrics(page); // Reset emulation try { if (cdpSession) { await cdpSession.send('Emulation.setCPUThrottlingRate', { rate: 1 }); await cdpSession.send('Network.emulateNetworkConditions', { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }); } } catch (e) { console.warn('Failed to reset mobile emulation:', e); } // Calculate mobile performance score const score = this.calculateMobilePerformanceScore({ ...coreWebVitals, ...additionalMetrics }); const grade = this.calculateGrade(score); const recommendations = this.generateMobileRecommendations(coreWebVitals, additionalMetrics); const isMobileOptimized = this.assessMobileOptimization(coreWebVitals); if (this.options.verbose) { console.log(`📱 Mobile performance analysis completed in ${Date.now() - startTime}ms`); console.log(`📊 Mobile Score: ${score}/100 (Grade: ${grade})`); } return { score, grade, coreWebVitals, metrics: additionalMetrics, recommendations, isMobileOptimized }; } catch (error) { console.error('❌ Mobile performance collection failed:', error); return this.getFallbackMobileMetrics(); } } /** * Collect Core Web Vitals with mobile-specific measurement */ async collectMobileCoreWebVitals(page) { // Inject mobile-optimized Web Vitals measurement await page.addScriptTag({ content: ` window.mobileWebVitalsData = { lcp: 0, fcp: 0, cls: 0, ttfb: 0 }; // Enhanced LCP Observer for mobile (buffered true for late observers) if (typeof PerformanceObserver !== 'undefined') { try { const lcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; window.mobileWebVitalsData.lcp = Math.round(lastEntry.startTime); }); try { lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }); } catch(_) { lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); } // CLS Observer with mobile-specific handling let clsValue = 0; const clsObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } window.mobileWebVitalsData.cls = Math.round(clsValue * 1000) / 1000; }); clsObserver.observe({ entryTypes: ['layout-shift'] }); // FCP Observer const paintObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name === 'first-contentful-paint') { window.mobileWebVitalsData.fcp = Math.round(entry.startTime); } } }); paintObserver.observe({ entryTypes: ['paint'] }); } catch (e) { console.warn('Mobile Web Vitals observation failed:', e); } } ` }); // Wait longer so the final LCP can settle await page.waitForTimeout(5000); // Get collected metrics const webVitals = await page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0]; const mobileData = window.mobileWebVitalsData || { lcp: 0, fcp: 0, cls: 0, ttfb: 0 }; // Calculate TTFB const ttfb = navigation ? Math.round(navigation.responseStart - navigation.requestStart) : 0; // Fallback measurements if observers didn't work if (mobileData.lcp === 0) { const paintEntries = performance.getEntriesByType('paint'); const fcp = paintEntries.find(entry => entry.name === 'first-contentful-paint')?.startTime || 0; mobileData.lcp = fcp > 0 ? fcp * 1.2 : navigation?.loadEventEnd * 0.8 || 0; } if (mobileData.fcp === 0) { const paintEntries = performance.getEntriesByType('paint'); mobileData.fcp = paintEntries.find(entry => entry.name === 'first-contentful-paint')?.startTime || 0; } return { lcp: Math.round(mobileData.lcp), fcp: Math.round(mobileData.fcp), cls: mobileData.cls, ttfb: Math.round(ttfb) }; }); return webVitals; } /** * Collect mobile-specific timing metrics */ async collectMobileTimingMetrics(page) { const timingData = await page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0]; return { domContentLoaded: navigation ? Math.round(navigation.domContentLoadedEventEnd - navigation.fetchStart) : 0, loadComplete: navigation ? Math.round(navigation.loadEventEnd - navigation.fetchStart) : 0, renderTime: navigation ? Math.round(navigation.domContentLoadedEventEnd - navigation.responseEnd) : 0 }; }); return timingData; } /** * Calculate mobile performance score with mobile-specific thresholds */ calculateMobilePerformanceScore(metrics) { let score = 100; // Mobile-specific thresholds (stricter than desktop) // LCP scoring (35% weight - most critical for mobile) if (metrics.lcp > 4000) score -= 35; else if (metrics.lcp > 2500) score -= 25; else if (metrics.lcp > 2000) score -= 15; else if (metrics.lcp > 1500) score -= 5; // FCP scoring (30% weight) if (metrics.fcp > 3000) score -= 30; else if (metrics.fcp > 2000) score -= 20; else if (metrics.fcp > 1500) score -= 10; else if (metrics.fcp > 1200) score -= 5; // TTFB scoring (25% weight - critical for mobile networks) if (metrics.ttfb > 1000) score -= 25; else if (metrics.ttfb > 600) score -= 15; else if (metrics.ttfb > 400) score -= 8; else if (metrics.ttfb > 200) score -= 3; // CLS scoring (10% weight) if (metrics.cls > 0.25) score -= 10; else if (metrics.cls > 0.1) score -= 5; else if (metrics.cls > 0.05) score -= 2; return Math.max(0, Math.round(score)); } /** * Calculate performance grade */ calculateGrade(score) { if (score >= 90) return 'A'; if (score >= 80) return 'B'; if (score >= 70) return 'C'; if (score >= 60) return 'D'; return 'F'; } /** * Generate mobile-specific performance recommendations */ generateMobileRecommendations(coreWebVitals, metrics) { const recommendations = []; // LCP recommendations for mobile if (coreWebVitals.lcp > 2500) { recommendations.push(`🎯 Mobile LCP is ${coreWebVitals.lcp}ms - optimize for mobile networks with image compression, lazy loading, and CDN`); } // FCP recommendations for mobile if (coreWebVitals.fcp > 1800) { recommendations.push(`⚡ Mobile FCP is ${coreWebVitals.fcp}ms - minimize critical CSS, optimize fonts, reduce JavaScript for mobile`); } // TTFB recommendations for mobile if (coreWebVitals.ttfb > 600) { recommendations.push(`🚀 Mobile TTFB is ${coreWebVitals.ttfb}ms - optimize server response, use mobile-optimized CDN, enable aggressive caching`); } // CLS recommendations for mobile if (coreWebVitals.cls > 0.1) { recommendations.push(`📐 Mobile CLS is ${coreWebVitals.cls} - set explicit dimensions for mobile images, avoid dynamic content insertion`); } // Load time recommendations for mobile if (metrics.loadComplete > 5000) { recommendations.push(`📱 Mobile load time is ${metrics.loadComplete}ms - implement service worker, optimize for mobile networks`); } if (recommendations.length === 0) { recommendations.push('🎉 Excellent mobile performance! All metrics meet mobile optimization standards.'); } return recommendations; } /** * Assess mobile optimization status */ assessMobileOptimization(coreWebVitals) { return (coreWebVitals.lcp <= 2500 && coreWebVitals.fcp <= 1800 && coreWebVitals.ttfb <= 600 && coreWebVitals.cls <= 0.1); } /** * Fallback metrics when collection fails */ getFallbackMobileMetrics() { return { score: 0, grade: 'F', coreWebVitals: { lcp: 0, fcp: 0, cls: 0, ttfb: 0 }, metrics: { domContentLoaded: 0, loadComplete: 0, renderTime: 0 }, recommendations: ['Mobile performance analysis failed - unable to collect metrics'], isMobileOptimized: false }; } } exports.MobilePerformanceCollector = MobilePerformanceCollector; //# sourceMappingURL=mobile-performance-collector.js.map