@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
JavaScript
/**
* 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;
}