Integrate performance monitoring with existing setup
This commit is contained in:
@@ -335,8 +335,13 @@ jobs:
|
|||||||
run: npm i -D @lhci/cli
|
run: npm i -D @lhci/cli
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run:
|
run: npm run build
|
||||||
npm run build
|
|
||||||
|
- name: Comprehensive Performance Testing
|
||||||
|
run: |
|
||||||
|
echo "🧪 Running comprehensive performance testing..."
|
||||||
|
npm run test:performance:ci
|
||||||
|
echo "✅ Performance testing complete"
|
||||||
|
|
||||||
# 1) Sanity check that the build exists
|
# 1) Sanity check that the build exists
|
||||||
- name: Verify Next build output
|
- name: Verify Next build output
|
||||||
@@ -456,12 +461,18 @@ jobs:
|
|||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
|
|
||||||
- name: Upload LHCI results
|
- name: Upload Performance Artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: lhci-results
|
name: performance-results
|
||||||
path: lhci-results
|
path: |
|
||||||
|
lhci-results
|
||||||
|
.next/analyze
|
||||||
|
.next/monitoring
|
||||||
|
.next/web-vitals
|
||||||
|
.next/test-results
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
storybook:
|
storybook:
|
||||||
runs-on: [self-hosted, macos-latest]
|
runs-on: [self-hosted, macos-latest]
|
||||||
|
|||||||
+3
-1
@@ -39,7 +39,9 @@
|
|||||||
"analyze:browser": "BUNDLE_ANALYZE=true npm run build",
|
"analyze:browser": "BUNDLE_ANALYZE=true npm run build",
|
||||||
"bundle:analyze": "node scripts/bundle-analyzer.js",
|
"bundle:analyze": "node scripts/bundle-analyzer.js",
|
||||||
"web-vitals:track": "node scripts/web-vitals-tracker.js",
|
"web-vitals:track": "node scripts/web-vitals-tracker.js",
|
||||||
"monitor:all": "npm run bundle:analyze && npm run performance:monitor && npm run web-vitals:track"
|
"monitor:all": "npm run bundle:analyze && npm run performance:monitor && npm run web-vitals:track",
|
||||||
|
"test:performance": "node scripts/test-performance.js",
|
||||||
|
"test:performance:ci": "npm run test:performance"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
|
|||||||
@@ -1,4 +1,41 @@
|
|||||||
{
|
{
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"name": "lcp",
|
||||||
|
"maxValue": 2500,
|
||||||
|
"description": "Largest Contentful Paint should be under 2.5s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fid",
|
||||||
|
"maxValue": 100,
|
||||||
|
"description": "First Input Delay should be under 100ms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cls",
|
||||||
|
"maxValue": 0.1,
|
||||||
|
"description": "Cumulative Layout Shift should be under 0.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fcp",
|
||||||
|
"maxValue": 1800,
|
||||||
|
"description": "First Contentful Paint should be under 1.8s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ttfb",
|
||||||
|
"maxValue": 800,
|
||||||
|
"description": "Time to First Byte should be under 800ms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bundle-size",
|
||||||
|
"maxSizeKB": 500,
|
||||||
|
"description": "Individual bundle size should be under 500KB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "total-size",
|
||||||
|
"maxSizeKB": 2000,
|
||||||
|
"description": "Total bundle size should be under 2MB"
|
||||||
|
}
|
||||||
|
],
|
||||||
"performance": {
|
"performance": {
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive Performance Testing Script
|
||||||
|
* Integrates bundle analysis, performance monitoring, and Web Vitals tracking
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const TEST_RESULTS_DIR = path.join(__dirname, "..", ".next", "test-results");
|
||||||
|
|
||||||
|
class PerformanceTester {
|
||||||
|
constructor() {
|
||||||
|
this.results = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
bundleAnalysis: {},
|
||||||
|
performanceMonitoring: {},
|
||||||
|
webVitals: {},
|
||||||
|
lighthouse: {},
|
||||||
|
summary: {
|
||||||
|
passed: 0,
|
||||||
|
failed: 0,
|
||||||
|
warnings: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run comprehensive performance testing
|
||||||
|
*/
|
||||||
|
async runTests() {
|
||||||
|
console.log("🧪 Starting comprehensive performance testing...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure test results directory exists
|
||||||
|
if (!fs.existsSync(TEST_RESULTS_DIR)) {
|
||||||
|
fs.mkdirSync(TEST_RESULTS_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Bundle Analysis
|
||||||
|
console.log("📊 Running bundle analysis...");
|
||||||
|
await this.runBundleAnalysis();
|
||||||
|
|
||||||
|
// 2. Performance Monitoring
|
||||||
|
console.log("📈 Running performance monitoring...");
|
||||||
|
await this.runPerformanceMonitoring();
|
||||||
|
|
||||||
|
// 3. Web Vitals Tracking
|
||||||
|
console.log("📊 Setting up Web Vitals tracking...");
|
||||||
|
await this.runWebVitalsTracking();
|
||||||
|
|
||||||
|
// 4. Lighthouse CI (if server is available)
|
||||||
|
console.log("🔍 Running Lighthouse CI...");
|
||||||
|
await this.runLighthouseCI();
|
||||||
|
|
||||||
|
// 5. Generate comprehensive report
|
||||||
|
this.generateComprehensiveReport();
|
||||||
|
|
||||||
|
console.log("✅ Performance testing complete!");
|
||||||
|
console.log(`📁 Results saved to: ${TEST_RESULTS_DIR}`);
|
||||||
|
|
||||||
|
// Return exit code based on results
|
||||||
|
const hasFailures = this.results.summary.failed > 0;
|
||||||
|
if (hasFailures) {
|
||||||
|
console.log("❌ Performance tests failed");
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log("✅ All performance tests passed");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Performance testing failed:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bundle analysis
|
||||||
|
*/
|
||||||
|
async runBundleAnalysis() {
|
||||||
|
try {
|
||||||
|
execSync("npm run bundle:analyze", { stdio: "inherit" });
|
||||||
|
|
||||||
|
// Parse bundle analysis results
|
||||||
|
const bundleReportPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
".next",
|
||||||
|
"analyze",
|
||||||
|
"bundle-analysis.json"
|
||||||
|
);
|
||||||
|
if (fs.existsSync(bundleReportPath)) {
|
||||||
|
const bundleData = JSON.parse(
|
||||||
|
fs.readFileSync(bundleReportPath, "utf8")
|
||||||
|
);
|
||||||
|
this.results.bundleAnalysis = bundleData;
|
||||||
|
|
||||||
|
// Check for budget violations
|
||||||
|
if (
|
||||||
|
bundleData.budgetViolations &&
|
||||||
|
bundleData.budgetViolations.length > 0
|
||||||
|
) {
|
||||||
|
this.results.summary.failed += bundleData.budgetViolations.length;
|
||||||
|
console.log(
|
||||||
|
`⚠️ Found ${bundleData.budgetViolations.length} budget violations`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.results.summary.passed += 1;
|
||||||
|
console.log("✅ Bundle analysis passed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Bundle analysis failed:", error.message);
|
||||||
|
this.results.summary.failed += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run performance monitoring
|
||||||
|
*/
|
||||||
|
async runPerformanceMonitoring() {
|
||||||
|
try {
|
||||||
|
execSync("npm run performance:monitor", { stdio: "inherit" });
|
||||||
|
|
||||||
|
// Parse performance monitoring results
|
||||||
|
const perfReportPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
".next",
|
||||||
|
"monitoring",
|
||||||
|
"performance-report.json"
|
||||||
|
);
|
||||||
|
if (fs.existsSync(perfReportPath)) {
|
||||||
|
const perfData = JSON.parse(fs.readFileSync(perfReportPath, "utf8"));
|
||||||
|
this.results.performanceMonitoring = perfData;
|
||||||
|
|
||||||
|
// Check for budget violations
|
||||||
|
if (perfData.budgetViolations && perfData.budgetViolations.length > 0) {
|
||||||
|
this.results.summary.failed += perfData.budgetViolations.length;
|
||||||
|
console.log(
|
||||||
|
`⚠️ Found ${perfData.budgetViolations.length} performance violations`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.results.summary.passed += 1;
|
||||||
|
console.log("✅ Performance monitoring passed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Performance monitoring failed:", error.message);
|
||||||
|
this.results.summary.failed += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Web Vitals tracking
|
||||||
|
*/
|
||||||
|
async runWebVitalsTracking() {
|
||||||
|
try {
|
||||||
|
execSync("npm run web-vitals:track", { stdio: "inherit" });
|
||||||
|
|
||||||
|
// Parse Web Vitals results
|
||||||
|
const vitalsReportPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
".next",
|
||||||
|
"web-vitals",
|
||||||
|
"report.json"
|
||||||
|
);
|
||||||
|
if (fs.existsSync(vitalsReportPath)) {
|
||||||
|
const vitalsData = JSON.parse(
|
||||||
|
fs.readFileSync(vitalsReportPath, "utf8")
|
||||||
|
);
|
||||||
|
this.results.webVitals = vitalsData;
|
||||||
|
console.log("✅ Web Vitals tracking setup complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results.summary.passed += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Web Vitals tracking failed:", error.message);
|
||||||
|
this.results.summary.failed += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Lighthouse CI
|
||||||
|
*/
|
||||||
|
async runLighthouseCI() {
|
||||||
|
try {
|
||||||
|
// Check if server is running
|
||||||
|
try {
|
||||||
|
execSync("curl -s http://localhost:3000 > /dev/null", {
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ Development server not running, skipping Lighthouse CI..."
|
||||||
|
);
|
||||||
|
this.results.summary.warnings += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
execSync("npm run lhci", { stdio: "inherit" });
|
||||||
|
|
||||||
|
// Parse Lighthouse results
|
||||||
|
const lhciResultsPath = path.join(__dirname, "..", ".lighthouseci");
|
||||||
|
if (fs.existsSync(lhciResultsPath)) {
|
||||||
|
const files = fs.readdirSync(lhciResultsPath);
|
||||||
|
const resultFile = files.find((f) => f.endsWith(".json"));
|
||||||
|
|
||||||
|
if (resultFile) {
|
||||||
|
const lhciData = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(lhciResultsPath, resultFile), "utf8")
|
||||||
|
);
|
||||||
|
this.results.lighthouse = lhciData;
|
||||||
|
console.log("✅ Lighthouse CI completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results.summary.passed += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("⚠️ Lighthouse CI failed:", error.message);
|
||||||
|
this.results.summary.warnings += 1;
|
||||||
|
this.results.summary.total += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate comprehensive test report
|
||||||
|
*/
|
||||||
|
generateComprehensiveReport() {
|
||||||
|
// Ensure test results directory exists
|
||||||
|
if (!fs.existsSync(TEST_RESULTS_DIR)) {
|
||||||
|
fs.mkdirSync(TEST_RESULTS_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportPath = path.join(
|
||||||
|
TEST_RESULTS_DIR,
|
||||||
|
"performance-test-report.json"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(reportPath, JSON.stringify(this.results, null, 2));
|
||||||
|
|
||||||
|
// Generate markdown report
|
||||||
|
this.generateMarkdownReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate markdown test report
|
||||||
|
*/
|
||||||
|
generateMarkdownReport() {
|
||||||
|
const reportPath = path.join(
|
||||||
|
TEST_RESULTS_DIR,
|
||||||
|
"performance-test-report.md"
|
||||||
|
);
|
||||||
|
|
||||||
|
let report = `# Performance Test Report\n\n`;
|
||||||
|
report += `**Generated:** ${this.results.timestamp}\n\n`;
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
report += `## Test Summary\n\n`;
|
||||||
|
report += `- **Total Tests:** ${this.results.summary.total}\n`;
|
||||||
|
report += `- **Passed:** ${this.results.summary.passed} ✅\n`;
|
||||||
|
report += `- **Failed:** ${this.results.summary.failed} ❌\n`;
|
||||||
|
report += `- **Warnings:** ${this.results.summary.warnings} ⚠️\n\n`;
|
||||||
|
|
||||||
|
// Bundle Analysis Results
|
||||||
|
if (Object.keys(this.results.bundleAnalysis).length > 0) {
|
||||||
|
report += `## Bundle Analysis\n\n`;
|
||||||
|
if (
|
||||||
|
this.results.bundleAnalysis.budgetViolations &&
|
||||||
|
this.results.bundleAnalysis.budgetViolations.length > 0
|
||||||
|
) {
|
||||||
|
report += `### Budget Violations\n\n`;
|
||||||
|
this.results.bundleAnalysis.budgetViolations.forEach((violation) => {
|
||||||
|
report += `- **${violation.file}**: ${
|
||||||
|
violation.currentSize
|
||||||
|
}KB (exceeds ${violation.maxSize}KB by ${
|
||||||
|
violation.overage
|
||||||
|
}KB) - ${violation.severity.toUpperCase()}\n`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report += `✅ No bundle budget violations found\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance Monitoring Results
|
||||||
|
if (Object.keys(this.results.performanceMonitoring).length > 0) {
|
||||||
|
report += `## Performance Monitoring\n\n`;
|
||||||
|
if (
|
||||||
|
this.results.performanceMonitoring.budgetViolations &&
|
||||||
|
this.results.performanceMonitoring.budgetViolations.length > 0
|
||||||
|
) {
|
||||||
|
report += `### Budget Violations\n\n`;
|
||||||
|
this.results.performanceMonitoring.budgetViolations.forEach(
|
||||||
|
(violation) => {
|
||||||
|
report += `- **${violation.metric}**: ${
|
||||||
|
violation.current
|
||||||
|
} (exceeds ${
|
||||||
|
violation.budget
|
||||||
|
}) - ${violation.severity.toUpperCase()}\n`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
report += `✅ No performance budget violations found\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web Vitals Results
|
||||||
|
if (Object.keys(this.results.webVitals).length > 0) {
|
||||||
|
report += `## Web Vitals Tracking\n\n`;
|
||||||
|
report += `✅ Web Vitals tracking setup complete\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lighthouse Results
|
||||||
|
if (Object.keys(this.results.lighthouse).length > 0) {
|
||||||
|
report += `## Lighthouse CI\n\n`;
|
||||||
|
report += `✅ Lighthouse CI completed successfully\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recommendations
|
||||||
|
report += `## Recommendations\n\n`;
|
||||||
|
report += `- Monitor bundle sizes regularly\n`;
|
||||||
|
report += `- Track Core Web Vitals in production\n`;
|
||||||
|
report += `- Run performance tests in CI/CD pipeline\n`;
|
||||||
|
report += `- Set up performance budgets and alerts\n`;
|
||||||
|
|
||||||
|
fs.writeFileSync(reportPath, report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
const tester = new PerformanceTester();
|
||||||
|
tester.runTests().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PerformanceTester;
|
||||||
Reference in New Issue
Block a user