UNPKG

@bschauer/webtools-mcp-server

Version:

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

350 lines (297 loc) 12.8 kB
/** * CSS variables impact analysis module */ /** * Analyze CSS variables impact in the trace data * @param {Array} styleEvents - Style-related events * @param {Array} allEvents - All trace events * @returns {Object} CSS variables impact analysis */ export function analyzeCssVariablesImpact(styleEvents, allEvents) { // In a real implementation, we would need to analyze the actual CSS // This is a simplified version that looks for patterns suggesting CSS variable usage // Look for recalculate style events that might be triggered by CSS variable changes const variableChangePatterns = detectCssVariableChangePatterns(styleEvents, allEvents); // Analyze the cascade impact of CSS variables const cascadeImpact = analyzeCascadeImpact(styleEvents, allEvents); // Find style recalculation bottlenecks const recalculationBottlenecks = findStyleRecalculationBottlenecks(styleEvents, allEvents); // Generate recommendations const recommendations = generateCssVariableRecommendations(variableChangePatterns, cascadeImpact, recalculationBottlenecks); return { variablesDetected: variableChangePatterns.length > 0, variableChanges: variableChangePatterns, cascadeImpact, recalculationBottlenecks, recommendations, }; } /** * Detect patterns suggesting CSS variable changes * @param {Array} styleEvents - Style-related events * @param {Array} allEvents - All trace events * @returns {Array} CSS variable change patterns */ function detectCssVariableChangePatterns(styleEvents, allEvents) { const variableChangePatterns = []; // Look for clusters of style recalculations const recalcStyleClusters = findRecalcStyleClusters(styleEvents); // For each cluster, try to find the JavaScript that might have triggered it for (const cluster of recalcStyleClusters) { // Look for JavaScript events that completed shortly before this cluster const jsEvents = allEvents.filter( (e) => (e.name === "V8.Execute" || e.name === "FunctionCall" || e.name === "EvaluateScript") && e.ts + e.dur <= cluster.startTime && e.ts + e.dur > cluster.startTime - 100000 // 100ms in microseconds ); if (jsEvents.length > 0) { // Sort by proximity to the cluster jsEvents.sort((a, b) => cluster.startTime - (a.ts + a.dur) - (cluster.startTime - (b.ts + b.dur))); const jsEvent = jsEvents[0]; // Closest JS event // Check if this JavaScript might be changing CSS variables // This is a heuristic - in a real implementation, we would need to analyze the actual code const mightChangeCssVariables = jsEvent.args?.data?.functionName?.includes("style") || jsEvent.args?.data?.functionName?.includes("css") || jsEvent.args?.data?.functionName?.includes("theme") || jsEvent.args?.data?.functionName?.includes("color") || jsEvent.args?.data?.functionName?.includes("var"); if (mightChangeCssVariables) { variableChangePatterns.push({ cluster: { startTime: cluster.startTime, endTime: cluster.endTime, eventCount: cluster.events.length, totalDuration: cluster.totalDuration / 1000, // Convert to ms }, javascript: { time: jsEvent.ts, duration: jsEvent.dur / 1000, functionName: jsEvent.args?.data?.functionName || "anonymous", url: jsEvent.args?.data?.url || jsEvent.args?.data?.fileName || "unknown", lineNumber: jsEvent.args?.data?.lineNumber, columnNumber: jsEvent.args?.data?.columnNumber, }, timeBetween: (cluster.startTime - (jsEvent.ts + jsEvent.dur)) / 1000, impactScore: (cluster.events.length * cluster.totalDuration) / 1000000, // Heuristic impact score }); } } } return variableChangePatterns; } /** * Find clusters of style recalculation events * @param {Array} styleEvents - Style-related events * @returns {Array} Clusters of style recalculation events */ function findRecalcStyleClusters(styleEvents) { const clusters = []; const CLUSTER_THRESHOLD_US = 50000; // 50ms in microseconds // Sort events by timestamp const sortedEvents = [...styleEvents].sort((a, b) => a.ts - b.ts); let currentCluster = null; for (const event of sortedEvents) { if (event.name !== "RecalculateStyles") { continue; } if (!currentCluster) { // Start a new cluster currentCluster = { startTime: event.ts, endTime: event.ts + (event.dur || 0), events: [event], totalDuration: event.dur || 0, }; } else if (event.ts - currentCluster.endTime <= CLUSTER_THRESHOLD_US) { // Add to current cluster currentCluster.events.push(event); currentCluster.endTime = Math.max(currentCluster.endTime, event.ts + (event.dur || 0)); currentCluster.totalDuration += event.dur || 0; } else { // End current cluster and start a new one clusters.push(currentCluster); currentCluster = { startTime: event.ts, endTime: event.ts + (event.dur || 0), events: [event], totalDuration: event.dur || 0, }; } } // Add the last cluster if it exists if (currentCluster) { clusters.push(currentCluster); } return clusters; } /** * Analyze the cascade impact of CSS variables * @param {Array} styleEvents - Style-related events * @param {Array} allEvents - All trace events * @returns {Object} Cascade impact analysis */ function analyzeCascadeImpact(styleEvents, allEvents) { // This is a simplified version - in a real implementation, we would need to analyze the actual CSS // Count the number of style recalculations const recalcStyleCount = styleEvents.filter((e) => e.name === "RecalculateStyles").length; // Count the number of layout updates const layoutUpdateCount = allEvents.filter((e) => e.name === "Layout" || e.name === "UpdateLayoutTree").length; // Estimate the number of affected elements // This is a heuristic - in a real implementation, we would need to analyze the actual DOM const estimatedAffectedElements = Math.min(recalcStyleCount * 10, 1000); // Assume each recalculation affects 10 elements, capped at 1000 return { recalcStyleCount, layoutUpdateCount, estimatedAffectedElements, cascadeDepthEstimate: estimateStyleCascadeDepth(styleEvents, allEvents), cascadeWidthEstimate: estimateStyleCascadeWidth(styleEvents, allEvents), }; } /** * Estimate the depth of the style cascade * @param {Array} styleEvents - Style-related events * @param {Array} allEvents - All trace events * @returns {number} Estimated cascade depth */ function estimateStyleCascadeDepth(styleEvents, allEvents) { // This is a simplified version - in a real implementation, we would need to analyze the actual CSS // Look for long style recalculations const longRecalcs = styleEvents.filter((e) => e.name === "RecalculateStyles" && e.dur > 5000); // 5ms in microseconds if (longRecalcs.length === 0) { return 1; // Shallow cascade } // Estimate cascade depth based on duration const maxDuration = Math.max(...longRecalcs.map((e) => e.dur || 0)); // Heuristic: longer durations suggest deeper cascades if (maxDuration > 20000) { // 20ms return 4; // Very deep cascade } else if (maxDuration > 10000) { // 10ms return 3; // Deep cascade } else if (maxDuration > 5000) { // 5ms return 2; // Moderate cascade } else { return 1; // Shallow cascade } } /** * Estimate the width of the style cascade * @param {Array} styleEvents - Style-related events * @param {Array} allEvents - All trace events * @returns {number} Estimated cascade width */ function estimateStyleCascadeWidth(styleEvents, allEvents) { // This is a simplified version - in a real implementation, we would need to analyze the actual CSS // Count the number of style recalculations const recalcStyleCount = styleEvents.filter((e) => e.name === "RecalculateStyles").length; // Heuristic: more recalculations suggest wider cascades if (recalcStyleCount > 100) { return 4; // Very wide cascade } else if (recalcStyleCount > 50) { return 3; // Wide cascade } else if (recalcStyleCount > 20) { return 2; // Moderate cascade } else { return 1; // Narrow cascade } } /** * Find style recalculation bottlenecks * @param {Array} styleEvents - Style-related events * @param {Array} allEvents - All trace events * @returns {Array} Style recalculation bottlenecks */ function findStyleRecalculationBottlenecks(styleEvents, allEvents) { const bottlenecks = []; // Find long style recalculations const longRecalcs = styleEvents.filter((e) => e.name === "RecalculateStyles" && e.dur > 5000); // 5ms in microseconds if (longRecalcs.length === 0) { return bottlenecks; } // Sort by duration longRecalcs.sort((a, b) => (b.dur || 0) - (a.dur || 0)); // Take the top 5 longest recalculations for (const recalc of longRecalcs.slice(0, 5)) { // Try to find the JavaScript that might have triggered this recalculation const jsEvents = allEvents.filter( (e) => (e.name === "V8.Execute" || e.name === "FunctionCall" || e.name === "EvaluateScript") && e.ts + e.dur <= recalc.ts && e.ts + e.dur > recalc.ts - 50000 // 50ms in microseconds ); let jsEvent = null; if (jsEvents.length > 0) { // Sort by proximity to the recalculation jsEvents.sort((a, b) => recalc.ts - (a.ts + a.dur) - (recalc.ts - (b.ts + b.dur))); jsEvent = jsEvents[0]; // Closest JS event } bottlenecks.push({ recalculation: { time: recalc.ts, duration: recalc.dur / 1000, // Convert to ms args: recalc.args, }, javascript: jsEvent ? { time: jsEvent.ts, duration: jsEvent.dur / 1000, functionName: jsEvent.args?.data?.functionName || "anonymous", url: jsEvent.args?.data?.url || jsEvent.args?.data?.fileName || "unknown", lineNumber: jsEvent.args?.data?.lineNumber, columnNumber: jsEvent.args?.data?.columnNumber, } : null, timeBetween: jsEvent ? (recalc.ts - (jsEvent.ts + jsEvent.dur)) / 1000 : null, }); } return bottlenecks; } /** * Generate recommendations for CSS variables * @param {Array} variableChangePatterns - CSS variable change patterns * @param {Object} cascadeImpact - Cascade impact analysis * @param {Array} recalculationBottlenecks - Style recalculation bottlenecks * @returns {Array} Recommendations */ function generateCssVariableRecommendations(variableChangePatterns, cascadeImpact, recalculationBottlenecks) { const recommendations = []; // Check for frequent CSS variable changes if (variableChangePatterns.length > 5) { recommendations.push({ type: "css_variable_change_frequency", description: `High frequency of CSS variable changes (${variableChangePatterns.length} detected)`, recommendation: "Batch CSS variable changes and minimize the frequency of updates to reduce style recalculations", }); } // Check for wide cascade impact if (cascadeImpact.cascadeWidthEstimate >= 3) { recommendations.push({ type: "css_variable_cascade_width", description: `Wide cascade impact (affecting approximately ${cascadeImpact.estimatedAffectedElements} elements)`, recommendation: "Limit the scope of CSS variables by using more specific selectors or consider CSS containment", }); } // Check for deep cascade impact if (cascadeImpact.cascadeDepthEstimate >= 3) { recommendations.push({ type: "css_variable_cascade_depth", description: "Deep cascade impact detected", recommendation: "Flatten your CSS hierarchy and reduce the depth of nested CSS variables", }); } // Check for style recalculation bottlenecks if (recalculationBottlenecks.length > 0) { const longestBottleneck = recalculationBottlenecks[0]; recommendations.push({ type: "style_recalculation_bottleneck", description: `Style recalculation bottleneck detected (${longestBottleneck.recalculation.duration.toFixed(2)}ms)`, recommendation: "Identify and optimize the CSS variables that trigger expensive style recalculations", }); if (longestBottleneck.javascript) { recommendations.push({ type: "specific_css_variable_optimization", description: `Function ${longestBottleneck.javascript.functionName} in ${longestBottleneck.javascript.url} may be triggering expensive style recalculations`, recommendation: "Review this function and optimize how it updates CSS variables", }); } } return recommendations; }