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>
|
||||
<div id="breakdown-stats" style="margin-top: 1rem;"></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>
|
||||
|
||||
<!-- 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
|
||||
};
|
||||
@@ -151,6 +151,12 @@ async function _executeTest(url, options) {
|
||||
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();
|
||||
|
||||
// Cleanup User Data Dir
|
||||
|
||||
21
main.js
21
main.js
@@ -123,6 +123,14 @@ function displayResults(data) {
|
||||
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');
|
||||
|
||||
// Scroll to results
|
||||
@@ -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 = '<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
|
||||
|
||||
32
server.js
32
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"));
|
||||
|
||||
Reference in New Issue
Block a user