UNPKG

browser-connect-mcp

Version:

MCP server for browser DevTools and backend debugging - analyze console logs, network requests, and backend logs with AI assistance

273 lines 11.9 kB
export class PerformanceProfiler { analyzePerformance(requests, responses, pageMetrics) { const responseMap = new Map(responses.map(r => [r.requestId, r])); // Calculate resource metrics const resourceMetrics = this.calculateResourceMetrics(requests, responseMap); // Generate waterfall const waterfall = this.generateWaterfall(requests, responseMap); // Find critical resources const critical = this.identifyCriticalResources(requests, responseMap); // Detect bottlenecks const bottlenecks = this.detectBottlenecks(requests, responseMap, waterfall); // Generate recommendations const recommendations = this.generateRecommendations(resourceMetrics, bottlenecks); // Calculate timing const timing = this.calculateTiming(requests, responseMap); return { navigation: { domContentLoaded: pageMetrics?.domContentLoaded, loadComplete: pageMetrics?.loadComplete, firstPaint: pageMetrics?.firstPaint, firstContentfulPaint: pageMetrics?.firstContentfulPaint, largestContentfulPaint: pageMetrics?.largestContentfulPaint, }, resources: { total: requests.length, byType: resourceMetrics, critical, waterfall: waterfall.slice(0, 50) // Limit waterfall entries }, timing, bottlenecks, recommendations }; } calculateResourceMetrics(requests, responseMap) { const metrics = {}; for (const request of requests) { const response = responseMap.get(request.requestId); const type = request.resourceType || 'Other'; if (!metrics[type]) { metrics[type] = { count: 0, totalSize: 0, totalDuration: 0, avgDuration: 0, cached: 0, failed: 0 }; } metrics[type].count++; if (response) { metrics[type].totalSize += response.encodedDataLength || 0; metrics[type].totalDuration += response.responseTime || 0; if (response.status >= 400) { metrics[type].failed++; } // Check if cached (status 304 or from cache headers) if (response.status === 304 || response.headers['x-cache'] === 'HIT') { metrics[type].cached++; } } } // Calculate averages for (const type in metrics) { if (metrics[type].count > 0) { metrics[type].avgDuration = metrics[type].totalDuration / metrics[type].count; } } return metrics; } generateWaterfall(requests, responseMap) { const entries = []; const sorted = [...requests].sort((a, b) => a.timestamp - b.timestamp); for (const request of sorted) { const response = responseMap.get(request.requestId); if (!response) continue; const startTime = request.timestamp; const endTime = response.timestamp; const duration = response.responseTime || 0; // Count parallel requests const parallelRequests = sorted.filter(r => { const rResponse = responseMap.get(r.requestId); if (!rResponse) return false; const rStart = r.timestamp; const rEnd = rResponse.timestamp; // Check if overlapping return (rStart <= startTime && rEnd >= startTime) || (rStart >= startTime && rStart <= endTime); }).length - 1; // Exclude self entries.push({ url: request.url, startTime, endTime, duration, type: request.resourceType || 'Other', status: response.status, parallelRequests }); } return entries; } identifyCriticalResources(requests, responseMap) { const critical = []; for (const request of requests) { const response = responseMap.get(request.requestId); if (!response) continue; let priority = 'low'; let blockingTime = 0; // Determine criticality based on resource type and characteristics if (request.resourceType === 'Document' || request.resourceType === 'Stylesheet') { priority = 'high'; blockingTime = response.responseTime || 0; } else if (request.resourceType === 'Script' && !request.url.includes('async')) { priority = 'high'; blockingTime = response.responseTime || 0; } else if (request.resourceType === 'Font') { priority = 'medium'; } // Also consider slow resources as critical if (response.responseTime && response.responseTime > 1000) { priority = priority === 'low' ? 'medium' : priority; } if (priority !== 'low') { critical.push({ url: request.url, type: request.resourceType || 'Other', duration: response.responseTime || 0, size: response.encodedDataLength || 0, priority, blockingTime }); } } // Sort by priority and duration return critical.sort((a, b) => { if (a.priority !== b.priority) { const priorityOrder = { high: 0, medium: 1, low: 2 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; } return b.duration - a.duration; }); } detectBottlenecks(requests, responseMap, waterfall) { const bottlenecks = []; // Slow resources (> 2 seconds) const slowResources = waterfall.filter(w => w.duration > 2000); if (slowResources.length > 0) { bottlenecks.push({ type: 'slow-resource', description: `${slowResources.length} resources took more than 2 seconds to load`, impact: 'high', resources: slowResources.slice(0, 5).map(r => r.url), suggestions: [ 'Consider using a CDN for static assets', 'Optimize server response time', 'Enable compression for large resources' ] }); } // Failed resources const failedResources = waterfall.filter(w => w.status >= 400); if (failedResources.length > 0) { bottlenecks.push({ type: 'failed-resource', description: `${failedResources.length} resources failed to load`, impact: 'high', resources: failedResources.slice(0, 5).map(r => r.url), suggestions: [ 'Fix broken resource URLs', 'Check server availability', 'Implement proper error handling' ] }); } // Large resources (> 1MB) const largeResources = requests.filter(r => { const response = responseMap.get(r.requestId); return response && response.encodedDataLength && response.encodedDataLength > 1024 * 1024; }); if (largeResources.length > 0) { bottlenecks.push({ type: 'large-resource', description: `${largeResources.length} resources are larger than 1MB`, impact: 'medium', resources: largeResources.slice(0, 5).map(r => r.url), suggestions: [ 'Optimize images (use WebP, AVIF formats)', 'Minify JavaScript and CSS files', 'Consider lazy loading for large resources' ] }); } // Blocking resources in critical path const blockingResources = waterfall .filter(w => w.type === 'Script' || w.type === 'Stylesheet') .filter(w => w.parallelRequests < 2 && w.duration > 500); if (blockingResources.length > 0) { bottlenecks.push({ type: 'blocking-resource', description: `${blockingResources.length} resources are blocking page rendering`, impact: 'high', resources: blockingResources.slice(0, 5).map(r => r.url), suggestions: [ 'Load scripts asynchronously with async or defer', 'Inline critical CSS', 'Use resource hints (preload, prefetch)' ] }); } return bottlenecks; } calculateTiming(requests, responseMap) { if (requests.length === 0) { return { totalDuration: 0, networkTime: 0, renderTime: 0 }; } const startTime = Math.min(...requests.map(r => r.timestamp)); let endTime = startTime; let totalNetworkTime = 0; for (const request of requests) { const response = responseMap.get(request.requestId); if (response) { endTime = Math.max(endTime, response.timestamp); totalNetworkTime += response.responseTime || 0; } } return { totalDuration: endTime - startTime, networkTime: totalNetworkTime, renderTime: Math.max(0, (endTime - startTime) - totalNetworkTime) }; } generateRecommendations(resourceMetrics, bottlenecks) { const recommendations = []; // Check for too many requests const totalRequests = Object.values(resourceMetrics).reduce((sum, m) => sum + m.count, 0); if (totalRequests > 100) { recommendations.push(`Reduce the number of HTTP requests (currently ${totalRequests}). Consider bundling resources.`); } // Check for low cache hit rate const totalCached = Object.values(resourceMetrics).reduce((sum, m) => sum + m.cached, 0); const cacheRate = totalRequests > 0 ? totalCached / totalRequests : 0; if (cacheRate < 0.3) { recommendations.push('Improve caching strategy. Only ' + Math.round(cacheRate * 100) + '% of resources are cached.'); } // Check for too many images if (resourceMetrics.Image && resourceMetrics.Image.count > 50) { recommendations.push('Consider implementing lazy loading for images. Found ' + resourceMetrics.Image.count + ' images.'); } // Check for uncompressed resources if (resourceMetrics.Script && resourceMetrics.Script.totalSize > 500 * 1024) { recommendations.push('Enable compression for JavaScript files. Total size: ' + Math.round(resourceMetrics.Script.totalSize / 1024) + 'KB'); } // Add specific recommendations based on bottlenecks const highImpactBottlenecks = bottlenecks.filter(b => b.impact === 'high'); if (highImpactBottlenecks.length > 0) { recommendations.push('Address high-impact performance issues first:'); highImpactBottlenecks.forEach(b => { recommendations.push(`- ${b.description}`); }); } return recommendations; } } //# sourceMappingURL=performance-profiler.js.map