Agent skill
performance-testing-anton-abyzov-specweave
Performance testing expert for load testing (k6, Artillery), web performance (Lighthouse CI, Core Web Vitals), database performance, and memory/resource profiling. Use for performance tests, load tests, stress tests, lighthouse audits, k6 scripts, web vitals monitoring, or performance budgets.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/performance-testing-anton-abyzov-specweave
SKILL.md
Performance Testing Expert
When to Use
Invoke this skill when the task involves:
- Load testing: "performance test", "load test", "stress test", "soak test", "spike test"
- Web performance: "lighthouse", "performance budget", "bundle size", "core web vitals"
- Specific tools: "k6", "artillery", "lighthouse ci", "webpagetest"
- Metrics: "web vitals", "LCP", "FID", "INP", "CLS", "TTFB", "p95 latency"
- Database performance: "slow query", "n+1 query", "connection pool"
- Resource profiling: "memory leak", "cpu profile", "network throttle"
Core Expertise
- Load testing with k6 (primary) and Artillery
- Web performance with Lighthouse CI and performance budgets
- Core Web Vitals monitoring and optimization
- Database performance testing and query analysis
- Memory and resource leak detection and profiling
1. Load Testing with k6
k6 is the primary tool for HTTP load testing. It uses JavaScript ES6 modules, runs in Go for high performance, and integrates with CI/CD pipelines.
Installation
# macOS
brew install k6
# Docker
docker run --rm -i grafana/k6 run - <script.js
# npm (for CI)
npm install -g k6
Basic k6 Script
// load-tests/api-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 20 }, // ramp up to 20 VUs
{ duration: '3m', target: 20 }, // hold at 20 VUs
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // error rate below 1%
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body has users': (r) => JSON.parse(r.body).length > 0,
});
sleep(1);
}
k6 Scenario Patterns
Stress Test (Find Breaking Point)
export const options = {
scenarios: {
stress: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 },
{ duration: '5m', target: 300 },
{ duration: '5m', target: 0 },
],
},
},
thresholds: {
http_req_duration: ['p(99)<1500'],
http_req_failed: ['rate<0.05'],
},
};
Spike Test (Sudden Burst)
export const options = {
scenarios: {
spike: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '30s', target: 10 }, // warm up
{ duration: '10s', target: 500 }, // spike
{ duration: '1m', target: 500 }, // hold spike
{ duration: '10s', target: 10 }, // drop
{ duration: '2m', target: 10 }, // recovery
{ duration: '30s', target: 0 }, // ramp down
],
},
},
};
Soak Test (Extended Duration)
export const options = {
scenarios: {
soak: {
executor: 'constant-vus',
vus: 50,
duration: '4h',
},
},
thresholds: {
http_req_duration: ['p(95)<800', 'p(99)<1500'],
http_req_failed: ['rate<0.01'],
},
};
Constant Request Rate
export const options = {
scenarios: {
constant_rate: {
executor: 'constant-arrival-rate',
rate: 100, // 100 requests per timeUnit
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
};
k6 Custom Metrics
import { Counter, Trend, Rate, Gauge } from 'k6/metrics';
const orderDuration = new Trend('order_duration');
const orderErrors = new Rate('order_errors');
const ordersCreated = new Counter('orders_created');
export default function () {
const start = Date.now();
const res = http.post('https://api.example.com/orders', JSON.stringify({
product: 'widget', quantity: 1,
}), { headers: { 'Content-Type': 'application/json' } });
orderDuration.add(Date.now() - start);
orderErrors.add(res.status !== 201);
if (res.status === 201) ordersCreated.add(1);
}
k6 Authentication and Headers
import http from 'k6/http';
import { check } from 'k6';
export function setup() {
const loginRes = http.post('https://api.example.com/auth/login', JSON.stringify({
username: 'loadtest',
password: __ENV.LOAD_TEST_PASSWORD,
}), { headers: { 'Content-Type': 'application/json' } });
return { token: JSON.parse(loginRes.body).token };
}
export default function (data) {
const params = {
headers: {
Authorization: `Bearer ${data.token}`,
'Content-Type': 'application/json',
},
};
const res = http.get('https://api.example.com/protected', params);
check(res, { 'authenticated request OK': (r) => r.status === 200 });
}
k6 Multi-Scenario (Mixed Workload)
export const options = {
scenarios: {
browse: {
executor: 'constant-vus', vus: 30, duration: '10m',
exec: 'browseProducts',
},
checkout: {
executor: 'ramping-arrival-rate', startRate: 5, timeUnit: '1s',
stages: [{ duration: '5m', target: 20 }, { duration: '5m', target: 20 }],
preAllocatedVUs: 50, exec: 'checkout',
},
},
thresholds: {
'http_req_duration{scenario:browse}': ['p(95)<300'],
'http_req_duration{scenario:checkout}': ['p(95)<1000'],
},
};
export function browseProducts() {
http.get('https://api.example.com/products');
sleep(Math.random() * 3 + 1);
}
export function checkout() {
http.post('https://api.example.com/checkout', JSON.stringify({
items: [{ id: 1, qty: 1 }],
}), { headers: { 'Content-Type': 'application/json' } });
}
Running k6
k6 run load-tests/api-load.js # Basic run
k6 run -e BASE_URL=https://staging.example.com load-tests/api-load.js # Env vars
k6 run --vus 100 --duration 5m load-tests/api-load.js # Override options
k6 run --out json=results.json load-tests/api-load.js # JSON output
k6 run --out influxdb=http://localhost:8086/k6 load-tests/api-load.js # Grafana
2. Load Testing with Artillery
Artillery uses YAML configuration and is well-suited for quick HTTP and WebSocket load tests.
Artillery Configuration
# load-tests/artillery.yml
config:
target: "https://api.example.com"
phases:
- duration: 60
arrivalRate: 5
name: "Warm up"
- duration: 120
arrivalRate: 20
rampTo: 50
name: "Ramp up"
- duration: 300
arrivalRate: 50
name: "Sustained load"
defaults:
headers:
Content-Type: "application/json"
ensure:
p95: 500
maxErrorRate: 1
scenarios:
- name: "Browse and Purchase"
weight: 70
flow:
- get:
url: "/products"
capture:
- json: "$[0].id"
as: "productId"
- think: 2
- get:
url: "/products/{{ productId }}"
- post:
url: "/cart"
json:
productId: "{{ productId }}"
quantity: 1
- name: "Search"
weight: 30
flow:
- get:
url: "/search?q=widget"
- think: 3
Running Artillery
npx artillery run load-tests/artillery.yml # Run test
npx artillery run --output report.json load-tests/artillery.yml # Save results
npx artillery report report.json # HTML report
npx artillery quick --count 50 --num 10 https://api.example.com/health # Quick test
3. Web Performance with Lighthouse CI
Lighthouse CI Configuration
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: [
'http://localhost:3000/',
'http://localhost:3000/products',
'http://localhost:3000/checkout',
],
startServerCommand: 'npm run start',
startServerReadyPattern: 'Server started',
numberOfRuns: 3,
settings: { preset: 'desktop' },
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['warn', { minScore: 0.9 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
// Core Web Vitals
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 300 }],
// Bundle and resource budgets
'resource-summary:script:size': ['error', { maxNumericValue: 300000 }],
'resource-summary:total:size': ['error', { maxNumericValue: 800000 }],
'resource-summary:third-party:count': ['warn', { maxNumericValue: 10 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
Performance Budgets
[
{
"path": "/*",
"timings": [
{ "metric": "interactive", "budget": 3000 },
{ "metric": "first-contentful-paint", "budget": 1500 },
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
{ "metric": "total-blocking-time", "budget": 300 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "stylesheet", "budget": 100 },
{ "resourceType": "image", "budget": 500 },
{ "resourceType": "total", "budget": 1000 }
],
"resourceCounts": [
{ "resourceType": "script", "budget": 15 },
{ "resourceType": "third-party", "budget": 10 },
{ "resourceType": "total", "budget": 50 }
]
}
]
Lighthouse CI in GitHub Actions
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Upload Lighthouse Report
if: always()
uses: actions/upload-artifact@v4
with:
name: lighthouse-report
path: .lighthouseci/
Lighthouse Programmatic API
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
async function runLighthouse(url: string) {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const result = await lighthouse(url, {
port: chrome.port, output: 'json', onlyCategories: ['performance'],
});
const { categories, audits } = result.lhr;
console.log('Performance Score:', categories.performance.score * 100);
console.log('LCP:', audits['largest-contentful-paint'].displayValue);
console.log('TBT:', audits['total-blocking-time'].displayValue);
console.log('CLS:', audits['cumulative-layout-shift'].displayValue);
if (categories.performance.score < 0.9) {
console.error('Performance score below 90!');
process.exit(1);
}
await chrome.kill();
}
4. Core Web Vitals Monitoring
Thresholds
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | <= 2.5s | <= 4.0s | > 4.0s |
| INP (Interaction to Next Paint) | <= 200ms | <= 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | <= 0.1 | <= 0.25 | > 0.25 |
| FCP (First Contentful Paint) | <= 1.8s | <= 3.0s | > 3.0s |
| TTFB (Time to First Byte) | <= 800ms | <= 1800ms | > 1800ms |
PerformanceObserver API
// LCP Observer
function observeLCP(callback: (value: number) => void) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
callback(entries[entries.length - 1].startTime);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
// INP Observer (replaces FID)
function observeINP(callback: (value: number) => void) {
let maxDuration = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const inp = (entry as PerformanceEventTiming).duration;
if (inp > maxDuration) { maxDuration = inp; callback(inp); }
}
});
observer.observe({ type: 'event', buffered: true });
}
// CLS Observer
function observeCLS(callback: (value: number) => void) {
let clsValue = 0, sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as any[]) {
if (!entry.hadRecentInput) {
const first = sessionEntries[0], last = sessionEntries[sessionEntries.length - 1];
if (sessionValue && entry.startTime - last?.startTime < 1000
&& entry.startTime - first?.startTime < 5000) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
if (sessionValue > clsValue) { clsValue = sessionValue; callback(clsValue); }
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
}
Using the web-vitals Library
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: { name: string; value: number; rating: string; id: string }) {
fetch('/api/vitals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: metric.name, value: metric.value, rating: metric.rating,
url: window.location.href, timestamp: Date.now(),
}),
keepalive: true,
});
}
export function initVitalsReporting() {
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}
Real User Monitoring (RUM) Setup
class RealUserMonitoring {
constructor(private config: { endpoint: string; sampleRate: number; appVersion: string }) {}
init() {
if (Math.random() > this.config.sampleRate) return;
this.observeNavigation();
this.observeResources();
this.observeLongTasks();
}
private observeNavigation() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const nav = entry as PerformanceNavigationTiming;
this.report('navigation', {
ttfb: nav.responseStart - nav.requestStart,
domComplete: nav.domComplete - nav.domInteractive,
totalTime: nav.loadEventEnd - nav.startTime,
});
}
}).observe({ type: 'navigation', buffered: true });
}
private observeResources() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const res = entry as PerformanceResourceTiming;
if (res.duration > 1000) {
this.report('slow-resource', {
name: res.name, duration: res.duration, size: res.transferSize,
});
}
}
}).observe({ type: 'resource', buffered: true });
}
private observeLongTasks() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.report('long-task', { duration: entry.duration, startTime: entry.startTime });
}
}).observe({ type: 'longtask', buffered: true });
}
private report(type: string, data: Record<string, unknown>) {
fetch(this.config.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type, data, appVersion: this.config.appVersion,
url: window.location.href, timestamp: Date.now(),
}),
keepalive: true,
}).catch(() => { /* RUM should never break the app */ });
}
}
// Usage: sample 10% of users
const rum = new RealUserMonitoring({ endpoint: '/api/rum', sampleRate: 0.1, appVersion: '1.2.3' });
rum.init();
5. Database Performance Testing
Slow Query Detection
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { Pool } from 'pg';
describe('Database Query Performance', () => {
let pool: Pool;
beforeAll(async () => { pool = new Pool({ connectionString: process.env.DATABASE_URL }); });
afterAll(async () => { await pool.end(); });
it('should fetch user by ID within 50ms', async () => {
const start = performance.now();
await pool.query('SELECT * FROM users WHERE id = $1', [1]);
expect(performance.now() - start).toBeLessThan(50);
});
it('should detect missing indexes via EXPLAIN ANALYZE', async () => {
const result = await pool.query(
"EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_email = '[email protected]'"
);
const plan = result.rows.map((r) => r['QUERY PLAN']).join('\n');
// Should use index scan, not sequential scan on large tables
expect(plan).not.toMatch(/Seq Scan.*orders/);
});
});
Connection Pool Testing
describe('Connection Pool Behavior', () => {
it('should handle concurrent connections within pool limits', async () => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, idleTimeoutMillis: 5000, connectionTimeoutMillis: 3000,
});
const concurrentQueries = 20;
const start = performance.now();
const results = await Promise.allSettled(
Array.from({ length: concurrentQueries }, () => pool.query('SELECT pg_sleep(0.1)'))
);
const duration = performance.now() - start;
expect(results.filter((r) => r.status === 'fulfilled').length).toBe(concurrentQueries);
// 10 pool connections, 20 queries at 100ms: ~200ms (2 batches), not 2000ms
expect(duration).toBeLessThan(1000);
await pool.end();
});
it('should timeout when pool is exhausted', async () => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL, max: 2, connectionTimeoutMillis: 500,
});
const held = await Promise.all([pool.connect(), pool.connect()]);
await expect(pool.connect()).rejects.toThrow(/timeout/);
for (const client of held) client.release();
await pool.end();
});
});
N+1 Query Detection
class QueryCounter {
queries: string[] = [];
private originalQuery: Function;
constructor(private pool: any) { this.originalQuery = pool.query.bind(pool); }
start() {
this.queries = [];
this.pool.query = (...args: any[]) => { this.queries.push(args[0]); return this.originalQuery(...args); };
}
stop() { this.pool.query = this.originalQuery; }
get count() { return this.queries.length; }
}
describe('N+1 Query Detection', () => {
it('should fetch orders with items in constant queries (not N+1)', async () => {
const counter = new QueryCounter(pool);
counter.start();
const orders = await orderService.getOrdersWithItems({ limit: 50 });
counter.stop();
// Expect at most 2-3 queries (orders + items batch), not 51 (1 + N)
expect(counter.count).toBeLessThanOrEqual(3);
expect(orders.length).toBeGreaterThan(0);
});
});
6. Memory and Resource Testing
Memory Leak Detection in Node.js
describe('Memory Leak Detection', () => {
it('should not leak memory during repeated operations', async () => {
if (global.gc) global.gc(); // Run with --expose-gc
const baselineMemory = process.memoryUsage().heapUsed;
const iterations = 1000;
for (let i = 0; i < iterations; i++) {
await processRequest({ id: i, data: 'test' });
}
if (global.gc) global.gc();
const growthPerIteration = (process.memoryUsage().heapUsed - baselineMemory) / iterations;
expect(growthPerIteration).toBeLessThan(1024); // < 1KB per iteration
});
it('should properly clean up event listeners', () => {
const emitter = new EventEmitter();
const initialListeners = emitter.listenerCount('data');
for (let i = 0; i < 100; i++) {
const service = new DataService(emitter);
service.start();
service.stop(); // Must remove listeners
}
expect(emitter.listenerCount('data')).toBe(initialListeners);
});
});
Browser Memory Profiling (Playwright)
import { test, expect } from '@playwright/test';
test('should not leak memory on repeated modal open/close', async ({ page }) => {
await page.goto('/dashboard');
const getMemory = () => page.evaluate(() => (performance as any).memory?.usedJSHeapSize || 0);
const initialMemory = await getMemory();
for (let i = 0; i < 20; i++) {
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[data-testid="modal"]');
await page.click('[data-testid="close-modal"]');
await page.waitForSelector('[data-testid="modal"]', { state: 'hidden' });
}
await page.evaluate(() => { if ((window as any).gc) (window as any).gc(); });
expect(await getMemory() - initialMemory).toBeLessThan(5 * 1024 * 1024); // < 5MB growth
});
CPU Profiling
import { Session } from 'inspector';
import { writeFileSync } from 'fs';
async function profileOperation(operation: () => Promise<void>) {
const session = new Session();
session.connect();
session.post('Profiler.enable');
session.post('Profiler.start');
await operation();
return new Promise<void>((resolve) => {
session.post('Profiler.stop', (err, { profile }) => {
if (!err) writeFileSync('cpu-profile.cpuprofile', JSON.stringify(profile));
session.disconnect();
resolve();
});
});
}
Network Throttling Test Strategies
import { test, expect } from '@playwright/test';
const networkConditions = {
'slow-3g': { downloadThroughput: 50_000, uploadThroughput: 25_000, latency: 400 },
'fast-3g': { downloadThroughput: 187_500, uploadThroughput: 75_000, latency: 150 },
'regular-4g': { downloadThroughput: 500_000, uploadThroughput: 250_000, latency: 50 },
};
const budgets: Record<string, number> = { 'slow-3g': 3000, 'fast-3g': 1500, 'regular-4g': 800 };
for (const [name, conditions] of Object.entries(networkConditions)) {
test(`page loads within budget on ${name}`, async ({ page }) => {
const cdp = await page.context().newCDPSession(page);
await cdp.send('Network.emulateNetworkConditions', { offline: false, ...conditions });
const start = Date.now();
await page.goto('/', { waitUntil: 'domcontentloaded' });
expect(Date.now() - start).toBeLessThan(budgets[name]);
});
}
Bundle Size Budget Tests
import { describe, it, expect } from 'vitest';
import { statSync, readdirSync } from 'fs';
import { join } from 'path';
describe('Bundle Size Budget', () => {
const distDir = join(process.cwd(), 'dist');
function totalSize(dir: string, ext: string): number {
const files = readdirSync(dir, { recursive: true }) as string[];
return files
.filter((f) => f.endsWith(ext))
.reduce((sum, f) => sum + statSync(join(dir, f)).size, 0);
}
it('total JS bundle should be under 300KB', () => {
expect(totalSize(distDir, '.js')).toBeLessThan(300 * 1024);
});
it('total CSS should be under 100KB', () => {
expect(totalSize(distDir, '.css')).toBeLessThan(100 * 1024);
});
});
7. CI/CD Performance Regression Detection
Performance Gate in GitHub Actions
# .github/workflows/perf-check.yml
name: Performance Check
on: [pull_request]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Start server
run: npm start &
env:
NODE_ENV: production
- run: npx wait-on http://localhost:3000 --timeout 30000
- name: Run k6 load test
uses: grafana/[email protected]
with:
filename: load-tests/api-load.js
flags: --out json=k6-results.json
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: k6-results
path: k6-results.json
Performance Comparison Script
interface PerfMetrics { p50: number; p95: number; p99: number; errorRate: number; throughput: number; }
function compareMetrics(base: PerfMetrics, current: PerfMetrics) {
const regressions: string[] = [];
const threshold = 0.10; // 10% regression threshold
if (current.p95 > base.p95 * (1 + threshold))
regressions.push(`p95 regressed: ${base.p95.toFixed(0)}ms -> ${current.p95.toFixed(0)}ms`);
if (current.errorRate > base.errorRate * (1 + threshold) && current.errorRate > 0.01)
regressions.push(`Error rate regressed: ${(base.errorRate*100).toFixed(2)}% -> ${(current.errorRate*100).toFixed(2)}%`);
if (current.throughput < base.throughput * (1 - threshold))
regressions.push(`Throughput regressed: ${base.throughput.toFixed(0)} -> ${current.throughput.toFixed(0)} rps`);
return { passed: regressions.length === 0, regressions };
}
8. Quick Reference
CLI Commands
# k6
k6 run script.js # Run load test
k6 run --vus 50 --duration 2m # Override options
k6 run -e API_KEY=xxx script.js # Pass env vars
k6 run --out json=out.json # JSON output
k6 inspect script.js # Validate script
# Lighthouse CI
lhci autorun # Full CI pipeline
lhci collect --url=http://... # Collect only
lhci assert # Assert against config
lhci upload # Upload results
Performance Testing Decision Matrix
| Scenario | Tool | Configuration |
|---|---|---|
| API load testing | k6 | ramping-vus, thresholds |
| Stress/breaking point | k6 | ramping-vus with high targets |
| Soak testing | k6 | constant-vus, long duration |
| Spike testing | k6 | ramping-vus, sudden jumps |
| Web performance audit | Lighthouse CI | assert with budgets |
| Bundle size regression | Vitest | statSync + budget checks |
| Core Web Vitals (lab) | Lighthouse CI | performance category |
| Core Web Vitals (field) | web-vitals lib | RUM endpoint |
| Database queries | Vitest + pg | EXPLAIN ANALYZE |
| Memory leaks | Node inspector | heap snapshots |
| Network conditions | Playwright CDP | emulateNetworkConditions |
Threshold Guidelines
| Metric | Target | Maximum |
|---|---|---|
| API p95 latency | < 200ms | < 500ms |
| API p99 latency | < 500ms | < 1500ms |
| Error rate | < 0.1% | < 1% |
| LCP | < 2.5s | < 4.0s |
| INP | < 200ms | < 500ms |
| CLS | < 0.1 | < 0.25 |
| JS bundle (total) | < 200KB | < 300KB |
| CSS (total) | < 50KB | < 100KB |
| DB query | < 50ms | < 200ms |
Best Practices
- Test in production-like environments -- staging with similar data volumes and infra
- Establish baselines before optimizing -- measure first, then improve
- Set realistic thresholds -- based on SLAs and user expectations, not arbitrary numbers
- Automate in CI/CD -- performance regressions should block merges
- Monitor real users (RUM) -- lab data is a proxy; field data is truth
- Test at scale -- seed databases with production-like data volumes
- Profile before optimizing -- find the bottleneck, do not guess
- Budget for third parties -- third-party scripts are the top performance killer
- Test degraded conditions -- slow networks, cold caches, high concurrency
- Track trends over time -- individual runs have variance; trends reveal regressions
Related Skills
qa-engineer- Overall test strategy and quality gatese2e-testing- Browser automation and E2E testsunit-testing- Unit tests and TDD workflow
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?