Files
Web-Page-Performance-Test/lib/har-parser.js

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
};