Performance testing implemented
CI Pipeline / test (18) (pull_request) Failing after 49s
CI Pipeline / test (20) (pull_request) Failing after 53s
CI Pipeline / e2e (chromium) (pull_request) Failing after 21m9s
CI Pipeline / e2e (firefox) (pull_request) Failing after 25m13s
CI Pipeline / visual-regression (pull_request) Failing after 7m9s
CI Pipeline / performance (pull_request) Failing after 2m1s
CI Pipeline / storybook (pull_request) Failing after 1m32s
CI Pipeline / lint (pull_request) Failing after 44s
CI Pipeline / build (pull_request) Failing after 1m43s
CI Pipeline / e2e (webkit) (pull_request) Failing after 23m14s

This commit is contained in:
adilallo
2025-08-29 13:53:00 -06:00
parent f7621d2086
commit d300772f19
8 changed files with 1823 additions and 10 deletions
+5 -4
View File
@@ -6,10 +6,11 @@ This document outlines our comprehensive testing strategy that properly separate
## Current Test Status
- **184 total tests** across the project
- **176 tests passing** (95.7% success rate)
- **8 tests failing** (all related to multiple element instances)
- **13 test files** covering all major components
- **236 total tests** across the project
- **227 tests passing** (96.2% success rate)
- **9 tests failing** (performance and interaction tests)
- **15 test files** covering all major components
- **Performance Monitoring**: Comprehensive regression detection and budget enforcement
## Testing Philosophy
+319
View File
@@ -0,0 +1,319 @@
# Performance Monitoring System
## Overview
The Community Rule platform includes a comprehensive performance monitoring system designed to detect performance regressions, maintain performance budgets, and ensure optimal user experience across all components and user interactions.
## Architecture
### Core Components
1. **Performance Monitor Module** (`tests/performance/performance-monitor.js`)
- Base `PerformanceMonitor` class for metric collection and analysis
- `WebPerformanceMonitor` for browser-based performance monitoring
- `PlaywrightPerformanceMonitor` for E2E performance testing
2. **Performance Tests** (`tests/e2e/performance.spec.ts`)
- Comprehensive E2E performance tests using Playwright
- Core Web Vitals monitoring
- Component render performance testing
- Interaction performance testing
3. **Lighthouse CI Integration** (`lighthouserc.json`)
- Automated performance audits
- Performance budget enforcement
- Core Web Vitals validation
4. **Performance Budgets** (`performance-budgets.json`)
- Resource size limits
- Timing budgets
- Resource count limits
5. **Monitoring Script** (`scripts/performance-monitor.js`)
- Standalone performance monitoring
- Regression detection
- Report generation
## Performance Budgets
### Timing Budgets
| Metric | Budget | Baseline | Description |
| ------------------------ | ------ | -------- | ------------------------- |
| Page Load Time | 3000ms | 2000ms | Total page load time |
| First Contentful Paint | 2000ms | 1500ms | First content appears |
| Largest Contentful Paint | 2500ms | 2000ms | Largest content element |
| First Input Delay | 100ms | 50ms | First user interaction |
| TTFB | 600ms | 400ms | Time to First Byte |
| Component Render | 500ms | 300ms | Component rendering time |
| Interaction Time | 100ms | 50ms | User interaction response |
| Scroll Performance | 50ms | 30ms | Scroll operation time |
### Resource Budgets
| Resource Type | Size Limit | Count Limit | Description |
| ------------- | ---------- | ----------- | ---------------------- |
| Scripts | 300KB | 10 | JavaScript files |
| Stylesheets | 50KB | 5 | CSS files |
| Images | 100KB | 20 | Image files |
| Fonts | 50KB | 5 | Font files |
| Total | 500KB | 50 | All resources combined |
## Usage
### Running Performance Tests
```bash
# Run all performance tests
npm run e2e:performance
# Run specific performance test
npx playwright test tests/e2e/performance.spec.ts --grep="homepage load performance"
# Run with specific browser
npx playwright test tests/e2e/performance.spec.ts --project=chromium
```
### Running Lighthouse CI
```bash
# Run Lighthouse CI with default settings
npm run lhci
# Run with mobile preset
npm run lhci:mobile
# Run with desktop preset
npm run lhci:desktop
# Run with performance budgets
npm run performance:budget
```
### Running Performance Monitoring
```bash
# Run comprehensive performance monitoring
npm run performance:monitor
# Run monitoring script directly
node scripts/performance-monitor.js
```
## Performance Metrics
### Core Web Vitals
1. **Largest Contentful Paint (LCP)**
- Measures loading performance
- Target: < 2.5 seconds
- Baseline: < 2.0 seconds
2. **First Input Delay (FID)**
- Measures interactivity
- Target: < 100ms
- Baseline: < 50ms
3. **Cumulative Layout Shift (CLS)**
- Measures visual stability
- Target: < 0.1
- Baseline: < 0.05
### Navigation Timing
- **DNS Lookup**: Domain name resolution time
- **TCP Connection**: Connection establishment time
- **TTFB**: Time to First Byte
- **Download**: Resource download time
- **DOM Content Loaded**: DOM parsing completion
- **Load**: Full page load completion
### Component Performance
- **Render Time**: Component rendering duration
- **Interaction Time**: User interaction response time
- **Scroll Performance**: Smooth scrolling performance
- **Memory Usage**: JavaScript heap memory consumption
## Regression Detection
### Automatic Detection
The performance monitoring system automatically detects regressions by:
1. **Comparing against baselines**: Current metrics vs. established baselines
2. **Threshold monitoring**: Real-time threshold violation detection
3. **Trend analysis**: Performance degradation over time
4. **Statistical analysis**: Variance and consistency monitoring
### Regression Thresholds
- **20% degradation**: Triggers regression warning
- **50% degradation**: Triggers regression error
- **Consistent degradation**: Pattern-based regression detection
### Alert System
```javascript
// Example regression detection output
🚨 Performance regression detected: scroll_performance = 111ms (baseline: 30ms)
Performance threshold exceeded: interaction_time = 1368ms (threshold: 100ms)
```
## Performance Reports
### Generated Reports
1. **Console Output**: Real-time performance metrics and warnings
2. **JSON Reports**: Structured performance data (`performance-report.json`)
3. **Lighthouse Reports**: Detailed performance audits
4. **Playwright Reports**: E2E test results with performance data
### Report Structure
```json
{
"timestamp": "2024-01-01T12:00:00.000Z",
"summary": {
"totalMetrics": 15,
"regressions": 2,
"warnings": 3
},
"regressions": [
{
"metric": "scroll_performance",
"current": 111,
"baseline": 30,
"regression": "270.0%"
}
],
"warnings": [
"Performance threshold exceeded: interaction_time = 1368ms (threshold: 100ms)"
],
"metrics": {
"page_load_time": {
"latest": 1704,
"average": 1704,
"min": 1704,
"max": 1704,
"count": 1
}
}
}
```
## CI/CD Integration
### GitHub Actions Integration
```yaml
# Example CI workflow
- name: Performance Tests
run: |
npm run e2e:performance
npm run lhci
npm run performance:budget
```
### Performance Gates
- **Performance Score**: Must be > 90
- **Core Web Vitals**: All metrics within budgets
- **Regression Detection**: No significant regressions
- **Resource Budgets**: All resources within limits
## Best Practices
### Development Workflow
1. **Pre-commit Checks**: Run performance tests before commits
2. **Baseline Updates**: Update baselines after performance improvements
3. **Budget Reviews**: Regular budget review and adjustment
4. **Regression Investigation**: Immediate investigation of detected regressions
### Performance Optimization
1. **Code Splitting**: Implement dynamic imports for better loading
2. **Image Optimization**: Use modern formats and proper sizing
3. **Caching**: Implement effective caching strategies
4. **Bundle Analysis**: Regular bundle size monitoring
### Monitoring Strategy
1. **Continuous Monitoring**: Automated performance testing in CI/CD
2. **Real User Monitoring**: Collect performance data from real users
3. **Alert Thresholds**: Set appropriate alert thresholds
4. **Performance Budgets**: Enforce strict performance budgets
## Troubleshooting
### Common Issues
1. **Test Timeouts**
- Increase timeout values for slow operations
- Add proper wait conditions
- Check for network issues
2. **False Positives**
- Adjust baseline values
- Review test environment
- Check for external dependencies
3. **Performance Fluctuations**
- Run multiple test iterations
- Use statistical analysis
- Consider environmental factors
### Debugging Performance Issues
```bash
# Enable detailed logging
DEBUG=playwright:* npm run e2e:performance
# Run with specific browser and debugging
npx playwright test tests/e2e/performance.spec.ts --project=chromium --debug
# Generate detailed reports
npm run performance:monitor -- --verbose
```
## Future Enhancements
### Planned Features
1. **Real User Monitoring (RUM)**
- Collect performance data from real users
- User-centric performance metrics
- Geographic performance analysis
2. **Advanced Analytics**
- Machine learning-based regression detection
- Predictive performance modeling
- Automated performance optimization suggestions
3. **Performance Dashboard**
- Web-based performance monitoring dashboard
- Real-time performance metrics visualization
- Historical performance trends
4. **Integration with APM Tools**
- New Relic integration
- DataDog integration
- Custom APM tool integration
## Conclusion
The performance monitoring system provides comprehensive coverage of application performance, enabling early detection of regressions and maintaining high performance standards. Regular monitoring and proactive optimization ensure optimal user experience across all platforms and devices.
For questions or issues with the performance monitoring system, please refer to the testing documentation or create an issue in the project repository.
+66 -6
View File
@@ -3,16 +3,76 @@
"collect": {
"startServerCommand": "npm run preview",
"url": ["http://localhost:3000/"],
"numberOfRuns": 2
"numberOfRuns": 3,
"settings": {
"preset": "desktop",
"throttling": {
"rttMs": 40,
"throughputKbps": 10240,
"cpuSlowdownMultiplier": 1,
"requestLatencyMs": 0,
"downloadThroughputKbps": 0,
"uploadThroughputKbps": 0
}
}
},
"assert": {
"assertions": {
"categories:performance": ["error", {"minScore": 0.9}],
"categories:accessibility": ["warn", {"minScore": 0.95}],
"first-contentful-paint": ["warn", {"maxNumericValue": 2000}],
"interactive": ["warn", {"maxNumericValue": 3000}]
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["warn", { "minScore": 0.95 }],
"categories:best-practices": ["warn", { "minScore": 0.9 }],
"categories:seo": ["warn", { "minScore": 0.9 }],
"first-contentful-paint": ["warn", { "maxNumericValue": 2000 }],
"largest-contentful-paint": ["warn", { "maxNumericValue": 2500 }],
"first-meaningful-paint": ["warn", { "maxNumericValue": 2000 }],
"speed-index": ["warn", { "maxNumericValue": 3000 }],
"interactive": ["warn", { "maxNumericValue": 3000 }],
"total-blocking-time": ["warn", { "maxNumericValue": 300 }],
"cumulative-layout-shift": ["warn", { "maxNumericValue": 0.1 }],
"max-potential-fid": ["warn", { "maxNumericValue": 130 }],
"server-response-time": ["warn", { "maxNumericValue": 600 }],
"render-blocking-resources": ["warn", { "maxLength": 0 }],
"unused-css-rules": ["warn", { "maxLength": 0 }],
"unused-javascript": ["warn", { "maxLength": 0 }],
"modern-image-formats": ["warn", { "maxLength": 0 }],
"uses-optimized-images": ["warn", { "maxLength": 0 }],
"uses-text-compression": ["warn", { "maxLength": 0 }],
"uses-responsive-images": ["warn", { "maxLength": 0 }],
"efficient-animated-content": ["warn", { "maxLength": 0 }],
"preload-lcp-image": ["warn", { "maxLength": 0 }],
"total-byte-weight": ["warn", { "maxNumericValue": 500000 }],
"uses-long-cache-ttl": ["warn", { "maxLength": 0 }],
"dom-size": ["warn", { "maxNumericValue": 1500 }],
"critical-request-chains": ["warn", { "maxLength": 0 }],
"user-timings": ["warn", { "maxLength": 0 }],
"bootup-time": ["warn", { "maxNumericValue": 1000 }],
"mainthread-work-breakdown": ["warn", { "maxLength": 0 }],
"font-display": ["warn", { "maxLength": 0 }],
"resource-summary": ["warn", { "maxLength": 0 }],
"third-party-summary": ["warn", { "maxLength": 0 }],
"largest-contentful-paint-element": ["warn", { "maxLength": 0 }],
"layout-shift-elements": ["warn", { "maxLength": 0 }],
"long-tasks": ["warn", { "maxLength": 0 }],
"non-composited-animations": ["warn", { "maxLength": 0 }],
"unsized-images": ["warn", { "maxLength": 0 }]
}
},
"upload": { "target": "temporary-public-storage" }
"upload": {
"target": "temporary-public-storage",
"outputDir": "./lighthouse-results"
}
},
"settings": {
"onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
"skipAudits": ["uses-http2"],
"formFactor": "desktop",
"throttling": {
"rttMs": 40,
"throughputKbps": 10240,
"cpuSlowdownMultiplier": 1,
"requestLatencyMs": 0,
"downloadThroughputKbps": 0,
"uploadThroughputKbps": 0
}
}
}
+5
View File
@@ -18,7 +18,12 @@
"test:sb": "storybook dev -p 6006 & wait-on http://localhost:6006 && test-storybook",
"e2e": "playwright test",
"e2e:ui": "playwright test --ui",
"e2e:performance": "playwright test tests/e2e/performance.spec.ts",
"lhci": "lhci autorun",
"lhci:mobile": "lhci autorun --config=lighthouserc.json --settings.preset=mobile",
"lhci:desktop": "lhci autorun --config=lighthouserc.json --settings.preset=desktop",
"performance:budget": "lhci autorun --budgetPath=performance-budgets.json",
"performance:monitor": "node scripts/performance-monitor.js",
"preview": "next build && next start -p 3000",
"e2e:serve": "start-server-and-test preview http://localhost:3000 e2e"
},
+186
View File
@@ -0,0 +1,186 @@
{
"performance": {
"budgets": [
{
"path": "/*",
"timings": [
{
"metric": "first-contentful-paint",
"budget": 2000
},
{
"metric": "largest-contentful-paint",
"budget": 2500
},
{
"metric": "first-meaningful-paint",
"budget": 2000
},
{
"metric": "speed-index",
"budget": 3000
},
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "total-blocking-time",
"budget": 300
},
{
"metric": "cumulative-layout-shift",
"budget": 0.1
},
{
"metric": "max-potential-fid",
"budget": 130
}
],
"resourceSizes": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "total",
"budget": 500
},
{
"resourceType": "image",
"budget": 100
},
{
"resourceType": "stylesheet",
"budget": 50
},
{
"resourceType": "font",
"budget": 50
}
],
"resourceCounts": [
{
"resourceType": "script",
"budget": 10
},
{
"resourceType": "total",
"budget": 50
},
{
"resourceType": "image",
"budget": 20
},
{
"resourceType": "stylesheet",
"budget": 5
},
{
"resourceType": "font",
"budget": 5
}
]
}
]
},
"timing": {
"budgets": [
{
"path": "/*",
"timings": [
{
"metric": "first-contentful-paint",
"budget": 2000
},
{
"metric": "largest-contentful-paint",
"budget": 2500
},
{
"metric": "first-meaningful-paint",
"budget": 2000
},
{
"metric": "speed-index",
"budget": 3000
},
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "total-blocking-time",
"budget": 300
},
{
"metric": "cumulative-layout-shift",
"budget": 0.1
},
{
"metric": "max-potential-fid",
"budget": 130
}
]
}
]
},
"resourceSizes": {
"budgets": [
{
"path": "/*",
"resourceSizes": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "total",
"budget": 500
},
{
"resourceType": "image",
"budget": 100
},
{
"resourceType": "stylesheet",
"budget": 50
},
{
"resourceType": "font",
"budget": 50
}
]
}
]
},
"resourceCounts": {
"budgets": [
{
"path": "/*",
"resourceCounts": [
{
"resourceType": "script",
"budget": 10
},
{
"resourceType": "total",
"budget": 50
},
{
"resourceType": "image",
"budget": 20
},
{
"resourceType": "stylesheet",
"budget": 5
},
{
"resourceType": "font",
"budget": 5
}
]
}
]
}
}
+387
View File
@@ -0,0 +1,387 @@
#!/usr/bin/env node
/**
* Performance Monitoring Script
*
* This script provides comprehensive performance monitoring capabilities
* for the Community Rule application.
*/
const { spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
// Performance budgets
const PERFORMANCE_BUDGETS = {
page_load_time: 3000,
first_contentful_paint: 2000,
largest_contentful_paint: 2500,
first_input_delay: 100,
dns_lookup: 100,
tcp_connection: 200,
ttfb: 600,
dom_content_loaded: 1500,
full_load: 3000,
component_render_time: 500,
interaction_time: 100,
scroll_performance: 50,
network_request_duration: 1000,
memory_usage_mb: 50,
};
// Baseline metrics for regression detection
const BASELINE_METRICS = {
page_load_time: 2000,
first_contentful_paint: 1500,
largest_contentful_paint: 2000,
first_input_delay: 50,
dns_lookup: 50,
tcp_connection: 100,
ttfb: 400,
dom_content_loaded: 1000,
full_load: 2000,
component_render_time: 300,
interaction_time: 50,
scroll_performance: 30,
network_request_duration: 500,
memory_usage_mb: 30,
};
class PerformanceMonitorScript {
constructor() {
this.metrics = new Map();
this.regressions = [];
this.warnings = [];
}
/**
* Run Lighthouse CI performance tests
*/
async runLighthouseCI() {
console.log("🚀 Running Lighthouse CI performance tests...");
return new Promise((resolve, reject) => {
const lhci = spawn("npx", ["lhci", "autorun"], {
stdio: "pipe",
shell: true,
});
let output = "";
let errorOutput = "";
lhci.stdout.on("data", (data) => {
output += data.toString();
console.log(data.toString());
});
lhci.stderr.on("data", (data) => {
errorOutput += data.toString();
console.error(data.toString());
});
lhci.on("close", (code) => {
if (code === 0) {
console.log("✅ Lighthouse CI tests completed successfully");
this.analyzeLighthouseResults(output);
resolve(output);
} else {
console.error("❌ Lighthouse CI tests failed");
reject(
new Error(`Lighthouse CI failed with code ${code}: ${errorOutput}`)
);
}
});
});
}
/**
* Run Playwright performance tests
*/
async runPlaywrightPerformanceTests() {
console.log("🎭 Running Playwright performance tests...");
return new Promise((resolve, reject) => {
const playwright = spawn(
"npx",
[
"playwright",
"test",
"tests/e2e/performance.spec.ts",
"--reporter=json",
],
{
stdio: "pipe",
shell: true,
}
);
let output = "";
let errorOutput = "";
playwright.stdout.on("data", (data) => {
output += data.toString();
});
playwright.stderr.on("data", (data) => {
errorOutput += data.toString();
});
playwright.on("close", (code) => {
if (code === 0) {
console.log("✅ Playwright performance tests completed successfully");
this.analyzePlaywrightResults(output);
resolve(output);
} else {
console.error("❌ Playwright performance tests failed");
reject(
new Error(
`Playwright tests failed with code ${code}: ${errorOutput}`
)
);
}
});
});
}
/**
* Analyze Lighthouse CI results
*/
analyzeLighthouseResults(output) {
console.log("📊 Analyzing Lighthouse CI results...");
// Parse Lighthouse results
const lines = output.split("\n");
let currentMetric = null;
for (const line of lines) {
if (line.includes("Performance")) {
const scoreMatch = line.match(/(\d+)/);
if (scoreMatch) {
const score = parseInt(scoreMatch[1]);
this.recordMetric("lighthouse_performance_score", score);
if (score < 90) {
this.warnings.push(
`Performance score below threshold: ${score}/100`
);
}
}
}
if (line.includes("First Contentful Paint")) {
const timeMatch = line.match(/(\d+(?:\.\d+)?)\s*ms/);
if (timeMatch) {
const time = parseFloat(timeMatch[1]);
this.recordMetric("first_contentful_paint", time);
if (time > PERFORMANCE_BUDGETS.first_contentful_paint) {
this.warnings.push(
`First Contentful Paint exceeded budget: ${time}ms`
);
}
}
}
if (line.includes("Largest Contentful Paint")) {
const timeMatch = line.match(/(\d+(?:\.\d+)?)\s*ms/);
if (timeMatch) {
const time = parseFloat(timeMatch[1]);
this.recordMetric("largest_contentful_paint", time);
if (time > PERFORMANCE_BUDGETS.largest_contentful_paint) {
this.warnings.push(
`Largest Contentful Paint exceeded budget: ${time}ms`
);
}
}
}
if (line.includes("Total Blocking Time")) {
const timeMatch = line.match(/(\d+(?:\.\d+)?)\s*ms/);
if (timeMatch) {
const time = parseFloat(timeMatch[1]);
this.recordMetric("total_blocking_time", time);
if (time > 300) {
this.warnings.push(
`Total Blocking Time exceeded budget: ${time}ms`
);
}
}
}
if (line.includes("Cumulative Layout Shift")) {
const shiftMatch = line.match(/(\d+(?:\.\d+)?)/);
if (shiftMatch) {
const shift = parseFloat(shiftMatch[1]);
this.recordMetric("cumulative_layout_shift", shift);
if (shift > 0.1) {
this.warnings.push(
`Cumulative Layout Shift exceeded budget: ${shift}`
);
}
}
}
}
}
/**
* Analyze Playwright test results
*/
analyzePlaywrightResults(output) {
console.log("📊 Analyzing Playwright test results...");
try {
const results = JSON.parse(output);
for (const result of results) {
if (result.status === "failed") {
this.warnings.push(`Test failed: ${result.title}`);
}
}
} catch (error) {
console.warn("Could not parse Playwright results as JSON");
}
}
/**
* Record a performance metric
*/
recordMetric(name, value) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push({
value,
timestamp: Date.now(),
});
// Check against baseline for regression detection
const baseline = BASELINE_METRICS[name];
if (baseline) {
const regressionThreshold = baseline * 1.2; // 20% regression threshold
if (value > regressionThreshold) {
this.regressions.push({
metric: name,
current: value,
baseline,
regression: (((value - baseline) / baseline) * 100).toFixed(1) + "%",
});
}
}
}
/**
* Generate performance report
*/
generateReport() {
console.log("\n📈 Performance Monitoring Report");
console.log("================================\n");
// Summary
console.log("📊 Summary:");
console.log(`- Total metrics recorded: ${this.metrics.size}`);
console.log(
`- Performance regressions detected: ${this.regressions.length}`
);
console.log(`- Warnings: ${this.warnings.length}\n`);
// Performance regressions
if (this.regressions.length > 0) {
console.log("🚨 Performance Regressions:");
for (const regression of this.regressions) {
console.log(
` - ${regression.metric}: ${regression.current} (baseline: ${regression.baseline}, regression: ${regression.regression})`
);
}
console.log("");
}
// Warnings
if (this.warnings.length > 0) {
console.log("⚠️ Warnings:");
for (const warning of this.warnings) {
console.log(` - ${warning}`);
}
console.log("");
}
// Metrics summary
console.log("📋 Metrics Summary:");
for (const [name, values] of this.metrics) {
const latest = values[values.length - 1];
const average =
values.reduce((sum, v) => sum + v.value, 0) / values.length;
const budget = PERFORMANCE_BUDGETS[name];
console.log(` - ${name}:`);
console.log(` Latest: ${latest.value}`);
console.log(` Average: ${average.toFixed(2)}`);
if (budget) {
const status = latest.value <= budget ? "✅" : "❌";
console.log(` Budget: ${budget} ${status}`);
}
}
// Save report to file
const report = {
timestamp: new Date().toISOString(),
summary: {
totalMetrics: this.metrics.size,
regressions: this.regressions.length,
warnings: this.warnings.length,
},
regressions: this.regressions,
warnings: this.warnings,
metrics: Object.fromEntries(this.metrics),
};
const reportPath = path.join(__dirname, "../performance-report.json");
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📄 Report saved to: ${reportPath}`);
return report;
}
/**
* Run all performance monitoring
*/
async run() {
console.log("🔍 Starting Performance Monitoring...\n");
try {
// Run Lighthouse CI tests
await this.runLighthouseCI();
// Run Playwright performance tests
await this.runPlaywrightPerformanceTests();
// Generate and display report
const report = this.generateReport();
// Exit with appropriate code
if (this.regressions.length > 0) {
console.log("❌ Performance regressions detected!");
process.exit(1);
} else if (this.warnings.length > 0) {
console.log("⚠️ Performance warnings detected.");
process.exit(0);
} else {
console.log("✅ All performance checks passed!");
process.exit(0);
}
} catch (error) {
console.error("❌ Performance monitoring failed:", error.message);
process.exit(1);
}
}
}
// Run the performance monitor if this script is executed directly
if (require.main === module) {
const monitor = new PerformanceMonitorScript();
monitor.run();
}
module.exports = PerformanceMonitorScript;
+381
View File
@@ -0,0 +1,381 @@
import { test, expect } from "@playwright/test";
import { PlaywrightPerformanceMonitor } from "../performance/performance-monitor.js";
// Performance budgets and thresholds
const PERFORMANCE_BUDGETS = {
// Page load performance
page_load_time: 3000, // 3 seconds
first_contentful_paint: 2000, // 2 seconds
largest_contentful_paint: 2500, // 2.5 seconds
first_input_delay: 100, // 100ms
// Navigation timing
dns_lookup: 100, // 100ms
tcp_connection: 200, // 200ms
ttfb: 600, // 600ms
dom_content_loaded: 1500, // 1.5 seconds
full_load: 3000, // 3 seconds
// Component performance
component_render_time: 500, // 500ms
interaction_time: 100, // 100ms
scroll_performance: 50, // 50ms
// Resource performance
network_request_duration: 1000, // 1 second
memory_usage_mb: 50, // 50MB
};
// Baseline metrics for regression detection
const BASELINE_METRICS = {
page_load_time: 2000,
first_contentful_paint: 1500,
largest_contentful_paint: 2000,
first_input_delay: 50,
dns_lookup: 50,
tcp_connection: 100,
ttfb: 400,
dom_content_loaded: 1000,
full_load: 2000,
component_render_time: 300,
interaction_time: 50,
scroll_performance: 30,
network_request_duration: 500,
memory_usage_mb: 30,
};
test.describe("Performance Monitoring", () => {
let performanceMonitor: PlaywrightPerformanceMonitor;
test.beforeEach(async ({ page }) => {
performanceMonitor = new PlaywrightPerformanceMonitor(page);
performanceMonitor.setThresholds(PERFORMANCE_BUDGETS);
performanceMonitor.setBaselines(BASELINE_METRICS);
});
test("homepage load performance", async ({ page }) => {
const result = await performanceMonitor.measurePageLoad("/");
// Assert page load time is within budget
expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
// Assert individual metrics
expect(result.metrics.ttfb).toBeLessThan(PERFORMANCE_BUDGETS.ttfb);
expect(result.metrics.domContentLoaded).toBeLessThan(
PERFORMANCE_BUDGETS.dom_content_loaded
);
expect(result.metrics.load).toBeLessThan(PERFORMANCE_BUDGETS.full_load);
// Check for performance regressions
const summary = performanceMonitor.getSummary();
console.log("Performance Summary:", summary);
});
test("core web vitals", async ({ page }) => {
await page.goto("/");
// Wait for page to fully load
await page.waitForLoadState("networkidle");
// Get Core Web Vitals with timeout
const coreWebVitals = await page.evaluate(() => {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
observer.disconnect();
resolve({ lcp: 0, fid: 0, cls: 0 }); // Default values if timeout
}, 10000); // 10 second timeout
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const metrics: any = {};
for (const entry of entries) {
if (entry.name === "LCP") {
metrics.lcp = entry.startTime;
} else if (entry.name === "FID") {
metrics.fid = entry.processingStart - entry.startTime;
} else if (entry.name === "CLS") {
metrics.cls = entry.value;
}
}
if (Object.keys(metrics).length === 3) {
clearTimeout(timeout);
observer.disconnect();
resolve(metrics);
}
});
observer.observe({
entryTypes: [
"largest-contentful-paint",
"first-input",
"layout-shift",
],
});
});
});
// Assert Core Web Vitals are within acceptable ranges
expect(coreWebVitals.lcp).toBeLessThan(
PERFORMANCE_BUDGETS.largest_contentful_paint
);
expect(coreWebVitals.fid).toBeLessThan(
PERFORMANCE_BUDGETS.first_input_delay
);
expect(coreWebVitals.cls).toBeLessThan(0.1); // CLS should be less than 0.1
});
test("component render performance", async ({ page }) => {
await page.goto("/");
// Measure header render time
const headerRenderTime = await performanceMonitor.measureComponentRender(
"header"
);
expect(headerRenderTime).toBeLessThan(
PERFORMANCE_BUDGETS.component_render_time
);
// Measure footer render time
const footerRenderTime = await performanceMonitor.measureComponentRender(
"footer"
);
expect(footerRenderTime).toBeLessThan(
PERFORMANCE_BUDGETS.component_render_time
);
// Measure main content render time
const mainRenderTime = await performanceMonitor.measureComponentRender(
"main"
);
expect(mainRenderTime).toBeLessThan(
PERFORMANCE_BUDGETS.component_render_time
);
});
test("interaction performance", async ({ page }) => {
await page.goto("/");
// Wait for page to be ready
await page.waitForLoadState("networkidle");
// Measure button click performance with better element selection
const buttonClickTime = await performanceMonitor.measureInteraction(
'button:has-text("Learn how CommunityRule works")',
async () => {
const button = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
await button.waitFor({ state: "visible" });
await button.click();
}
);
expect(buttonClickTime).toBeLessThan(PERFORMANCE_BUDGETS.interaction_time);
// Measure link click performance with better element selection
const linkClickTime = await performanceMonitor.measureInteraction(
'a:has-text("Use Cases")',
async () => {
const link = page.locator('a:has-text("Use Cases")').first();
await link.waitFor({ state: "visible" });
await link.click();
}
);
expect(linkClickTime).toBeLessThan(PERFORMANCE_BUDGETS.interaction_time);
});
test("scroll performance", async ({ page }) => {
await page.goto("/");
// Measure scroll performance
const scrollTime = await performanceMonitor.measureScrollPerformance();
expect(scrollTime).toBeLessThan(PERFORMANCE_BUDGETS.scroll_performance);
});
test("memory usage", async ({ page }) => {
await page.goto("/");
// Get memory usage
const memoryUsage = await performanceMonitor.getMemoryUsage();
if (memoryUsage) {
const usedMemoryMB = memoryUsage.usedJSHeapSize / 1024 / 1024;
expect(usedMemoryMB).toBeLessThan(PERFORMANCE_BUDGETS.memory_usage_mb);
console.log(`Memory Usage: ${usedMemoryMB.toFixed(2)}MB`);
}
});
test("network request performance", async ({ page }) => {
const requests = await performanceMonitor.monitorNetworkRequests();
await page.goto("/");
await page.waitForLoadState("networkidle");
// Check that all requests completed within budget
const summary = performanceMonitor.getSummary();
if (summary.network_request_duration) {
expect(summary.network_request_duration.average).toBeLessThan(
PERFORMANCE_BUDGETS.network_request_duration
);
}
});
test("responsive performance across breakpoints", async ({ page }) => {
const breakpoints = [
{ name: "mobile", width: 375, height: 667 },
{ name: "tablet", width: 768, height: 1024 },
{ name: "desktop", width: 1280, height: 720 },
];
for (const breakpoint of breakpoints) {
await page.setViewportSize(breakpoint);
const result = await performanceMonitor.measurePageLoad("/");
// Assert performance is maintained across breakpoints
expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
console.log(`${breakpoint.name} load time: ${result.loadTime}ms`);
}
});
test("performance under load", async ({ page }) => {
// Simulate slower network conditions
await page.route("**/*", (route) => {
route.continue();
});
// Add artificial delay to simulate network latency
await page.addInitScript(() => {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms delay
return originalFetch(...args);
};
});
const result = await performanceMonitor.measurePageLoad("/");
// Even under load, page should load within reasonable time
expect(result.loadTime).toBeLessThan(
PERFORMANCE_BUDGETS.page_load_time * 1.5
);
});
test("performance regression detection", async ({ page }) => {
await page.goto("/");
// Simulate a performance regression by adding a heavy operation
await page.addInitScript(() => {
// Add a heavy operation that would cause regression
const heavyOperation = () => {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
};
// Execute heavy operation on page load
window.addEventListener("load", () => {
heavyOperation();
});
});
const result = await performanceMonitor.measurePageLoad("/");
// This should trigger a performance regression warning
const summary = performanceMonitor.getSummary();
console.log("Performance Summary with Regression:", summary);
});
test("performance metrics export", async ({ page }) => {
await page.goto("/");
// Perform various operations to collect metrics
await performanceMonitor.measureComponentRender("header");
await performanceMonitor.measureScrollPerformance();
await performanceMonitor.getMemoryUsage();
// Export all metrics
const exportedData = performanceMonitor.export();
// Verify exported data structure
expect(exportedData.metrics).toBeDefined();
expect(exportedData.baselines).toBeDefined();
expect(exportedData.thresholds).toBeDefined();
expect(exportedData.summary).toBeDefined();
console.log(
"Exported Performance Data:",
JSON.stringify(exportedData, null, 2)
);
});
test("performance budget compliance", async ({ page }) => {
await page.goto("/");
// Collect comprehensive metrics
await performanceMonitor.measurePageLoad("/");
await performanceMonitor.measureComponentRender("header");
await performanceMonitor.measureComponentRender("footer");
await performanceMonitor.measureScrollPerformance();
await performanceMonitor.getMemoryUsage();
const summary = performanceMonitor.getSummary();
// Check all metrics against budgets
for (const [metricName, budget] of Object.entries(PERFORMANCE_BUDGETS)) {
if (summary[metricName]) {
const actualValue =
summary[metricName].latest || summary[metricName].average;
expect(actualValue).toBeLessThan(budget);
console.log(`${metricName}: ${actualValue}ms (budget: ${budget}ms)`);
}
}
});
});
test.describe("Performance Regression Testing", () => {
test("detect performance regressions over time", async ({ page }) => {
const performanceMonitor = new PlaywrightPerformanceMonitor(page);
// Set strict baselines for regression detection
const strictBaselines = {
page_load_time: 1500,
first_contentful_paint: 1000,
component_render_time: 200,
interaction_time: 30,
};
performanceMonitor.setBaselines(strictBaselines);
// Run multiple iterations to detect trends
const iterations = 3;
const results = [];
for (let i = 0; i < iterations; i++) {
const result = await performanceMonitor.measurePageLoad("/");
results.push(result.loadTime);
// Small delay between iterations
await page.waitForTimeout(1000);
}
// Check for consistent performance
const averageLoadTime = results.reduce((a, b) => a + b, 0) / results.length;
const variance =
results.reduce(
(acc, val) => acc + Math.pow(val - averageLoadTime, 2),
0
) / results.length;
// Performance should be consistent (low variance)
expect(variance).toBeLessThan(100000); // Variance should be less than 100ms²
console.log(`Average load time: ${averageLoadTime}ms`);
console.log(`Variance: ${variance}`);
});
});
+474
View File
@@ -0,0 +1,474 @@
/**
* Performance Monitoring Module
*
* This module provides comprehensive performance monitoring capabilities
* for detecting performance regressions and maintaining performance budgets.
*/
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.baselines = new Map();
this.thresholds = new Map();
this.history = [];
}
/**
* Set performance thresholds for different metrics
*/
setThresholds(thresholds) {
this.thresholds = new Map(Object.entries(thresholds));
}
/**
* Set baseline metrics for comparison
*/
setBaselines(baselines) {
this.baselines = new Map(Object.entries(baselines));
}
/**
* Record a performance metric
*/
recordMetric(name, value, context = {}) {
const metric = {
name,
value,
timestamp: Date.now(),
context,
};
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push(metric);
// Check against thresholds
this.checkThreshold(name, value);
// Check against baselines
this.checkBaseline(name, value);
return metric;
}
/**
* Check if a metric exceeds its threshold
*/
checkThreshold(name, value) {
const threshold = this.thresholds.get(name);
if (!threshold) return;
if (value > threshold) {
console.warn(
`⚠️ Performance threshold exceeded: ${name} = ${value}ms (threshold: ${threshold}ms)`
);
return false;
}
return true;
}
/**
* Check if a metric has regressed from baseline
*/
checkBaseline(name, value) {
const baseline = this.baselines.get(name);
if (!baseline) return;
const regressionThreshold = baseline * 1.2; // 20% regression threshold
if (value > regressionThreshold) {
console.error(
`🚨 Performance regression detected: ${name} = ${value}ms (baseline: ${baseline}ms)`
);
return false;
}
return true;
}
/**
* Get the latest metric value
*/
getLatestMetric(name) {
const metrics = this.metrics.get(name);
if (!metrics || metrics.length === 0) return null;
return metrics[metrics.length - 1];
}
/**
* Get average metric value
*/
getAverageMetric(name) {
const metrics = this.metrics.get(name);
if (!metrics || metrics.length === 0) return null;
const sum = metrics.reduce((acc, metric) => acc + metric.value, 0);
return sum / metrics.length;
}
/**
* Get performance summary
*/
getSummary() {
const summary = {};
for (const [name, metrics] of this.metrics) {
const values = metrics.map((m) => m.value);
summary[name] = {
latest: values[values.length - 1],
average: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
count: values.length,
};
}
return summary;
}
/**
* Clear all metrics
*/
clear() {
this.metrics.clear();
}
/**
* Export metrics for analysis
*/
export() {
return {
metrics: Object.fromEntries(this.metrics),
baselines: Object.fromEntries(this.baselines),
thresholds: Object.fromEntries(this.thresholds),
summary: this.getSummary(),
};
}
}
/**
* Web Performance API wrapper
*/
class WebPerformanceMonitor extends PerformanceMonitor {
constructor() {
super();
this.performanceObserver = null;
this.setupPerformanceObserver();
}
/**
* Setup Performance Observer for automatic metric collection
*/
setupPerformanceObserver() {
if (typeof window === "undefined" || !window.PerformanceObserver) {
return;
}
try {
this.performanceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordMetric(entry.name, entry.duration, {
entryType: entry.entryType,
startTime: entry.startTime,
});
}
});
// Observe navigation timing
this.performanceObserver.observe({ entryTypes: ["navigation"] });
// Observe resource timing
this.performanceObserver.observe({ entryTypes: ["resource"] });
// Observe paint timing
this.performanceObserver.observe({ entryTypes: ["paint"] });
// Observe layout shifts
this.performanceObserver.observe({ entryTypes: ["layout-shift"] });
// Observe first input delay
this.performanceObserver.observe({ entryTypes: ["first-input"] });
} catch (error) {
console.warn("Performance Observer not supported:", error);
}
}
/**
* Get Core Web Vitals metrics
*/
async getCoreWebVitals() {
if (typeof window === "undefined") {
return null;
}
return new Promise((resolve) => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const metrics = {};
for (const entry of entries) {
if (entry.name === "LCP") {
metrics.lcp = entry.startTime;
} else if (entry.name === "FID") {
metrics.fid = entry.processingStart - entry.startTime;
} else if (entry.name === "CLS") {
metrics.cls = entry.value;
}
}
if (Object.keys(metrics).length === 3) {
observer.disconnect();
resolve(metrics);
}
});
observer.observe({
entryTypes: ["largest-contentful-paint", "first-input", "layout-shift"],
});
});
}
/**
* Get navigation timing metrics
*/
getNavigationTiming() {
if (typeof window === "undefined" || !window.performance) {
return null;
}
const navigation = performance.getEntriesByType("navigation")[0];
if (!navigation) return null;
return {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
domContentLoaded:
navigation.domContentLoadedEventEnd -
navigation.domContentLoadedEventStart,
load: navigation.loadEventEnd - navigation.loadEventStart,
total: navigation.loadEventEnd - navigation.fetchStart,
};
}
/**
* Get resource timing metrics
*/
getResourceTiming() {
if (typeof window === "undefined" || !window.performance) {
return null;
}
const resources = performance.getEntriesByType("resource");
return resources.map((resource) => ({
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType,
}));
}
/**
* Measure function execution time
*/
async measureFunction(name, fn) {
const start = performance.now();
try {
const result = await fn();
const duration = performance.now() - start;
this.recordMetric(name, duration);
return result;
} catch (error) {
const duration = performance.now() - start;
this.recordMetric(`${name}_error`, duration);
throw error;
}
}
/**
* Measure page load performance
*/
async measurePageLoad(url) {
return this.measureFunction("page_load", async () => {
const start = performance.now();
// Simulate page load (in real implementation, this would be actual navigation)
await new Promise((resolve) => setTimeout(resolve, 100));
const navigation = this.getNavigationTiming();
const coreWebVitals = await this.getCoreWebVitals();
return {
loadTime: performance.now() - start,
navigation,
coreWebVitals,
};
});
}
}
/**
* Playwright Performance Monitor
*/
class PlaywrightPerformanceMonitor extends PerformanceMonitor {
constructor(page) {
super();
this.page = page;
}
/**
* Measure page load performance using Playwright
*/
async measurePageLoad(url) {
const startTime = Date.now();
// Navigate to the page
await this.page.goto(url, { waitUntil: "networkidle" });
const loadTime = Date.now() - startTime;
this.recordMetric("page_load_time", loadTime, { url });
// Get performance metrics from the page
const metrics = await this.page.evaluate(() => {
const navigation = performance.getEntriesByType("navigation")[0];
const paint = performance.getEntriesByType("paint");
return {
dns: navigation?.domainLookupEnd - navigation?.domainLookupStart || 0,
tcp: navigation?.connectEnd - navigation?.connectStart || 0,
ttfb: navigation?.responseStart - navigation?.requestStart || 0,
download: navigation?.responseEnd - navigation?.responseStart || 0,
domContentLoaded:
navigation?.domContentLoadedEventEnd -
navigation?.domContentLoadedEventStart || 0,
load: navigation?.loadEventEnd - navigation?.loadEventStart || 0,
firstPaint: paint.find((p) => p.name === "first-paint")?.startTime || 0,
firstContentfulPaint:
paint.find((p) => p.name === "first-contentful-paint")?.startTime ||
0,
};
});
// Record individual metrics
for (const [name, value] of Object.entries(metrics)) {
this.recordMetric(name, value, { url });
}
return {
loadTime,
metrics,
};
}
/**
* Measure component render performance
*/
async measureComponentRender(selector) {
const startTime = Date.now();
// Wait for the component to be visible
await this.page.waitForSelector(selector, { state: "visible" });
const renderTime = Date.now() - startTime;
this.recordMetric("component_render_time", renderTime, { selector });
return renderTime;
}
/**
* Measure interaction performance
*/
async measureInteraction(selector, action) {
const startTime = Date.now();
// Perform the action
await action();
const interactionTime = Date.now() - startTime;
this.recordMetric("interaction_time", interactionTime, {
selector,
action: action.name,
});
return interactionTime;
}
/**
* Measure scroll performance
*/
async measureScrollPerformance() {
const startTime = Date.now();
// Scroll to bottom
await this.page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
const scrollTime = Date.now() - startTime;
this.recordMetric("scroll_performance", scrollTime);
return scrollTime;
}
/**
* Get memory usage
*/
async getMemoryUsage() {
const memory = await this.page.evaluate(() => {
if (performance.memory) {
return {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
};
}
return null;
});
if (memory) {
this.recordMetric("memory_usage_mb", memory.usedJSHeapSize / 1024 / 1024);
}
return memory;
}
/**
* Monitor network requests
*/
async monitorNetworkRequests() {
const requests = [];
this.page.on("request", (request) => {
requests.push({
url: request.url(),
method: request.method(),
resourceType: request.resourceType(),
timestamp: Date.now(),
});
});
this.page.on("response", (response) => {
const request = requests.find((r) => r.url === response.url());
if (request) {
request.status = response.status();
request.size = response.headers()["content-length"] || 0;
request.duration = Date.now() - request.timestamp;
this.recordMetric("network_request_duration", request.duration, {
url: request.url,
method: request.method,
status: request.status,
});
}
});
return requests;
}
}
// Export the performance monitors
module.exports = {
PerformanceMonitor,
WebPerformanceMonitor,
PlaywrightPerformanceMonitor,
};