UNPKG

pagespeed-insights-mcp

Version:

MCP server for Google PageSpeed Insights API - analyze web performance with Claude

325 lines 15.5 kB
export class PerformanceRecommendationsEngine { constructor() { this.auditMappings = new Map([ // Performance audits ['unused-css-rules', { title: '🎨 Remove unused CSS', impact: 'medium', effort: 'medium', category: 'performance', howToFix: [ 'Use tools like PurgeCSS or UnCSS to remove unused styles', 'Split CSS by page/component to reduce bundle size', 'Use critical CSS for above-the-fold content', 'Consider CSS-in-JS solutions for dynamic styling' ], moreInfo: 'Removing unused CSS can significantly reduce file sizes and improve loading times' }], ['unused-javascript', { title: '⚡ Remove unused JavaScript', impact: 'high', effort: 'medium', category: 'performance', howToFix: [ 'Use tree-shaking with modern bundlers (Webpack, Rollup, Vite)', 'Split code by routes/pages using dynamic imports', 'Remove dead code and unused libraries', 'Use bundle analyzers to identify large unused dependencies' ], moreInfo: 'JavaScript is the most expensive resource - removing unused code has immediate impact' }], ['render-blocking-resources', { title: '🚫 Eliminate render-blocking resources', impact: 'high', effort: 'low', category: 'performance', howToFix: [ 'Inline critical CSS in <head>', 'Load non-critical CSS asynchronously with rel="preload"', 'Defer non-critical JavaScript', 'Use resource hints like dns-prefetch and preconnect' ], moreInfo: 'Render-blocking resources delay First Contentful Paint' }], ['unminified-css', { title: '🗜️ Minify CSS', impact: 'low', effort: 'low', category: 'performance', howToFix: [ 'Use CSS minification tools (cssnano, clean-css)', 'Enable minification in your build process', 'Configure your bundler to minify CSS in production', 'Use automated deployment pipelines with minification' ], moreInfo: 'CSS minification is a quick win with automated tools' }], ['unminified-javascript', { title: '🗜️ Minify JavaScript', impact: 'medium', effort: 'low', category: 'performance', howToFix: [ 'Use JavaScript minification (Terser, UglifyJS)', 'Enable minification in production builds', 'Configure bundlers for automatic minification', 'Use modern compression techniques' ], moreInfo: 'JavaScript minification reduces file sizes and parsing time' }], ['efficiently-encode-images', { title: '🖼️ Use efficient image formats', impact: 'high', effort: 'medium', category: 'performance', howToFix: [ 'Convert images to WebP/AVIF formats', 'Use responsive images with srcset', 'Implement lazy loading for images', 'Optimize image dimensions for actual display size' ], moreInfo: 'Modern image formats can reduce file sizes by 25-50%' }], ['uses-text-compression', { title: '📦 Enable text compression', impact: 'high', effort: 'low', category: 'performance', howToFix: [ 'Enable Gzip/Brotli compression on your server', 'Configure CDN compression settings', 'Ensure HTML, CSS, JS, and JSON are compressed', 'Use compression middleware in your application' ], moreInfo: 'Text compression can reduce file sizes by 60-80%' }], ['uses-responsive-images', { title: '📱 Use responsive images', impact: 'medium', effort: 'medium', category: 'performance', howToFix: [ 'Implement srcset attribute for different screen sizes', 'Use picture element for art direction', 'Generate multiple image sizes in build process', 'Consider using image CDN services' ], moreInfo: 'Responsive images prevent loading oversized images on small screens' }], ['offscreen-images', { title: '👁️ Defer offscreen images', impact: 'medium', effort: 'low', category: 'performance', howToFix: [ 'Implement native lazy loading with loading="lazy"', 'Use Intersection Observer API for custom lazy loading', 'Prioritize above-the-fold images', 'Consider progressive image loading' ], moreInfo: 'Lazy loading images improves initial page load time' }], // Accessibility audits ['color-contrast', { title: '🎨 Ensure sufficient color contrast', impact: 'high', effort: 'low', category: 'accessibility', howToFix: [ 'Use contrast checking tools (WebAIM, Stark)', 'Ensure 4.5:1 ratio for normal text, 3:1 for large text', 'Test with different color blindness simulators', 'Create a consistent color palette with good contrast' ], moreInfo: 'Good contrast helps users with visual impairments' }], ['image-alt', { title: '🖼️ Add alt text to images', impact: 'high', effort: 'low', category: 'accessibility', howToFix: [ 'Add descriptive alt attributes to all images', 'Use empty alt="" for decorative images', 'Describe image content and context', 'Avoid redundant phrases like "image of"' ], moreInfo: 'Alt text is essential for screen readers and SEO' }], // SEO audits ['meta-description', { title: '📝 Add meta description', impact: 'medium', effort: 'low', category: 'seo', howToFix: [ 'Write unique, descriptive meta descriptions (150-160 chars)', 'Include target keywords naturally', 'Make each page description unique', 'Write compelling copy that encourages clicks' ], moreInfo: 'Meta descriptions improve click-through rates from search results' }], ['document-title', { title: '📋 Optimize page title', impact: 'high', effort: 'low', category: 'seo', howToFix: [ 'Write descriptive, unique titles for each page', 'Keep titles under 60 characters', 'Include primary keywords near the beginning', 'Use consistent title structure across site' ], moreInfo: 'Page titles are crucial for search rankings and user experience' }] ]); } generateRecommendations(data) { const lighthouse = data.lighthouseResult; if (!lighthouse) { throw new Error('No Lighthouse data available'); } const recommendations = []; const audits = lighthouse.audits || {}; const performanceScore = lighthouse.categories?.performance?.score || 0; // Process failed audits and opportunities Object.entries(audits).forEach(([auditId, audit]) => { if (!audit || audit.score === 1 || audit.score === null) return; const baseRecommendation = this.auditMappings.get(auditId); if (!baseRecommendation) return; const recommendation = { id: auditId, title: baseRecommendation.title || audit.title, description: audit.description || 'No description available', impact: baseRecommendation.impact || 'medium', effort: baseRecommendation.effort || 'medium', priority: this.calculatePriority(audit, baseRecommendation), category: baseRecommendation.category || 'performance', potentialSavings: audit.displayValue || undefined, howToFix: baseRecommendation.howToFix || ['Check Lighthouse documentation for specific guidance'], moreInfo: baseRecommendation.moreInfo }; recommendations.push(recommendation); }); // Sort by priority (highest first) recommendations.sort((a, b) => b.priority - a.priority); // Identify quick wins (low effort, medium+ impact) const quickWins = recommendations.filter(r => r.effort === 'low' && (r.impact === 'high' || r.impact === 'medium')); const summary = this.generateSummary(recommendations, performanceScore); return { url: lighthouse.requestedUrl || 'Unknown', strategy: lighthouse.configSettings?.formFactor || 'unknown', overallScore: Math.round(performanceScore * 100), recommendations, quickWins, summary }; } calculatePriority(audit, baseRec) { let priority = 50; // Base priority // Impact scoring switch (baseRec.impact) { case 'high': priority += 30; break; case 'medium': priority += 20; break; case 'low': priority += 10; break; } // Effort scoring (less effort = higher priority) switch (baseRec.effort) { case 'low': priority += 20; break; case 'medium': priority += 10; break; case 'high': priority += 5; break; } // Audit score impact (lower score = higher priority) const score = audit.score || 0; priority += (1 - score) * 20; // Potential savings bonus if (audit.details?.type === 'opportunity') { priority += 15; } return Math.min(Math.max(Math.round(priority), 1), 100); } generateSummary(recommendations, performanceScore) { const highPriority = recommendations.filter(r => r.priority >= 80).length; const mediumPriority = recommendations.filter(r => r.priority >= 50 && r.priority < 80).length; const lowPriority = recommendations.filter(r => r.priority < 50).length; let estimatedImpact = 'Low'; if (performanceScore < 0.5 && highPriority > 3) { estimatedImpact = 'Very High'; } else if (performanceScore < 0.7 && highPriority > 1) { estimatedImpact = 'High'; } else if (highPriority > 0 || mediumPriority > 2) { estimatedImpact = 'Medium'; } return { totalRecommendations: recommendations.length, highPriority, mediumPriority, lowPriority, estimatedImpact }; } formatRecommendations(report) { let output = `# 🚀 Performance Recommendations\n\n`; output += `**URL:** ${report.url}\n`; output += `**Current Score:** ${report.overallScore}/100\n`; output += `**Device:** ${report.strategy}\n\n`; // Summary output += `## 📊 Summary\n`; output += `- **Total Recommendations:** ${report.summary.totalRecommendations}\n`; output += `- **High Priority:** ${report.summary.highPriority} 🔴\n`; output += `- **Medium Priority:** ${report.summary.mediumPriority} 🟡\n`; output += `- **Low Priority:** ${report.summary.lowPriority} 🟢\n`; output += `- **Estimated Impact:** ${report.summary.estimatedImpact}\n\n`; // Quick wins if (report.quickWins.length > 0) { output += `## ⚡ Quick Wins (Low Effort, High Impact)\n`; report.quickWins.slice(0, 5).forEach((rec, i) => { output += `### ${i + 1}. ${rec.title}\n`; output += `**Priority Score:** ${rec.priority}/100 | **Impact:** ${rec.impact} | **Effort:** ${rec.effort}\n\n`; output += `${rec.description}\n\n`; output += `**How to fix:**\n`; rec.howToFix.forEach(fix => output += `- ${fix}\n`); output += `\n`; if (rec.potentialSavings) { output += `**Potential Savings:** ${rec.potentialSavings}\n\n`; } }); } // All recommendations by priority output += `## 📋 All Recommendations (Priority Order)\n`; report.recommendations.slice(0, 10).forEach((rec, i) => { const priorityEmoji = rec.priority >= 80 ? '🔴' : rec.priority >= 50 ? '🟡' : '🟢'; output += `### ${i + 1}. ${rec.title} ${priorityEmoji}\n`; output += `**Score:** ${rec.priority}/100 | **Category:** ${rec.category} | **Impact:** ${rec.impact} | **Effort:** ${rec.effort}\n\n`; if (rec.potentialSavings) { output += `**Potential Savings:** ${rec.potentialSavings}\n\n`; } output += `${rec.description}\n\n`; output += `**Action Steps:**\n`; rec.howToFix.slice(0, 3).forEach(fix => output += `- ${fix}\n`); if (rec.moreInfo) { output += `\n*💡 ${rec.moreInfo}*\n`; } output += `\n---\n\n`; }); return output; } } //# sourceMappingURL=recommendations.js.map