Files
Web-Page-Performance-Test/vitals.html

194 lines
8.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Web Vitals - Web Page Performance Test</title>
<link rel="icon" type="image/png" href="Logo.png">
<link rel="stylesheet" href="styles.css?v=2.2">
<style>
.vitals-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.metric-block {
background: var(--color-bg-tertiary);
padding: 2rem;
border-radius: 12px;
border: 1px solid var(--color-border);
margin-bottom: 2rem;
}
.metric-title {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border);
padding-bottom: 0.5rem;
}
.lcp-element-box {
background: rgba(0,0,0,0.3);
padding: 1rem;
border-radius: 6px;
font-family: monospace;
overflow-x: auto;
color: #a5d6ff;
}
.cls-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.cls-table th, .cls-table td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
.cls-table th { color: var(--color-text-secondary); }
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1 class="title">Advanced Vitals Analysis</h1>
<nav>
<a href="/" class="btn-secondary">⬅ Dashboard</a>
</nav>
</header>
<div class="vitals-container" id="content-area">
<div id="loading" style="text-align: center;">Loading Vitals Data...</div>
<!-- LCP Section -->
<div id="lcp-section" class="metric-block" style="display:none;">
<h2 class="metric-title">Largest Contentful Paint (LCP)</h2>
<div style="display: flex; gap: 2rem; flex-wrap: wrap;">
<div style="flex: 1; min-width: 300px;">
<h3>LCP Element</h3>
<div id="lcp-element-code" class="lcp-element-box"></div>
</div>
<div style="flex: 1; min-width: 300px;">
<h3>Details</h3>
<p><strong>Load Time:</strong> <span id="lcp-time" style="color: var(--color-accent); font-weight: bold;"></span></p>
<p><strong>Phase Breakdown:</strong></p>
<ul id="lcp-phases" style="list-style: none; padding-left: 0.5rem; line-height: 1.8;"></ul>
</div>
</div>
</div>
<!-- CLS Section -->
<div id="cls-section" class="metric-block" style="display:none;">
<h2 class="metric-title">Cumulative Layout Shift (CLS)</h2>
<h3>Shift Events</h3>
<table class="cls-table">
<thead>
<tr>
<th>Time</th>
<th>Score</th>
<th>Moved Elements</th>
</tr>
</thead>
<tbody id="cls-tbody"></tbody>
</table>
</div>
<!-- Long Tasks Section -->
<div id="tbt-section" class="metric-block" style="display:none;">
<h2 class="metric-title">Total Blocking Time (TBT) & Long Tasks</h2>
<div id="long-tasks-list"></div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) {
document.getElementById('loading').textContent = 'No Test ID provided.';
return;
}
try {
const response = await fetch(`/reports/${id}.json`);
if (!response.ok) throw new Error('Report not found');
const data = await response.json();
renderVitals(data);
} catch (e) {
document.getElementById('loading').textContent = 'Error: ' + e.message;
}
});
function renderVitals(data) {
document.getElementById('loading').style.display = 'none';
// Check both root details (file format) and metrics.details (DB format)
const details = data.details || (data.metrics && data.metrics.details) || {};
// LCP
const lcpSec = document.getElementById('lcp-section');
if (details.lcpElement) {
lcpSec.style.display = 'block';
document.getElementById('lcp-element-code').textContent = details.lcpElement.node?.snippet || 'N/A';
document.getElementById('lcp-time').textContent = data.metrics.lcp.toFixed(0) + 'ms';
// phases if extracted, otherwise extract from audits in future
}
// CLS
const clsSec = document.getElementById('cls-section');
if (details.clsShifts && details.clsShifts.length > 0) {
clsSec.style.display = 'block';
const tbody = document.getElementById('cls-tbody');
details.clsShifts.forEach(shift => {
const row = document.createElement('tr');
// Handle different Lighthouse versions / audit structures
// Sometimes it's shift.score, sometimes it's inside an object.
// Usually: { score: 0.05, startTime: 1200, impactedNodes: [...] }
const time = (shift.startTime !== undefined) ? (shift.startTime).toFixed(0) + 'ms' : '-';
const score = (shift.score !== undefined) ? shift.score.toFixed(4) : (shift.value ? shift.value.toFixed(4) : '0');
let nodes = 'Unknown';
// layout-shifts audit typically uses 'items' with 'node' or 'caused shifts'
// but often it's just `items` array of shifts.
if (shift.impactedNodes) {
nodes = shift.impactedNodes.map(n => n.node?.snippet || n.node?.selector || 'Node').join('<br>');
} else if (shift.node) {
nodes = shift.node.snippet || shift.node.selector || 'Node';
}
row.innerHTML = `<td>${time}</td><td>${score}</td><td>${nodes}</td>`;
tbody.appendChild(row);
});
}
// TBT / Long Tasks
const tbtSec = document.getElementById('tbt-section');
if (details.longTasks && details.longTasks.length > 0) {
tbtSec.style.display = 'block';
const list = document.getElementById('long-tasks-list');
// Sort by duration desc
const tasks = details.longTasks.sort((a,b) => b.duration - a.duration).slice(0, 10);
tasks.forEach(task => {
const div = document.createElement('div');
div.style.marginBottom = '0.5rem';
div.style.padding = '0.5rem';
div.style.background = 'rgba(255,100,100,0.1)';
div.style.borderLeft = '3px solid red';
div.innerHTML = `<strong>${task.duration.toFixed(0)}ms</strong> at ${task.startTime.toFixed(0)}ms - ${task.url || 'Script Evaluation'}`;
list.appendChild(div);
});
}
if (!details.lcpElement && (!details.clsShifts || details.clsShifts.length === 0) && (!details.longTasks || details.longTasks.length === 0)) {
document.getElementById('content-area').innerHTML += '<p>No advanced details available for this test. (Test might be old or failed to extract details)</p>';
}
}
</script>
</body>
</html>