UNPKG

@bschauer/webtools-mcp-server

Version:

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

299 lines (267 loc) 9.24 kB
/** * Core Web Vitals Analyzer * Analyzes Core Web Vitals metrics from Performance Observer data */ import { logInfo, logError } from "../../../utils/logging.js"; /** * Analyze Web Vitals data from samples * @param {Array} samples - Web Vitals data samples * @returns {Object} Web Vitals analysis */ export function analyzeWebVitalsData(samples) { try { if (!samples || !Array.isArray(samples) || samples.length === 0) { return { error: "No Web Vitals data available", }; } // Extract metrics from samples const lcpValues = samples.map((sample) => sample.lcp).filter(Boolean); const clsValues = samples.map((sample) => sample.cls).filter(Boolean); const fidValues = samples.map((sample) => sample.fid).filter(Boolean); const inpValues = samples.map((sample) => sample.inp).filter(Boolean); const ttfbValues = samples.map((sample) => sample.ttfb).filter(Boolean); // Calculate LCP analysis const lcpAnalysis = lcpValues.length > 0 ? { value: calculateAverage(lcpValues), min: Math.min(...lcpValues), max: Math.max(...lcpValues), median: calculateMedian(lcpValues), p75: calculatePercentile(lcpValues, 75), p95: calculatePercentile(lcpValues, 95), rating: getRatingForLCP(calculateAverage(lcpValues)), samples: lcpValues, } : null; // Calculate CLS analysis const clsAnalysis = clsValues.length > 0 ? { value: calculateAverage(clsValues), min: Math.min(...clsValues), max: Math.max(...clsValues), median: calculateMedian(clsValues), p75: calculatePercentile(clsValues, 75), p95: calculatePercentile(clsValues, 95), rating: getRatingForCLS(calculateAverage(clsValues)), samples: clsValues, } : null; // Calculate FID analysis const fidAnalysis = fidValues.length > 0 ? { value: calculateAverage(fidValues), min: Math.min(...fidValues), max: Math.max(...fidValues), median: calculateMedian(fidValues), p75: calculatePercentile(fidValues, 75), p95: calculatePercentile(fidValues, 95), rating: getRatingForFID(calculateAverage(fidValues)), samples: fidValues, } : null; // Calculate INP analysis const inpAnalysis = inpValues.length > 0 ? { value: calculateAverage(inpValues), min: Math.min(...inpValues), max: Math.max(...inpValues), median: calculateMedian(inpValues), p75: calculatePercentile(inpValues, 75), p95: calculatePercentile(inpValues, 95), rating: getRatingForINP(calculateAverage(inpValues)), samples: inpValues, } : null; // Calculate TTFB analysis const ttfbAnalysis = ttfbValues.length > 0 ? { value: calculateAverage(ttfbValues), min: Math.min(...ttfbValues), max: Math.max(...ttfbValues), median: calculateMedian(ttfbValues), p75: calculatePercentile(ttfbValues, 75), p95: calculatePercentile(ttfbValues, 95), rating: getRatingForTTFB(calculateAverage(ttfbValues)), samples: ttfbValues, } : null; // Calculate overall score const overallScore = calculateOverallScore({ lcp: lcpAnalysis, cls: clsAnalysis, fid: fidAnalysis, inp: inpAnalysis, ttfb: ttfbAnalysis, }); return { lcp: lcpAnalysis, cls: clsAnalysis, fid: fidAnalysis, inp: inpAnalysis, ttfb: ttfbAnalysis, overallScore, sampleCount: samples.length, }; } catch (error) { logError("web_vitals_analyzer", "Failed to analyze Web Vitals data", error); return { error: `Failed to analyze Web Vitals data: ${error.message}`, }; } } /** * Calculate average of an array of numbers * @param {Array<number>} values - Array of numbers * @returns {number} Average value */ function calculateAverage(values) { if (!values || values.length === 0) return 0; return values.reduce((sum, value) => sum + value, 0) / values.length; } /** * Calculate median of an array of numbers * @param {Array<number>} values - Array of numbers * @returns {number} Median value */ function calculateMedian(values) { if (!values || values.length === 0) return 0; const sortedValues = [...values].sort((a, b) => a - b); const mid = Math.floor(sortedValues.length / 2); return sortedValues.length % 2 === 0 ? (sortedValues[mid - 1] + sortedValues[mid]) / 2 : sortedValues[mid]; } /** * Calculate percentile of an array of numbers * @param {Array<number>} values - Array of numbers * @param {number} percentile - Percentile to calculate (0-100) * @returns {number} Percentile value */ function calculatePercentile(values, percentile) { if (!values || values.length === 0) return 0; const sortedValues = [...values].sort((a, b) => a - b); const index = Math.ceil((percentile / 100) * sortedValues.length) - 1; return sortedValues[Math.max(0, Math.min(index, sortedValues.length - 1))]; } /** * Get rating for LCP value * @param {number} lcp - LCP value in milliseconds * @returns {string} Rating (good, needs-improvement, poor) */ function getRatingForLCP(lcp) { if (lcp <= 2500) return "good"; if (lcp <= 4000) return "needs-improvement"; return "poor"; } /** * Get rating for CLS value * @param {number} cls - CLS value * @returns {string} Rating (good, needs-improvement, poor) */ function getRatingForCLS(cls) { if (cls <= 0.1) return "good"; if (cls <= 0.25) return "needs-improvement"; return "poor"; } /** * Get rating for FID value * @param {number} fid - FID value in milliseconds * @returns {string} Rating (good, needs-improvement, poor) */ function getRatingForFID(fid) { if (fid <= 100) return "good"; if (fid <= 300) return "needs-improvement"; return "poor"; } /** * Get rating for INP value * @param {number} inp - INP value in milliseconds * @returns {string} Rating (good, needs-improvement, poor) */ function getRatingForINP(inp) { if (inp <= 200) return "good"; if (inp <= 500) return "needs-improvement"; return "poor"; } /** * Get rating for TTFB value * @param {number} ttfb - TTFB value in milliseconds * @returns {string} Rating (good, needs-improvement, poor) */ function getRatingForTTFB(ttfb) { if (ttfb <= 800) return "good"; if (ttfb <= 1800) return "needs-improvement"; return "poor"; } /** * Calculate overall performance score based on Core Web Vitals * @param {Object} metrics - Core Web Vitals metrics * @returns {number} Overall score (0-100) */ function calculateOverallScore(metrics) { // Define weights for each metric const weights = { lcp: 0.25, cls: 0.25, inp: 0.25, // Prefer INP over FID as it's the newer metric fid: 0.15, // Use FID as fallback if INP is not available ttfb: 0.1, }; let totalScore = 0; let totalWeight = 0; // Calculate score for LCP if (metrics.lcp) { const lcpScore = calculateMetricScore(metrics.lcp.value, [2500, 4000], false); totalScore += lcpScore * weights.lcp; totalWeight += weights.lcp; } // Calculate score for CLS if (metrics.cls) { const clsScore = calculateMetricScore(metrics.cls.value, [0.1, 0.25], false); totalScore += clsScore * weights.cls; totalWeight += weights.cls; } // Calculate score for INP or FID if (metrics.inp) { const inpScore = calculateMetricScore(metrics.inp.value, [200, 500], false); totalScore += inpScore * weights.inp; totalWeight += weights.inp; } else if (metrics.fid) { const fidScore = calculateMetricScore(metrics.fid.value, [100, 300], false); totalScore += fidScore * weights.fid; totalWeight += weights.fid; } // Calculate score for TTFB if (metrics.ttfb) { const ttfbScore = calculateMetricScore(metrics.ttfb.value, [800, 1800], false); totalScore += ttfbScore * weights.ttfb; totalWeight += weights.ttfb; } // Normalize score based on available metrics return totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0; } /** * Calculate score for a metric based on thresholds * @param {number} value - Metric value * @param {Array<number>} thresholds - Thresholds for needs-improvement and poor ratings * @param {boolean} higherIsBetter - Whether higher values are better * @returns {number} Score (0-1) */ function calculateMetricScore(value, thresholds, higherIsBetter = false) { const [goodThreshold, poorThreshold] = thresholds; if (higherIsBetter) { // Higher values are better (e.g., throughput) if (value >= goodThreshold) return 1; if (value <= poorThreshold) return 0; return (value - poorThreshold) / (goodThreshold - poorThreshold); } else { // Lower values are better (e.g., latency) if (value <= goodThreshold) return 1; if (value >= poorThreshold) return 0; return 1 - (value - goodThreshold) / (poorThreshold - goodThreshold); } }