mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 20:05:58 +00:00
Add Export APIs & Optimization Checks (Phases 14-15)
Features Added: - HAR file export endpoint (/api/export/:testId/har) - CSV metrics export endpoint (/api/export/:testId/csv) - Optimization checker analyzing 8 categories: * Image optimization * Text compression (gzip/brotli) * Cache policies * Render-blocking resources * Unused JavaScript * Unused CSS * HTTP/2 adoption * Code minification - Frontend optimization checklist with color-coded warnings - Export buttons integrated into results UI Technical Implementation: - Created lib/optimization-checker.js with Lighthouse audit analysis - Added optimization score calculation (0-100%) - Potential time savings displayed for each check - Export buttons wired to download endpoints - Optimization data saved alongside each test result
This commit is contained in:
20
index.html
20
index.html
@@ -105,6 +105,26 @@
|
|||||||
<canvas id="breakdown-chart" style="max-width: 500px; margin: 0 auto;"></canvas>
|
<canvas id="breakdown-chart" style="max-width: 500px; margin: 0 auto;"></canvas>
|
||||||
<div id="breakdown-stats" style="margin-top: 1rem;"></div>
|
<div id="breakdown-stats" style="margin-top: 1rem;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Optimization Checklist -->
|
||||||
|
<div id="optimization-checklist" style="margin-top: 2rem; display: none;">
|
||||||
|
<h3>Optimization Opportunities</h3>
|
||||||
|
<div id="optimization-score" style="font-size: 2rem; font-weight: bold; text-align: center; margin: 1rem 0;"></div>
|
||||||
|
<div id="optimization-items"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Buttons -->
|
||||||
|
<div id="export-buttons" style="margin-top: 2rem; display: none; text-align: center;">
|
||||||
|
<h3>Export Results</h3>
|
||||||
|
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<a id="export-har" href="#" class="button" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-bg-tertiary); color: var(--color-text-primary); text-decoration: none; border-radius: 8px; border: 2px solid var(--color-border);">
|
||||||
|
📦 Download HAR
|
||||||
|
</a>
|
||||||
|
<a id="export-csv" href="#" class="button" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-bg-tertiary); color: var(--color-text-primary); text-decoration: none; border-radius: 8px; border: 2px solid var(--color-border);">
|
||||||
|
📊 Download CSV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
|
|||||||
134
lib/optimization-checker.js
Normal file
134
lib/optimization-checker.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -150,6 +150,12 @@ async function _executeTest(url, options) {
|
|||||||
const harData = harParser.parseHAR(lhr);
|
const harData = harParser.parseHAR(lhr);
|
||||||
const harPath = path.join(reportDir, `${testId}.har.json`);
|
const harPath = path.join(reportDir, `${testId}.har.json`);
|
||||||
fs.writeFileSync(harPath, JSON.stringify(harData, null, 2));
|
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();
|
await chrome.kill();
|
||||||
|
|
||||||
|
|||||||
21
main.js
21
main.js
@@ -122,6 +122,14 @@ function displayResults(data) {
|
|||||||
if (typeof renderContentBreakdown === 'function') {
|
if (typeof renderContentBreakdown === 'function') {
|
||||||
renderContentBreakdown(data.id);
|
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');
|
resultsArea.classList.add('visible');
|
||||||
|
|
||||||
@@ -238,9 +246,18 @@ function getUserUuid() {
|
|||||||
let uuid = localStorage.getItem('user_uuid');
|
let uuid = localStorage.getItem('user_uuid');
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
uuid = crypto.randomUUID();
|
uuid = crypto.randomUUID();
|
||||||
localStorage.setItem('user_uuid', uuid);
|
});
|
||||||
|
|
||||||
|
if (data.checks.length === 0) {
|
||||||
|
html = '<p style="text-align: center; color: var(--color-text-secondary);">✅ All optimization checks passed!</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsEl.innerHTML = html;
|
||||||
|
container.style.display = 'block';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load optimizations:', error);
|
||||||
}
|
}
|
||||||
return uuid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
|
|||||||
32
server.js
32
server.js
@@ -62,6 +62,38 @@ app.get("/api/history", async (req, res) => {
|
|||||||
res.json(history);
|
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
|
// Serve index.html for all other routes
|
||||||
app.get("*", (req, res) => {
|
app.get("*", (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "index.html"));
|
res.sendFile(path.join(__dirname, "index.html"));
|
||||||
|
|||||||
Reference in New Issue
Block a user