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