UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

801 lines (693 loc) 27.1 kB
/** * Performance Validator for Quality Gates * * Comprehensive performance validation and benchmarking: * - Bundle size analysis * - Runtime performance metrics * - Memory usage validation * - Loading time benchmarks * - Core Web Vitals compliance * - Lighthouse score validation */ const EventEmitter = require('events'); class PerformanceValidator extends EventEmitter { constructor(config = {}) { super(); this.config = { // Performance thresholds maxBundleSize: config.maxBundleSize || 2048, // 2MB maxInitialLoadTime: config.maxInitialLoadTime || 3000, // 3 seconds maxMemoryUsage: config.maxMemoryUsage || 100, // 100MB maxCpuUsage: config.maxCpuUsage || 80, // 80% // Core Web Vitals thresholds maxLCP: config.maxLCP || 2500, // Largest Contentful Paint (ms) maxFID: config.maxFID || 100, // First Input Delay (ms) maxCLS: config.maxCLS || 0.1, // Cumulative Layout Shift // Lighthouse thresholds minPerformanceScore: config.minPerformanceScore || 90, minAccessibilityScore: config.minAccessibilityScore || 90, minBestPracticesScore: config.minBestPracticesScore || 90, minSEOScore: config.minSEOScore || 90, // Validation settings enableBundleAnalysis: config.enableBundleAnalysis !== false, enableRuntimeMetrics: config.enableRuntimeMetrics !== false, enableWebVitals: config.enableWebVitals !== false, enableLighthouseScores: config.enableLighthouseScores || false, ...config }; // Performance metrics storage this.metrics = { current: {}, baseline: {}, history: [] }; // Statistics this.stats = { totalValidations: 0, passedValidations: 0, failedValidations: 0, averageScore: 0, regressionCount: 0 }; } /** * Validate performance of artifact */ async validatePerformance(artifact, context = {}) { const startTime = Date.now(); console.log('⚡ Starting performance validation...'); try { const validationResults = { passed: true, issues: [], metrics: {}, scores: {}, recommendations: [], summary: { performance: 0, accessibility: 0, bestPractices: 0, seo: 0 } }; // Bundle size analysis if (this.config.enableBundleAnalysis) { const bundleResults = await this._analyzeBundleSize(artifact, context); this._mergeValidationResults(validationResults, bundleResults); } // Runtime performance metrics if (this.config.enableRuntimeMetrics) { const runtimeResults = await this._measureRuntimeMetrics(artifact, context); this._mergeValidationResults(validationResults, runtimeResults); } // Core Web Vitals validation if (this.config.enableWebVitals) { const webVitalsResults = await this._validateWebVitals(artifact, context); this._mergeValidationResults(validationResults, webVitalsResults); } // Lighthouse scores if (this.config.enableLighthouseScores) { const lighthouseResults = await this._runLighthouseAudit(artifact, context); this._mergeValidationResults(validationResults, lighthouseResults); } // Calculate overall performance score validationResults.scores.overall = this._calculateOverallScore(validationResults); // Determine pass/fail validationResults.passed = this._determinePassFail(validationResults); // Generate performance recommendations validationResults.recommendations = this._generatePerformanceRecommendations(validationResults); // Check for performance regressions const regressionCheck = await this._checkForRegressions(validationResults, context); if (regressionCheck.hasRegression) { validationResults.passed = false; validationResults.issues.push(...regressionCheck.issues); } const validationTime = Date.now() - startTime; // Store current metrics this.metrics.current = validationResults.metrics; this.metrics.history.push({ timestamp: Date.now(), metrics: validationResults.metrics, scores: validationResults.scores, passed: validationResults.passed }); // Update statistics this.stats.totalValidations++; if (validationResults.passed) { this.stats.passedValidations++; } else { this.stats.failedValidations++; } this._updateAverageScore(validationResults.scores.overall); console.log(`⚡ Performance validation complete: ${validationResults.passed ? 'PASSED' : 'FAILED'} (Score: ${validationResults.scores.overall}) in ${validationTime}ms`); this.emit('validation:completed', { passed: validationResults.passed, score: validationResults.scores.overall, issueCount: validationResults.issues.length, validationTime }); return { ...validationResults, validationTime, metadata: { validatorVersion: '1.0.0', thresholds: this._getConfiguredThresholds(), validationTypes: this._getEnabledValidationTypes() } }; } catch (error) { console.error(`❌ Performance validation failed: ${error.message}`); this.emit('validation:failed', { error: error.message }); return { passed: false, issues: [{ type: 'validation_error', severity: 'high', title: 'Performance validation failed', description: error.message, recommendation: 'Fix validation configuration and retry' }], metrics: {}, scores: { overall: 0 }, validationTime: Date.now() - startTime }; } } /** * Analyze bundle size and composition */ async _analyzeBundleSize(artifact, context) { console.log(' 📦 Analyzing bundle size...'); const analysis = { type: 'bundle_analysis', issues: [], metrics: {} }; // Estimate bundle size from artifact const bundleInfo = this._estimateBundleSize(artifact, context); analysis.metrics.bundleSize = bundleInfo; // Check against thresholds if (bundleInfo.totalSize > this.config.maxBundleSize * 1024) { // Convert KB to bytes analysis.issues.push({ type: 'bundle_size_exceeded', severity: 'high', title: 'Bundle Size Exceeds Limit', description: `Bundle size (${Math.round(bundleInfo.totalSize / 1024)}KB) exceeds limit (${this.config.maxBundleSize}KB)`, recommendation: 'Optimize bundle size through code splitting, tree shaking, or dependency reduction', current: bundleInfo.totalSize, threshold: this.config.maxBundleSize * 1024 }); } // Check for large dependencies if (bundleInfo.largeDependencies && bundleInfo.largeDependencies.length > 0) { bundleInfo.largeDependencies.forEach(dep => { analysis.issues.push({ type: 'large_dependency', severity: 'medium', title: `Large Dependency: ${dep.name}`, description: `Dependency ${dep.name} contributes ${Math.round(dep.size / 1024)}KB to bundle`, recommendation: 'Consider alternatives or dynamic imports for large dependencies', dependency: dep.name, size: dep.size }); }); } // Check for duplicate dependencies if (bundleInfo.duplicates && bundleInfo.duplicates.length > 0) { analysis.issues.push({ type: 'duplicate_dependencies', severity: 'medium', title: 'Duplicate Dependencies Detected', description: `Found ${bundleInfo.duplicates.length} duplicate dependencies`, recommendation: 'Remove duplicate dependencies or use webpack-bundle-analyzer', duplicates: bundleInfo.duplicates }); } return analysis; } /** * Measure runtime performance metrics */ async _measureRuntimeMetrics(artifact, context) { console.log(' 🚀 Measuring runtime metrics...'); const metrics = { type: 'runtime_metrics', issues: [], metrics: {} }; // Simulate runtime metrics (in a real implementation, this would run actual tests) const runtimeData = await this._simulateRuntimeMetrics(artifact, context); metrics.metrics.runtime = runtimeData; // Check load time if (runtimeData.initialLoadTime > this.config.maxInitialLoadTime) { metrics.issues.push({ type: 'slow_load_time', severity: 'high', title: 'Initial Load Time Too Slow', description: `Initial load time (${runtimeData.initialLoadTime}ms) exceeds threshold (${this.config.maxInitialLoadTime}ms)`, recommendation: 'Optimize initial loading through code splitting and lazy loading', current: runtimeData.initialLoadTime, threshold: this.config.maxInitialLoadTime }); } // Check memory usage if (runtimeData.memoryUsage > this.config.maxMemoryUsage * 1024 * 1024) { // Convert MB to bytes metrics.issues.push({ type: 'high_memory_usage', severity: 'medium', title: 'High Memory Usage', description: `Memory usage (${Math.round(runtimeData.memoryUsage / 1024 / 1024)}MB) exceeds threshold (${this.config.maxMemoryUsage}MB)`, recommendation: 'Optimize memory usage by reducing object retention and improving garbage collection', current: runtimeData.memoryUsage, threshold: this.config.maxMemoryUsage * 1024 * 1024 }); } // Check CPU usage if (runtimeData.cpuUsage > this.config.maxCpuUsage) { metrics.issues.push({ type: 'high_cpu_usage', severity: 'medium', title: 'High CPU Usage', description: `CPU usage (${runtimeData.cpuUsage}%) exceeds threshold (${this.config.maxCpuUsage}%)`, recommendation: 'Optimize computational complexity and use web workers for heavy tasks', current: runtimeData.cpuUsage, threshold: this.config.maxCpuUsage }); } return metrics; } /** * Validate Core Web Vitals */ async _validateWebVitals(artifact, context) { console.log(' 📊 Validating Core Web Vitals...'); const webVitals = { type: 'web_vitals', issues: [], metrics: {} }; // Simulate Web Vitals measurements const vitalsData = await this._simulateWebVitals(artifact, context); webVitals.metrics.webVitals = vitalsData; // Check Largest Contentful Paint (LCP) if (vitalsData.lcp > this.config.maxLCP) { webVitals.issues.push({ type: 'poor_lcp', severity: 'high', title: 'Poor Largest Contentful Paint', description: `LCP (${vitalsData.lcp}ms) exceeds threshold (${this.config.maxLCP}ms)`, recommendation: 'Optimize image loading, server response times, and critical resource loading', current: vitalsData.lcp, threshold: this.config.maxLCP, webVital: 'LCP' }); } // Check First Input Delay (FID) if (vitalsData.fid > this.config.maxFID) { webVitals.issues.push({ type: 'poor_fid', severity: 'high', title: 'Poor First Input Delay', description: `FID (${vitalsData.fid}ms) exceeds threshold (${this.config.maxFID}ms)`, recommendation: 'Reduce JavaScript execution time and break up long tasks', current: vitalsData.fid, threshold: this.config.maxFID, webVital: 'FID' }); } // Check Cumulative Layout Shift (CLS) if (vitalsData.cls > this.config.maxCLS) { webVitals.issues.push({ type: 'poor_cls', severity: 'medium', title: 'Poor Cumulative Layout Shift', description: `CLS (${vitalsData.cls}) exceeds threshold (${this.config.maxCLS})`, recommendation: 'Ensure images and ads have defined dimensions and avoid inserting content above existing content', current: vitalsData.cls, threshold: this.config.maxCLS, webVital: 'CLS' }); } return webVitals; } /** * Run Lighthouse audit */ async _runLighthouseAudit(artifact, context) { console.log(' 🏠 Running Lighthouse audit...'); const lighthouse = { type: 'lighthouse_audit', issues: [], metrics: {} }; // Simulate Lighthouse scores const lighthouseData = await this._simulateLighthouseScores(artifact, context); lighthouse.metrics.lighthouse = lighthouseData; // Check performance score if (lighthouseData.performance < this.config.minPerformanceScore) { lighthouse.issues.push({ type: 'low_performance_score', severity: 'high', title: 'Low Lighthouse Performance Score', description: `Performance score (${lighthouseData.performance}) below threshold (${this.config.minPerformanceScore})`, recommendation: 'Improve performance through bundle optimization, image optimization, and caching', current: lighthouseData.performance, threshold: this.config.minPerformanceScore }); } // Check accessibility score if (lighthouseData.accessibility < this.config.minAccessibilityScore) { lighthouse.issues.push({ type: 'low_accessibility_score', severity: 'medium', title: 'Low Lighthouse Accessibility Score', description: `Accessibility score (${lighthouseData.accessibility}) below threshold (${this.config.minAccessibilityScore})`, recommendation: 'Improve accessibility with proper ARIA labels, semantic HTML, and color contrast', current: lighthouseData.accessibility, threshold: this.config.minAccessibilityScore }); } // Check best practices score if (lighthouseData.bestPractices < this.config.minBestPracticesScore) { lighthouse.issues.push({ type: 'low_best_practices_score', severity: 'medium', title: 'Low Lighthouse Best Practices Score', description: `Best Practices score (${lighthouseData.bestPractices}) below threshold (${this.config.minBestPracticesScore})`, recommendation: 'Follow web best practices for security, performance, and modern standards', current: lighthouseData.bestPractices, threshold: this.config.minBestPracticesScore }); } // Check SEO score if (lighthouseData.seo < this.config.minSEOScore) { lighthouse.issues.push({ type: 'low_seo_score', severity: 'low', title: 'Low Lighthouse SEO Score', description: `SEO score (${lighthouseData.seo}) below threshold (${this.config.minSEOScore})`, recommendation: 'Improve SEO with proper meta tags, structured data, and mobile-friendliness', current: lighthouseData.seo, threshold: this.config.minSEOScore }); } return lighthouse; } /** * Check for performance regressions */ async _checkForRegressions(currentResults, context) { const regressionCheck = { hasRegression: false, issues: [] }; if (this.metrics.baseline && Object.keys(this.metrics.baseline).length > 0) { // Compare with baseline const comparison = this._compareWithBaseline(currentResults.metrics); if (comparison.regressions.length > 0) { regressionCheck.hasRegression = true; comparison.regressions.forEach(regression => { regressionCheck.issues.push({ type: 'performance_regression', severity: regression.severity, title: `Performance Regression: ${regression.metric}`, description: `${regression.metric} has regressed by ${regression.change}%`, recommendation: 'Investigate recent changes that may have caused performance regression', metric: regression.metric, current: regression.current, baseline: regression.baseline, change: regression.change }); }); this.stats.regressionCount++; } } return regressionCheck; } // Simulation methods (replace with actual implementations) /** * Estimate bundle size from artifact */ _estimateBundleSize(artifact, context) { const code = this._extractCode(artifact); const codeSize = new Blob([code]).size; // Simulate bundle analysis const estimatedBundle = { totalSize: codeSize * 1.5, // Estimate overhead jsSize: codeSize * 0.8, cssSize: codeSize * 0.1, assetsSize: codeSize * 0.1, largeDependencies: [ // Simulate detection of large dependencies ...(code.includes('react') ? [{ name: 'react', size: 45000 }] : []), ...(code.includes('lodash') ? [{ name: 'lodash', size: 72000 }] : []), ...(code.includes('moment') ? [{ name: 'moment', size: 67000 }] : []) ].filter(dep => dep.size > 50000), // Only large ones duplicates: [] // Would detect duplicates in real implementation }; return estimatedBundle; } /** * Simulate runtime performance metrics */ async _simulateRuntimeMetrics(artifact, context) { const code = this._extractCode(artifact); const complexity = this._estimateComplexity(code); // Simulate metrics based on code complexity return { initialLoadTime: Math.max(800, complexity * 0.1), // Base 800ms + complexity factor timeToInteractive: Math.max(1200, complexity * 0.15), firstContentfulPaint: Math.max(600, complexity * 0.08), memoryUsage: Math.max(20 * 1024 * 1024, complexity * 1000), // Base 20MB + complexity cpuUsage: Math.min(95, Math.max(10, complexity * 0.01)), // 10-95% networkRequests: Math.max(5, Math.floor(complexity * 0.001)) }; } /** * Simulate Core Web Vitals */ async _simulateWebVitals(artifact, context) { const code = this._extractCode(artifact); const complexity = this._estimateComplexity(code); return { lcp: Math.max(1200, complexity * 0.12), // Largest Contentful Paint fid: Math.max(50, complexity * 0.005), // First Input Delay cls: Math.max(0.05, Math.min(0.3, complexity * 0.00001)), // Cumulative Layout Shift ttfb: Math.max(200, complexity * 0.02), // Time to First Byte fcp: Math.max(800, complexity * 0.08) // First Contentful Paint }; } /** * Simulate Lighthouse scores */ async _simulateLighthouseScores(artifact, context) { const code = this._extractCode(artifact); const complexity = this._estimateComplexity(code); // Base scores reduced by complexity const baseScores = { performance: 95, accessibility: 92, bestPractices: 90, seo: 88 }; const complexityPenalty = Math.min(30, complexity * 0.0001); return { performance: Math.max(60, baseScores.performance - complexityPenalty), accessibility: Math.max(70, baseScores.accessibility - complexityPenalty * 0.5), bestPractices: Math.max(75, baseScores.bestPractices - complexityPenalty * 0.3), seo: Math.max(80, baseScores.seo - complexityPenalty * 0.2) }; } /** * Estimate code complexity for simulation */ _estimateComplexity(code) { const lines = code.split('\n').length; const functions = (code.match(/function|=>/g) || []).length; const imports = (code.match(/import/g) || []).length; const loops = (code.match(/for|while|forEach|map|filter|reduce/g) || []).length; return lines + (functions * 10) + (imports * 5) + (loops * 20); } /** * Extract code from artifact */ _extractCode(artifact) { if (typeof artifact === 'string') return artifact; if (artifact.content) return artifact.content; if (artifact.files && Array.isArray(artifact.files)) { return artifact.files.map(f => f.content || '').join('\n'); } return ''; } /** * Compare current metrics with baseline */ _compareWithBaseline(currentMetrics) { const regressions = []; const metricsToCompare = [ { path: 'runtime.initialLoadTime', threshold: 0.1 }, // 10% regression threshold { path: 'runtime.memoryUsage', threshold: 0.15 }, // 15% regression threshold { path: 'webVitals.lcp', threshold: 0.1 }, { path: 'webVitals.fid', threshold: 0.2 }, { path: 'lighthouse.performance', threshold: -0.05, reverse: true } // Score decrease is bad ]; metricsToCompare.forEach(({ path, threshold, reverse = false }) => { const current = this._getNestedValue(currentMetrics, path); const baseline = this._getNestedValue(this.metrics.baseline, path); if (current !== undefined && baseline !== undefined) { const change = reverse ? (baseline - current) / baseline : // For scores, decrease is regression (current - baseline) / baseline; // For metrics, increase is regression if (change > threshold) { regressions.push({ metric: path, current, baseline, change: Math.round(change * 100), severity: change > threshold * 2 ? 'high' : 'medium' }); } } }); return { regressions }; } /** * Get nested object value by path */ _getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj); } /** * Merge validation results */ _mergeValidationResults(mainResults, newResults) { mainResults.issues.push(...newResults.issues); if (newResults.metrics) { Object.assign(mainResults.metrics, newResults.metrics); } } /** * Calculate overall performance score */ _calculateOverallScore(results) { let score = 100; // Deduct points for issues results.issues.forEach(issue => { switch (issue.severity) { case 'high': score -= 15; break; case 'medium': score -= 8; break; case 'low': score -= 3; break; } }); // Factor in Lighthouse scores if available if (results.metrics.lighthouse) { const lighthouseAvg = ( results.metrics.lighthouse.performance + results.metrics.lighthouse.accessibility + results.metrics.lighthouse.bestPractices + results.metrics.lighthouse.seo ) / 4; score = Math.round((score + lighthouseAvg) / 2); } return Math.max(0, Math.min(100, Math.round(score))); } /** * Determine pass/fail based on issues and scores */ _determinePassFail(results) { // Fail if there are high-severity issues const highSeverityIssues = results.issues.filter(issue => issue.severity === 'high'); if (highSeverityIssues.length > 0) { return false; } // Fail if overall score is too low if (results.scores.overall < 70) { return false; } // Fail if too many medium severity issues const mediumSeverityIssues = results.issues.filter(issue => issue.severity === 'medium'); if (mediumSeverityIssues.length > 5) { return false; } return true; } /** * Generate performance recommendations */ _generatePerformanceRecommendations(results) { const recommendations = []; // Bundle size recommendations const bundleIssues = results.issues.filter(issue => issue.type.includes('bundle')); if (bundleIssues.length > 0) { recommendations.push('Consider implementing code splitting and lazy loading to reduce initial bundle size'); recommendations.push('Use webpack-bundle-analyzer to identify and optimize large dependencies'); } // Runtime performance recommendations const runtimeIssues = results.issues.filter(issue => issue.type.includes('load') || issue.type.includes('memory') || issue.type.includes('cpu')); if (runtimeIssues.length > 0) { recommendations.push('Optimize critical rendering path and reduce blocking resources'); recommendations.push('Implement performance monitoring to track real-user metrics'); } // Web Vitals recommendations const webVitalsIssues = results.issues.filter(issue => issue.webVital); if (webVitalsIssues.length > 0) { recommendations.push('Focus on Core Web Vitals improvements for better user experience'); recommendations.push('Use tools like PageSpeed Insights for detailed optimization suggestions'); } // General recommendations if (results.issues.length > 3) { recommendations.push('Consider setting up continuous performance monitoring'); recommendations.push('Establish performance budgets to prevent future regressions'); } return recommendations; } /** * Get configured thresholds */ _getConfiguredThresholds() { return { maxBundleSize: this.config.maxBundleSize, maxInitialLoadTime: this.config.maxInitialLoadTime, maxMemoryUsage: this.config.maxMemoryUsage, maxCpuUsage: this.config.maxCpuUsage, maxLCP: this.config.maxLCP, maxFID: this.config.maxFID, maxCLS: this.config.maxCLS, minPerformanceScore: this.config.minPerformanceScore, minAccessibilityScore: this.config.minAccessibilityScore, minBestPracticesScore: this.config.minBestPracticesScore, minSEOScore: this.config.minSEOScore }; } /** * Get enabled validation types */ _getEnabledValidationTypes() { return { bundleAnalysis: this.config.enableBundleAnalysis, runtimeMetrics: this.config.enableRuntimeMetrics, webVitals: this.config.enableWebVitals, lighthouseScores: this.config.enableLighthouseScores }; } /** * Update average score */ _updateAverageScore(newScore) { const alpha = 0.1; // Exponential moving average factor this.stats.averageScore = this.stats.averageScore === 0 ? newScore : alpha * newScore + (1 - alpha) * this.stats.averageScore; } /** * Set performance baseline */ setBaseline(metrics) { this.metrics.baseline = metrics; console.log('📊 Performance baseline established'); } /** * Get performance statistics */ getStatistics() { return { ...this.stats, averageScore: Math.round(this.stats.averageScore), successRate: this.stats.totalValidations > 0 ? Math.round((this.stats.passedValidations / this.stats.totalValidations) * 100) : 0, hasBaseline: Object.keys(this.metrics.baseline).length > 0, config: this._getConfiguredThresholds() }; } } module.exports = { PerformanceValidator };