mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 20:05:58 +00:00
210 lines
6.0 KiB
JavaScript
210 lines
6.0 KiB
JavaScript
/**
|
|
* HAR Parser - Extract network request data from Lighthouse results
|
|
* Provides timing, size, and metadata for waterfall visualization
|
|
*/
|
|
|
|
/**
|
|
* Parse Lighthouse result and generate HAR-compatible data
|
|
* @param {object} lighthouseResult - Full Lighthouse result object
|
|
* @returns {object} HAR data structure
|
|
*/
|
|
function parseHAR(lighthouseResult) {
|
|
const audits = lighthouseResult.audits;
|
|
const networkRequests = audits['network-requests']?.details?.items || [];
|
|
|
|
const entries = networkRequests.map((request, index) => {
|
|
const timing = calculateTiming(request);
|
|
|
|
return {
|
|
requestId: index + 1,
|
|
url: request.url,
|
|
method: request.requestMethod || 'GET',
|
|
status: request.statusCode,
|
|
mimeType: request.mimeType || request.resourceType,
|
|
resourceType: request.resourceType,
|
|
protocol: request.protocol,
|
|
priority: request.priority,
|
|
|
|
// Connection/Socket information for connection view
|
|
socket: request.connectionId || request.socket || null,
|
|
connectionReused: request.connectionReused || false,
|
|
|
|
// Timing breakdown (in milliseconds)
|
|
timing: {
|
|
dns: timing.dns,
|
|
connect: timing.connect,
|
|
ssl: timing.ssl,
|
|
send: timing.send,
|
|
wait: timing.wait,
|
|
receive: timing.receive,
|
|
total: timing.total,
|
|
startTime: request.startTime,
|
|
endTime: request.endTime
|
|
},
|
|
|
|
// Size information (in bytes)
|
|
size: {
|
|
transferSize: request.transferSize || 0,
|
|
resourceSize: request.resourceSize || 0,
|
|
compressionRatio: request.resourceSize > 0
|
|
? (request.transferSize / request.resourceSize).toFixed(2)
|
|
: 1
|
|
},
|
|
|
|
// Additional metadata
|
|
isSecure: request.url?.startsWith('https://'),
|
|
domain: extractDomain(request.url),
|
|
isThirdParty: checkIfThirdParty(request.url, lighthouseResult.finalUrl),
|
|
renderBlocking: request.renderBlocking === 'blocking',
|
|
|
|
// HTTP Headers
|
|
requestHeaders: request.requestHeaders || {},
|
|
responseHeaders: request.responseHeaders || {},
|
|
|
|
// Extracted from response headers if available
|
|
cacheControl: request.responseHeaders?.['cache-control'],
|
|
contentEncoding: request.responseHeaders?.['content-encoding']
|
|
};
|
|
});
|
|
|
|
return {
|
|
entries,
|
|
summary: calculateSummary(entries),
|
|
pageMetrics: extractPageMetrics(lighthouseResult)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate timing breakdown for a single request
|
|
*/
|
|
function calculateTiming(request) {
|
|
const start = request.startTime || 0;
|
|
const end = request.endTime || start;
|
|
const total = (end - start) * 1000; // Convert to ms
|
|
|
|
// Parse timing object if available
|
|
const timing = request.timing || {};
|
|
|
|
return {
|
|
dns: timing.dnsEnd >= 0 ? (timing.dnsEnd - timing.dnsStart) : -1,
|
|
connect: timing.connectEnd >= 0 ? (timing.connectEnd - timing.connectStart) : -1,
|
|
ssl: timing.sslEnd >= 0 ? (timing.sslEnd - timing.sslStart) : -1,
|
|
send: timing.sendEnd >= 0 ? (timing.sendEnd - timing.sendStart) : -1,
|
|
wait: timing.receiveHeadersEnd >= 0 ? (timing.receiveHeadersEnd - timing.sendEnd) : -1,
|
|
receive: -1, // Calculated from total - (dns + connect + ssl + send + wait)
|
|
total: total > 0 ? total : (request.networkEndTime - request.networkRequestTime) * 1000 || 0,
|
|
startTime: start,
|
|
endTime: end
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate summary statistics
|
|
*/
|
|
function calculateSummary(entries) {
|
|
const byType = {};
|
|
let totalSize = 0;
|
|
let totalTransfer = 0;
|
|
|
|
entries.forEach(entry => {
|
|
const type = entry.resourceType || 'Other';
|
|
|
|
if (!byType[type]) {
|
|
byType[type] = { count: 0, size: 0, transfer: 0 };
|
|
}
|
|
|
|
byType[type].count++;
|
|
byType[type].size += entry.size.resourceSize;
|
|
byType[type].transfer += entry.size.transferSize;
|
|
|
|
totalSize += entry.size.resourceSize;
|
|
totalTransfer += entry.size.transferSize;
|
|
});
|
|
|
|
return {
|
|
totalRequests: entries.length,
|
|
totalSize,
|
|
totalTransfer,
|
|
byType,
|
|
compressionSavings: totalSize > 0 ? ((totalSize - totalTransfer) / totalSize * 100).toFixed(1) : 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract key page metrics from Lighthouse
|
|
*/
|
|
function extractPageMetrics(lighthouseResult) {
|
|
const audits = lighthouseResult.audits;
|
|
|
|
return {
|
|
firstContentfulPaint: audits['first-contentful-paint']?.numericValue,
|
|
largestContentfulPaint: audits['largest-contentful-paint']?.numericValue,
|
|
cumulativeLayoutShift: audits['cumulative-layout-shift']?.numericValue,
|
|
totalBlockingTime: audits['total-blocking-time']?.numericValue,
|
|
speedIndex: audits['speed-index']?.numericValue,
|
|
timeToInteractive: audits['interactive']?.numericValue,
|
|
domContentLoaded: lighthouseResult.audits['metrics']?.details?.items?.[0]?.observedDomContentLoaded,
|
|
loadEventEnd: lighthouseResult.audits['metrics']?.details?.items?.[0]?.observedLoad
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract domain from URL
|
|
*/
|
|
function extractDomain(url) {
|
|
try {
|
|
const urlObj = new URL(url);
|
|
return urlObj.hostname;
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if request is third-party
|
|
*/
|
|
function checkIfThirdParty(requestUrl, pageUrl) {
|
|
const requestDomain = extractDomain(requestUrl);
|
|
const pageDomain = extractDomain(pageUrl);
|
|
return requestDomain !== pageDomain;
|
|
}
|
|
|
|
/**
|
|
* Group entries by domain
|
|
*/
|
|
function groupByDomain(entries) {
|
|
const domains = {};
|
|
|
|
entries.forEach(entry => {
|
|
const domain = entry.domain;
|
|
if (!domains[domain]) {
|
|
domains[domain] = {
|
|
count: 0,
|
|
size: 0,
|
|
transfer: 0,
|
|
requests: []
|
|
};
|
|
}
|
|
|
|
domains[domain].count++;
|
|
domains[domain].size += entry.size.resourceSize;
|
|
domains[domain].transfer += entry.size.transferSize;
|
|
domains[domain].requests.push(entry.requestId);
|
|
});
|
|
|
|
// Sort by transfer size (descending)
|
|
return Object.entries(domains)
|
|
.sort((a, b) => b[1].transfer - a[1].transfer)
|
|
.reduce((acc, [domain, data]) => {
|
|
acc[domain] = data;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
module.exports = {
|
|
parseHAR,
|
|
calculateTiming,
|
|
calculateSummary,
|
|
groupByDomain
|
|
};
|