UNPKG

@bschauer/webtools-mcp-server

Version:

MCP server providing web analysis tools including screenshot, debug, performance, security, accessibility, SEO, and asset optimization capabilities

341 lines (290 loc) 11 kB
/** * Resource loading analysis module */ import { determineResourceType, groupResourcesByType, generateResourceOptimizationSuggestions } from "./utils.js"; /** * Analyze resource loading in the trace data * @param {Array} events - All trace events * @returns {Object|null} Resource loading analysis or null if no resources found */ export function analyzeResourceLoading(events) { // Find resource events const resourceEvents = events.filter((event) => event.name === "ResourceReceiveResponse" || event.name === "ResourceFinish" || event.name === "ResourceSendRequest"); if (resourceEvents.length === 0) { return null; } // Extract resource information const resources = extractResourceInfo(resourceEvents); // Find large resources const largeResources = resources.filter((resource) => resource.size > 500000); // 500KB // Group resources by type const resourcesByType = groupResourcesByType(resources); // Generate optimization suggestions const optimizationSuggestions = generateResourceOptimizationSuggestions(resources); // Analyze resource loading waterfall const waterfall = analyzeResourceWaterfall(resources, events); return { type: "large_resources", description: largeResources.length > 0 ? `Found ${largeResources.length} large resources (>500KB)` : `Analyzed ${resources.length} resources`, details: { resources: largeResources, resourcesByType, optimizationSuggestions, waterfall, totalResources: resources.length, totalSize: resources.reduce((sum, r) => sum + (r.size || 0), 0), totalTransferSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0), totalDuration: resources.reduce((sum, r) => sum + (r.duration || 0), 0), }, }; } /** * Extract resource information from resource events * @param {Array} resourceEvents - Resource-related events * @returns {Array} Resource information */ function extractResourceInfo(resourceEvents) { const resources = []; const resourceMap = {}; // First pass: collect basic information for (const event of resourceEvents) { if (event.name === "ResourceSendRequest") { const url = event.args?.data?.url; if (url) { resourceMap[url] = resourceMap[url] || { url, requestTime: event.ts, type: determineResourceType(url), size: 0, transferSize: 0, duration: 0, status: 0, mimeType: "", priority: event.args?.data?.priority || "Low", }; } } else if (event.name === "ResourceReceiveResponse") { const url = event.args?.data?.url; if (url && resourceMap[url]) { resourceMap[url].responseTime = event.ts; resourceMap[url].status = event.args?.data?.statusCode || 0; resourceMap[url].mimeType = event.args?.data?.mimeType || ""; resourceMap[url].fromCache = event.args?.data?.fromCache || false; resourceMap[url].fromServiceWorker = event.args?.data?.fromServiceWorker || false; } } else if (event.name === "ResourceFinish") { const url = event.args?.data?.url; if (url && resourceMap[url]) { resourceMap[url].finishTime = event.ts; resourceMap[url].encodedDataLength = event.args?.data?.encodedDataLength || 0; resourceMap[url].decodedBodyLength = event.args?.data?.decodedBodyLength || 0; // Calculate size and duration resourceMap[url].size = resourceMap[url].decodedBodyLength || resourceMap[url].encodedDataLength || 0; resourceMap[url].transferSize = resourceMap[url].encodedDataLength || 0; if (resourceMap[url].requestTime && resourceMap[url].finishTime) { resourceMap[url].duration = (resourceMap[url].finishTime - resourceMap[url].requestTime) / 1000; // Convert to ms } // Add to resources array resources.push(resourceMap[url]); } } } return resources; } /** * Analyze resource loading waterfall * @param {Array} resources - Resource information * @param {Array} events - All trace events * @returns {Object} Waterfall analysis */ function analyzeResourceWaterfall(resources, events) { // Sort resources by request time const sortedResources = [...resources].sort((a, b) => a.requestTime - b.requestTime); // Calculate critical path const criticalPath = calculateCriticalPath(sortedResources); // Calculate resource contention const contention = calculateResourceContention(sortedResources); // Find render-blocking resources const renderBlockingResources = findRenderBlockingResources(sortedResources, events); return { criticalPath, contention, renderBlockingResources, timeToFirstByte: calculateTimeToFirstByte(sortedResources), loadSequence: sortedResources.map((r) => ({ url: r.url, type: r.type, startTime: r.requestTime, endTime: r.finishTime, duration: r.duration, size: r.size, transferSize: r.transferSize, status: r.status, priority: r.priority, fromCache: r.fromCache, fromServiceWorker: r.fromServiceWorker, })), }; } /** * Calculate the critical path of resource loading * @param {Array} resources - Resource information * @returns {Array} Critical path resources */ function calculateCriticalPath(resources) { // This is a simplified version - in a real implementation, we would need to analyze dependencies // For now, consider high-priority resources and large resources on the critical path const criticalResources = resources.filter((r) => r.priority === "High" || r.priority === "VeryHigh" || (r.type === "script" && r.size > 100000) || (r.type === "style" && r.size > 50000)); return criticalResources.map((r) => ({ url: r.url, type: r.type, duration: r.duration, size: r.size, priority: r.priority, })); } /** * Calculate resource contention * @param {Array} resources - Resource information * @returns {Object} Resource contention analysis */ function calculateResourceContention(resources) { // Count the maximum number of concurrent requests let maxConcurrent = 0; let currentConcurrent = 0; // Create events for request start and end const events = []; for (const resource of resources) { if (resource.requestTime && resource.finishTime) { events.push({ time: resource.requestTime, type: "start", resource, }); events.push({ time: resource.finishTime, type: "end", resource, }); } } // Sort events by time events.sort((a, b) => a.time - b.time); // Process events to find maximum concurrency const concurrentRequests = []; for (const event of events) { if (event.type === "start") { currentConcurrent++; concurrentRequests.push(event.resource); if (currentConcurrent > maxConcurrent) { maxConcurrent = currentConcurrent; } } else if (event.type === "end") { currentConcurrent--; // Remove this resource from concurrent requests const index = concurrentRequests.findIndex((r) => r.url === event.resource.url); if (index !== -1) { concurrentRequests.splice(index, 1); } } } return { maxConcurrentRequests: maxConcurrent, contentionPeriods: findContentionPeriods(events), }; } /** * Find periods of high resource contention * @param {Array} events - Resource start/end events * @returns {Array} Contention periods */ function findContentionPeriods(events) { const contentionPeriods = []; let currentConcurrent = 0; let contentionStart = null; // Process events to find periods of high contention for (const event of events) { if (event.type === "start") { currentConcurrent++; // Start a contention period if we reach 6 concurrent requests if (currentConcurrent >= 6 && contentionStart === null) { contentionStart = event.time; } } else if (event.type === "end") { currentConcurrent--; // End a contention period if we drop below 6 concurrent requests if (currentConcurrent < 6 && contentionStart !== null) { contentionPeriods.push({ startTime: contentionStart, endTime: event.time, duration: (event.time - contentionStart) / 1000, // Convert to ms concurrentRequests: currentConcurrent + 1, // +1 because we just decremented }); contentionStart = null; } } } // If we still have an open contention period, close it with the last event if (contentionStart !== null && events.length > 0) { contentionPeriods.push({ startTime: contentionStart, endTime: events[events.length - 1].time, duration: (events[events.length - 1].time - contentionStart) / 1000, // Convert to ms concurrentRequests: currentConcurrent, }); } return contentionPeriods; } /** * Find render-blocking resources * @param {Array} resources - Resource information * @param {Array} events - All trace events * @returns {Array} Render-blocking resources */ function findRenderBlockingResources(resources, events) { // Find the first paint event const firstPaintEvents = events.filter((e) => e.name === "firstPaint" || e.name === "firstContentfulPaint" || e.name.includes("MarkFirstPaint")); if (firstPaintEvents.length === 0) { return []; } // Sort by timestamp firstPaintEvents.sort((a, b) => a.ts - b.ts); const firstPaintTime = firstPaintEvents[0].ts; // Find resources that finished loading after the first paint // and are of a type that could block rendering const blockingResources = resources.filter( (r) => r.finishTime > firstPaintTime && (r.type === "script" || r.type === "style" || r.type === "font") && !r.fromCache && // Cached resources don't block rendering r.priority !== "Low" // Low priority resources are usually async/defer ); return blockingResources.map((r) => ({ url: r.url, type: r.type, finishTime: r.finishTime, delayToFirstPaint: (r.finishTime - firstPaintTime) / 1000, // Convert to ms size: r.size, priority: r.priority, })); } /** * Calculate time to first byte * @param {Array} resources - Resource information * @returns {number} Time to first byte in milliseconds */ function calculateTimeToFirstByte(resources) { // Find the main document resource (usually the first HTML resource) const documentResources = resources.filter((r) => r.type === "document" || r.mimeType.includes("html")); if (documentResources.length === 0) { return null; } // Sort by request time documentResources.sort((a, b) => a.requestTime - b.requestTime); const mainDocument = documentResources[0]; // Calculate TTFB if we have both request and response times if (mainDocument.requestTime && mainDocument.responseTime) { return (mainDocument.responseTime - mainDocument.requestTime) / 1000; // Convert to ms } return null; }