Skip to content
Performance Testing logo

Performance Testing

> **Skill Purpose:** Performance audit implementation, load testing patterns, and performance benchmarking procedures

Coverage-Creatives/zeus0installs0stars

SKILL.md

Full skill instructions

Performance Testing

Skill Purpose: Performance audit implementation, load testing patterns, and performance benchmarking procedures


Core Skill Pattern

Objective: Establish comprehensive performance testing strategy with Lighthouse audits, load testing, and performance monitoring.

Universal Pattern:

  1. Define performance testing strategy and benchmarks
  2. Create Lighthouse performance audit procedures
  3. Establish load testing and stress testing patterns
  4. Set up performance monitoring and reporting
  5. Create performance optimization and remediation procedures

Key Decisions (Project-Specific):

  • Performance metrics and thresholds (Core Web Vitals)
  • Load testing tools and scenarios
  • Performance monitoring frequency and alerts
  • Benchmark targets and improvement goals
  • Performance regression detection procedures

Project-Specific Implementation Notes

Customize per project:

  • Performance budgets based on user experience requirements
  • Load testing scenarios based on expected traffic patterns
  • Monitoring frequency based on application criticality
  • Benchmark targets based on industry standards and competitors
  • Testing tools based on tech stack and infrastructure

Example Implementation (Next.js Performance Testing Pattern)

Note: This is an example pattern using Lighthouse, WebPageTest, and custom performance monitoring. Adapt performance tools and patterns based on your specific project requirements and performance needs.

Prerequisites (Example)

  • Next.js project with performance optimization
  • Lighthouse CLI configured
  • Performance monitoring tools set up
  • Load testing environment established

Example: Performance Testing Implementation

Framework-Specific Example: This demonstrates performance testing patterns with Next.js. Adapt for your performance framework and application requirements.

Performance Configuration

// lib/​performance-config.ts
export interface PerformanceConfig {
  coreWebVitals: {
    lcp: number; // Largest Contentful Paint (ms)
    fid: number; // First Input Delay (ms)
    cls: number; // Cumulative Layout Shift
    fcp: number; // First Contentful Paint (ms)
    ttfb: number; // Time to First Byte (ms)
  };
  loadTesting: {
    concurrentUsers: number;
    rampUpTime: number; // seconds
    testDuration: number; // seconds
    thinkTime: number; // seconds between requests
  };
  monitoring: {
    alertThresholds: {
      responseTime: number; // ms
      errorRate: number; // percentage
      throughput: number; // requests per second
    };
    samplingRate: number; // percentage of requests to monitor
  };
}

export const performanceConfig: PerformanceConfig = {
  coreWebVitals: {
    lcp: 2500, // Good: <2.5s
    fid: 100,  // Good: <100ms
    cls: 0.1,   // Good: <0.1
    fcp: 1800, // Good: <1.8s
    ttfb: 600,  // Good: <600ms
  },
  loadTesting: {
    concurrentUsers: 100,
    rampUpTime: 60,
    testDuration: 300,
    thinkTime: 2,
  },
  monitoring: {
    alertThresholds: {
      responseTime: 1000,
      errorRate: 1,
      throughput: 10,
    },
    samplingRate: 10,
  },
};

Lighthouse Performance Testing

// scripts/​lighthouse-audit.ts
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
import { writeFileSync } from 'fs';
import { performanceConfig } from '../​lib/​performance-config';

export class LighthouseAuditor {
  static async runAudit(url: string, outputPath: string) {
    const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
    const options = {
      logLevel: 'info',
      output: 'json',
      onlyCategories: ['performance'],
      port: chrome.port,
    };

    try {
      const runnerResult = await lighthouse(url, options);
      const { lhr } = runnerResult;
      
      const performanceScore = lhr.categories.performance.score * 100;
      const coreWebVitals = this.extractCoreWebVitals(lhr);
      
      const results = {
        url,
        timestamp: new Date().toISOString(),
        performanceScore,
        coreWebVitals,
        audits: this.extractKeyAudits(lhr),
        recommendations: this.generateRecommendations(lhr),
      };
      
      // Save results
      writeFileSync(outputPath, JSON.stringify(results, null, 2));
      
      // Check against thresholds
      this.validateResults(results);
      
      return results;
    } finally {
      await chrome.kill();
    }
  }
  
  private static extractCoreWebVitals(lhr: any) {
    return {
      lcp: lhr.audits['largest-contentful-paint'].numericValue,
      fid: lhr.audits['max-potential-fid'].numericValue,
      cls: lhr.audits['cumulative-layout-shift'].numericValue,
      fcp: lhr.audits['first-contentful-paint'].numericValue,
      ttfb: lhr.audits['server-response-time'].numericValue,
    };
  }
  
  private static extractKeyAudits(lhr: any) {
    return {
      'first-contentful-paint': lhr.audits['first-contentful-paint'].numericValue,
      'largest-contentful-paint': lhr.audits['largest-contentful-paint'].numericValue,
      'speed-index': lhr.audits['speed-index'].numericValue,
      'total-blocking-time': lhr.audits['total-blocking-time'].numericValue,
      'cumulative-layout-shift': lhr.audits['cumulative-layout-shift'].numericValue,
    };
  }
  
  private static generateRecommendations(lhr: any) {
    const recommendations = [];
    
    // Check performance budget
    if (lhr.audits['total-byte-weight'].numericValue > 1500000) {
      recommendations.push({
        type: 'size',
        issue: 'Large total page size',
        suggestion: 'Optimize images, enable compression, reduce bundle size',
        impact: 'high',
      });
    }
    
    // Check unused JavaScript
    if (lhr.audits['unused-javascript'].numericValue > 0) {
      recommendations.push({
        type: 'javascript',
        issue: 'Unused JavaScript detected',
        suggestion: 'Remove unused code, implement code splitting',
        impact: 'medium',
      });
    }
    
    // Check render-blocking resources
    if (lhr.audits['render-blocking-resources'].details.items.length > 0) {
      recommendations.push({
        type: 'render-blocking',
        issue: 'Render-blocking resources found',
        suggestion: 'Optimize CSS delivery, defer non-critical JavaScript',
        impact: 'high',
      });
    }
    
    return recommendations;
  }
  
  private static validateResults(results: any) {
    const { coreWebVitals, performanceScore } = results;
    const thresholds = performanceConfig.coreWebVitals;
    
    const issues = [];
    
    if (coreWebVitals.lcp > thresholds.lcp) {
      issues.push(`LCP too slow: ${coreWebVitals.lcp}ms > ${thresholds.lcp}ms`);
    }
    
    if (coreWebVitals.fid > thresholds.fid) {
      issues.push(`FID too slow: ${coreWebVitals.fid}ms > ${thresholds.fid}ms`);
    }
    
    if (coreWebVitals.cls > thresholds.cls) {
      issues.push(`CLS too high: ${coreWebVitals.cls} > ${thresholds.cls}`);
    }
    
    if (performanceScore < 90) {
      issues.push(`Performance score too low: ${performanceScore} < 90`);
    }
    
    if (issues.length > 0) {
      console.error('Performance issues detected:', issues);
      throw new Error(`Performance audit failed: ${issues.join(', ')}`);
    }
    
    console.log('✅ Performance audit passed');
  }
}

Load Testing with Artillery

# artillery/​load-test-config.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 5
      name: "Warm up"
    - duration: 120
      arrivalRate: 10
      name: "Ramp up load"
    - duration: 300
      arrivalRate: 50
      name: "Sustained load"
  processor: "./​test-processor.js"

scenarios:
  - name: "User Journey"
    weight: 70
    flow:
      - get:
          url: "/"
      - think: 2
      - get:
          url: "/​products"
      - think: 1
      - get:
          url: "/​api/​products"
      - think: 1
      
  - name: "API Load"
    weight: 30
    flow:
      - get:
          url: "/​api/​products"
      - think: 0.5
      - get:
          url: "/​api/​users"
      - think: 0.5
// artillery/​test-processor.js
module.exports = {
  // Custom functions for load testing
  generateRandomUserId() {
    return Math.floor(Math.random() * 1000) + 1;
  },
  
  generateTestData() {
    return {
      email: `test-${Date.now()}@example.com`,
      name: 'Load Test User',
    };
  }
};

Performance Monitoring

// lib/​performance-monitor.ts
export class PerformanceMonitor {
  private static metrics: Map<string, number[]> = new Map();
  
  static recordMetric(name: string, value: number) {
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }
    this.metrics.get(name)!.push(value);
  }
  
  static getAverageMetric(name: string): number {
    const values = this.metrics.get(name) || [];
    return values.length > 0 ? values.reduce((a, b) => a + b) / values.length : 0;
  }
  
  static getPercentileMetric(name: string, percentile: number): number {
    const values = (this.metrics.get(name) || []).sort((a, b) => a - b);
    const index = Math.ceil(values.length * (percentile / 100)) - 1;
    return values[index] || 0;
  }
  
  static generateReport() {
    const report = {
      timestamp: new Date().toISOString(),
      metrics: {} as Record<string, any>,
    };
    
    for (const [name, values] of this.metrics.entries()) {
      report.metrics[name] = {
        count: values.length,
        average: this.getAverageMetric(name),
        p50: this.getPercentileMetric(name, 50),
        p95: this.getPercentileMetric(name, 95),
        p99: this.getPercentileMetric(name, 99),
        min: Math.min(...values),
        max: Math.max(...values),
      };
    }
    
    return report;
  }
  
  static checkThresholds(metricName: string, threshold: number) {
    const average = this.getAverageMetric(metricName);
    if (average > threshold) {
      console.warn(`⚠️ Performance threshold exceeded: ${metricName} = ${average} > ${threshold}`);
      return false;
    }
    return true;
  }
}

Performance Middleware

// middleware/​performance.ts
import { NextResponse } from 'next/​server';
import type { NextRequest } from 'next/​server';
import { PerformanceMonitor } from '../​lib/​performance-monitor';

export function performanceMiddleware(request: NextRequest) {
  const startTime = Date.now();
  
  const response = NextResponse.next();
  
  response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`);
  
  // Record performance metrics
  if (process.env.NODE_ENV === 'production') {
    const responseTime = Date.now() - startTime;
    PerformanceMonitor.recordMetric('response-time', responseTime);
    
    // Check performance thresholds
    const thresholds = performanceConfig.monitoring.alertThresholds;
    PerformanceMonitor.checkThresholds('response-time', thresholds.responseTime);
  }
  
  return response;
}

export const config = {
  matcher: ['/​api/:path*'],
};

Performance Testing Utilities

// utils/​performance-testing.ts
export class PerformanceTestingUtils {
  static async measurePageLoad(url: string) {
    const startTime = performance.now();
    
    // Navigate to page
    await page.goto(url, { waitUntil: 'networkidle' });
    
    // Wait for LCP
    await this.waitForLargestContentfulPaint();
    
    const loadTime = performance.now() - startTime;
    
    return {
      loadTime,
      coreWebVitals: await this.getCoreWebVitals(),
    };
  }
  
  static async waitForLargestContentfulPaint() {
    return new Promise((resolve) => {
      // @ts-ignore
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lcpEntry = entries.find(entry => entry.name === 'largest-contentful-paint');
        if (lcpEntry) {
          resolve(lcpEntry.startTime);
        }
      }).observe({ entryTypes: ['largest-contentful-paint'] });
    });
  }
  
  static async getCoreWebVitals() {
    return new Promise((resolve) => {
      // @ts-ignore
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const vitals = {
          lcp: entries.find(e => e.name === 'largest-contentful-paint')?.startTime || 0,
          fid: entries.find(e => e.name === 'first-input')?.processingStart - entries.find(e => e.name === 'first-input')?.startTime || 0,
          cls: entries.find(e => e.name === 'cumulative-layout-shift')?.value || 0,
        };
        resolve(vitals);
      }).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'cumulative-layout-shift'] });
    });
  }
  
  static async simulateSlowNetwork() {
    // Simulate 3G network conditions
    await page.route('**/​*', route => {
      // Add delay to simulate slow network
      setTimeout(() => {
        route.continue();
      }, 1000);
    });
  }
  
  static async measureBundleSize() {
    const bundleStats = await page.evaluate(() => {
      // @ts-ignore
      return performance.getEntriesByType('resource')
        .filter(entry => entry.name.includes('.js') || entry.name.includes('.css'))
        .reduce((total, entry) => total + entry.transferSize, 0);
    });
    
    return {
      totalSize: bundleStats,
      sizeFormatted: this.formatBytes(bundleStats),
    };
  }
  
  private static formatBytes(bytes: number): string {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
}

Performance Test Automation

// __tests__/​performance/​performance.test.ts
import { test, expect } from '@playwright/​test';
import { PerformanceTestingUtils } from '../​utils/​performance-testing';

test.describe('Performance Tests', () => {
  test('should meet Core Web Vitals thresholds', async ({ page }) => {
    await page.goto('/');
    
    const vitals = await PerformanceTestingUtils.getCoreWebVitals();
    
    expect(vitals.lcp).toBeLessThan(2500); // LCP: 2.5s
    expect(vitals.fid).toBeLessThan(100);   // FID: 100ms
    expect(vitals.cls).toBeLessThan(0.1);   // CLS: 0.1
  });
  
  test('should load within performance budget', async ({ page }) => {
    const startTime = Date.now();
    await page.goto('/');
    await page.waitForLoadState('networkidle');
    
    const loadTime = Date.now() - startTime;
    expect(loadTime).toBeLessThan(3000); // 3 seconds
  });
  
  test('should have acceptable bundle size', async ({ page }) => {
    await page.goto('/');
    
    const bundleSize = await PerformanceTestingUtils.measureBundleSize();
    expect(bundleSize.totalSize).toBeLessThan(1500000); // 1.5MB
  });
  
  test('should perform well on slow network', async ({ page }) => {
    await PerformanceTestingUtils.simulateSlowNetwork();
    
    const startTime = Date.now();
    await page.goto('/');
    await page.waitForLoadState('networkidle');
    
    const loadTime = Date.now() - startTime;
    expect(loadTime).toBeLessThan(10000); // 10 seconds on slow network
  });
});

Best Practices

  1. Performance budgets - Set and enforce budgets for bundle size, load time, and Core Web Vitals
  2. Continuous monitoring - Monitor performance in production and set up alerts for regressions
  3. Realistic testing - Test with realistic network conditions and device capabilities
  4. Progressive enhancement - Ensure core functionality works even with poor performance
  5. Performance regression detection - Automatically detect and prevent performance regressions

Stop Conditions

STOP and escalate if:

  • Performance requirements unclear or incomplete
  • Performance thresholds not defined
  • Testing environment not representative of production
  • Performance monitoring strategy missing

Skill Version: 1.0.0