diff --git a/index.html b/index.html index 37dd8d4..bbc168b 100644 --- a/index.html +++ b/index.html @@ -105,6 +105,26 @@
+ + + + + + diff --git a/lib/optimization-checker.js b/lib/optimization-checker.js new file mode 100644 index 0000000..1ddc79c --- /dev/null +++ b/lib/optimization-checker.js @@ -0,0 +1,134 @@ +/** + * Optimization Checker + * Analyzes Lighthouse results and provides actionable recommendations + */ + +function analyzeOptimizations(lighthouseResult) { + const audits = lighthouseResult.audits; + const checks = []; + + // 1. Image Optimization + const unoptimizedImages = audits['uses-optimized-images']; + if (unoptimizedImages && unoptimizedImages.score < 1) { + checks.push({ + category: 'Images', + status: 'warning', + title: 'Unoptimized Images Detected', + description: `${unoptimizedImages.details?.items?.length || 0} images could be optimized`, + savings: unoptimizedImages.numericValue || 0, + impact: 'high' + }); + } + + // 2. Text Compression + const textCompression = audits['uses-text-compression']; + if (textCompression && textCompression.score < 1) { + checks.push({ + category: 'Compression', + status: 'warning', + title: 'Enable Text Compression', + description: 'Text resources not served with compression (gzip/brotli)', + savings: textCompression.numericValue || 0, + impact: 'medium' + }); + } + + // 3. Cache Policy + const cachePolicy = audits['uses-long-cache-ttl']; + if (cachePolicy && cachePolicy.score < 0.9) { + checks.push({ + category: 'Caching', + status: 'info', + title: 'Serve Static Assets with Efficient Cache Policy', + description: `${cachePolicy.details?.items?.length || 0} resources have low cache TTL`, + savings: cachePolicy.numericValue || 0, + impact: 'medium' + }); + } + + // 4. Render-Blocking Resources + const renderBlocking = audits['render-blocking-resources']; + if (renderBlocking && renderBlocking.score < 1) { + checks.push({ + category: 'Performance', + status: 'error', + title: 'Eliminate Render-Blocking Resources', + description: `${renderBlocking.details?.items?.length || 0} render-blocking resources found`, + savings: renderBlocking.numericValue || 0, + impact: 'high' + }); + } + + // 5. Unused JavaScript + const unusedJS = audits['unused-javascript']; + if (unusedJS && unusedJS.score < 0.9) { + checks.push({ + category: 'JavaScript', + status: 'warning', + title: 'Reduce Unused JavaScript', + description: 'Defer or remove unused JS code', + savings: unusedJS.numericValue || 0, + impact: 'high' + }); + } + + // 6. Unused CSS + const unusedCSS = audits['unused-css-rules']; + if (unusedCSS && unusedCSS.score < 0.9) { + checks.push({ + category: 'CSS', + status: 'warning', + title: 'Remove Unused CSS', + description: 'Defer or remove unused CSS rules', + savings: unusedCSS.numericValue || 0, + impact: 'medium' + }); + } + + // 7. HTTP/2 + const http2 = audits['uses-http2']; + if (http2 && http2.score < 1) { + checks.push({ + category: 'Protocol', + status: 'info', + title: 'Use HTTP/2', + description: 'Serve resources over HTTP/2 for better performance', + impact: 'medium' + }); + } + + // 8. Minification + const minifyCSS = audits['unminified-css']; + const minifyJS = audits['unminified-javascript']; + if ((minifyCSS && minifyCSS.score < 1) || (minifyJS && minifyJS.score < 1)) { + checks.push({ + category: 'Minification', + status: 'warning', + title: 'Minify CSS and JavaScript', + description: 'Minify code to reduce file sizes', + savings: (minifyCSS?.numericValue || 0) + (minifyJS?.numericValue || 0), + impact: 'medium' + }); + } + + // Calculate overall optimization score + const totalChecks = 8; + const passed = checks.filter(c => c.status === 'success').length; + const warnings = checks.filter(c => c.status === 'warning').length; + const errors = checks.filter(c => c.status === 'error').length; + + return { + checks, + summary: { + totalChecks, + passed: totalChecks - checks.length, + warnings, + errors, + score: Math.round(((totalChecks - checks.length) / totalChecks) * 100) + } + }; +} + +module.exports = { + analyzeOptimizations +}; diff --git a/lib/runner.js b/lib/runner.js index 6abfa8c..8350a32 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -150,6 +150,12 @@ async function _executeTest(url, options) { const harData = harParser.parseHAR(lhr); const harPath = path.join(reportDir, `${testId}.har.json`); fs.writeFileSync(harPath, JSON.stringify(harData, null, 2)); + + // Run optimization checks + const optimizationChecker = require('./optimization-checker'); + const optimizations = optimizationChecker.analyzeOptimizations(lhr); + const optPath = path.join(reportDir, `${testId}.optimizations.json`); + fs.writeFileSync(optPath, JSON.stringify(optimizations, null, 2)); await chrome.kill(); diff --git a/main.js b/main.js index 222e3e7..5a71da3 100644 --- a/main.js +++ b/main.js @@ -122,6 +122,14 @@ function displayResults(data) { if (typeof renderContentBreakdown === 'function') { renderContentBreakdown(data.id); } + + // Load and display optimizations + loadOptimizations(data.id); + + // Wire export buttons + document.getElementById('export-buttons').style.display = 'block'; + document.getElementById('export-har').href = `/api/export/${data.id}/har`; + document.getElementById('export-csv').href = `/api/export/${data.id}/csv`; resultsArea.classList.add('visible'); @@ -238,9 +246,18 @@ function getUserUuid() { let uuid = localStorage.getItem('user_uuid'); if (!uuid) { uuid = crypto.randomUUID(); - localStorage.setItem('user_uuid', uuid); + }); + + if (data.checks.length === 0) { + html = '

✅ All optimization checks passed!

'; + } + + itemsEl.innerHTML = html; + container.style.display = 'block'; + + } catch (error) { + console.error('Failed to load optimizations:', error); } - return uuid; } // Initialization diff --git a/server.js b/server.js index 3e9de17..c4a67b1 100644 --- a/server.js +++ b/server.js @@ -62,6 +62,38 @@ app.get("/api/history", async (req, res) => { res.json(history); }); +// API Endpoint: Export HAR +app.get("/api/export/:testId/har", (req, res) => { + const { testId } = req.params; + const harPath = path.join(__dirname, 'reports', `${testId}.har.json`); + + if (!fs.existsSync(harPath)) { + return res.status(404).json({ error: "HAR file not found" }); + } + + res.download(harPath, `test-${testId}.har`, (err) => { + if (err) console.error("Download error:", err); + }); +}); + +// API Endpoint: Export CSV +app.get("/api/export/:testId/csv", (req, res) => { + const { testId } = req.params; + const jsonPath = path.join(__dirname, 'reports', `${testId}.json`); + + if (!fs.existsSync(jsonPath)) { + return res.status(404).json({ error: "Test results not found" }); + } + + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + const csv = `URL,Timestamp,Device,Performance,Accessibility,Best Practices,SEO,LCP,CLS,TBT +${data.url},${data.timestamp},${data.isMobile ? 'Mobile' : 'Desktop'},${data.scores.performance},${data.scores.accessibility},${data.scores.bestPractices},${data.scores.seo},${data.metrics.lcp},${data.metrics.cls},${data.metrics.tbt}`; + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="test-${testId}.csv"`); + res.send(csv); +}); + // Serve index.html for all other routes app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "index.html"));