Performance Testing
> **Skill Purpose:** Performance audit implementation, load testing patterns, and performance benchmarking procedures
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:
- Define performance testing strategy and benchmarks
- Create Lighthouse performance audit procedures
- Establish load testing and stress testing patterns
- Set up performance monitoring and reporting
- 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
- Performance budgets - Set and enforce budgets for bundle size, load time, and Core Web Vitals
- Continuous monitoring - Monitor performance in production and set up alerts for regressions
- Realistic testing - Test with realistic network conditions and device capabilities
- Progressive enhancement - Ensure core functionality works even with poor performance
- 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
