mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 11:55:59 +00:00
Phase A.4 - Add Page Images Gallery with optimization analysis
This commit is contained in:
172
images-gallery.js
Normal file
172
images-gallery.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Images Gallery - Display all loaded images with optimization analysis
|
||||
*/
|
||||
|
||||
async function init() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const testId = params.get('id');
|
||||
|
||||
if (!testId) {
|
||||
document.getElementById('imagesGrid').innerHTML =
|
||||
'<p style="color: red;">Error: No test ID provided</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/reports/${testId}.har.json`);
|
||||
if (!response.ok) throw new Error('HAR data not found');
|
||||
|
||||
const harData = await response.json();
|
||||
renderImageGallery(harData);
|
||||
} catch (error) {
|
||||
document.getElementById('imagesGrid').innerHTML =
|
||||
`<p style="color: red;">Error loading images: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderImageGallery(harData) {
|
||||
// Filter for image requests
|
||||
const images = harData.entries.filter(entry =>
|
||||
entry.resourceType === 'Image' ||
|
||||
entry.mimeType?.startsWith('image/') ||
|
||||
entry.url.match(/\.(jpg|jpeg|png|gif|webp|svg|ico|bmp)($|\?)/i)
|
||||
);
|
||||
|
||||
if (images.length === 0) {
|
||||
document.getElementById('imagesGrid').innerHTML =
|
||||
'<p>No images found in this page load.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate summary stats
|
||||
const totalSize = images.reduce((sum, img) => sum + img.size.transferSize, 0);
|
||||
const unoptimized = images.filter(img => analyzeOptimization(img).level === 'error').length;
|
||||
const avgSize = totalSize / images.length;
|
||||
|
||||
// Render summary
|
||||
document.getElementById('summaryStats').innerHTML = `
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${images.length}</div>
|
||||
<div class="stat-label">Total Images</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${formatBytes(totalSize)}</div>
|
||||
<div class="stat-label">Total Size</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${formatBytes(avgSize)}</div>
|
||||
<div class="stat-label">Average Size</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">${unoptimized}</div>
|
||||
<div class="stat-label">Needs Optimization</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Render image cards
|
||||
let html = '';
|
||||
images.forEach(image => {
|
||||
const optimization = analyzeOptimization(image);
|
||||
const filename = extractFilename(image.url);
|
||||
const format = getImageFormat(image);
|
||||
|
||||
html += `
|
||||
<div class="image-card">
|
||||
<div class="image-preview">
|
||||
<img src="${image.url}" alt="${filename}" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||
<div class="image-icon" style="display: none;">🖼️</div>
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<div class="image-url" title="${image.url}">${filename}</div>
|
||||
<div class="image-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Format</span>
|
||||
<span class="detail-value">${format}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Size</span>
|
||||
<span class="detail-value">${formatBytes(image.size.transferSize)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Compression</span>
|
||||
<span class="detail-value">${(image.size.compressionRatio * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Load Time</span>
|
||||
<span class="detail-value">${image.timing.total.toFixed(0)}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="optimization-badge badge-${optimization.level}">
|
||||
${optimization.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById('imagesGrid').innerHTML = html;
|
||||
}
|
||||
|
||||
function analyzeOptimization(image) {
|
||||
const size = image.size.transferSize;
|
||||
const format = getImageFormat(image);
|
||||
|
||||
// Check if modern format (WebP, AVIF)
|
||||
if (format === 'WebP' || format === 'AVIF') {
|
||||
return { level: 'good', message: '✓ Modern format' };
|
||||
}
|
||||
|
||||
// Check size thresholds
|
||||
if (size > 500 * 1024) { // > 500KB
|
||||
return { level: 'error', message: '⚠️ Very large - optimize!' };
|
||||
}
|
||||
|
||||
if (size > 200 * 1024) { // > 200KB
|
||||
return { level: 'warning', message: '⚠️ Could be smaller' };
|
||||
}
|
||||
|
||||
// Check if SVG (good for icons/logos)
|
||||
if (format === 'SVG') {
|
||||
return { level: 'good', message: '✓ Vector (scalable)' };
|
||||
}
|
||||
|
||||
return { level: 'good', message: '✓ Optimized' };
|
||||
}
|
||||
|
||||
function getImageFormat(image) {
|
||||
const url = image.url.toLowerCase();
|
||||
const mime = image.mimeType?.toLowerCase();
|
||||
|
||||
if (mime?.includes('webp') || url.includes('.webp')) return 'WebP';
|
||||
if (mime?.includes('avif') || url.includes('.avif')) return 'AVIF';
|
||||
if (mime?.includes('svg') || url.includes('.svg')) return 'SVG';
|
||||
if (mime?.includes('png') || url.includes('.png')) return 'PNG';
|
||||
if (mime?.includes('gif') || url.includes('.gif')) return 'GIF';
|
||||
if (mime?.includes('jpeg') || mime?.includes('jpg') || url.match(/\.jpe?g/)) return 'JPEG';
|
||||
if (url.includes('.ico')) return 'ICO';
|
||||
if (url.includes('.bmp')) return 'BMP';
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
function extractFilename(url) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname;
|
||||
const filename = pathname.split('/').pop() || pathname;
|
||||
return filename.length > 40 ? filename.substring(0, 37) + '...' : filename;
|
||||
} catch {
|
||||
return url.substring(0, 40) + '...';
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
160
images.html
Normal file
160
images.html
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Image Gallery - Web Page Performance Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.images-container {
|
||||
max-width: 1400px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.images-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.image-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.image-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(114, 9, 183, 0.2);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.image-info {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.image-url {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.optimization-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.badge-good {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="images-container">
|
||||
<h1>Page Images Gallery</h1>
|
||||
<p>All images loaded by this page with optimization analysis</p>
|
||||
|
||||
<div class="summary-stats" id="summaryStats">
|
||||
<!-- Generated by JS -->
|
||||
</div>
|
||||
|
||||
<div class="images-grid" id="imagesGrid">
|
||||
<!-- Generated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="images-gallery.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -100,9 +100,12 @@
|
||||
|
||||
<!-- Waterfall Link -->
|
||||
<div style="margin-top: 1.5rem; display: none;" id="waterfall-link-container">
|
||||
<a href="#" id="view-waterfall" class="button" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-accent); color: white; text-decoration: none; border-radius: 8px; font-weight: 600;">
|
||||
<a href="#" id="view-waterfall" class="button" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-accent); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; margin-right: 1rem;">
|
||||
📊 View Waterfall Chart
|
||||
</a>
|
||||
<a href="#" id="view-images" class="button" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--color-accent); color: white; text-decoration: none; border-radius: 8px; font-weight: 600;">
|
||||
🖼️ View Image Gallery
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Content Breakdown -->
|
||||
|
||||
Reference in New Issue
Block a user