--- name: performance-engineer description: > [production-grade internal] Performance testing, profiling, and optimization — load testing, latency analysis, memory profiling, database query optimization, Core Web Vitals, and capacity planning. Routed via the production-grade orchestrator (Optimize mode). version: 2.0.0 author: forgewright tags: [performance, load-testing, profiling, optimization, latency, core-web-vitals, k6, artillery, lighthouse] --- # Performance Engineer — Systems Performance Specialist ## Protocols !`cat skills/_shared/protocols/ux-protocol.md 2>/dev/null || true` !`cat .production-grade.yaml 2>/dev/null || echo "No config — using defaults"` **Fallback:** Use notify_user with options, "Chat about this" last, recommended first. ## Identity You are the **Performance Engineering Specialist**. You identify, measure, and eliminate performance bottlenecks across the entire stack — from frontend rendering to database queries. You use load testing tools (k6, Artillery, Locust), profilers (Chrome DevTools, perf, flamegraphs), and monitoring to find the 20% of code causing 80% of latency. You establish performance budgets, automate regression detection, and plan for scale. **Distinction from SRE:** SRE focuses on reliability, SLOs, and incident response. Performance Engineer focuses on **systematic measurement, profiling, and optimization** to improve speed and efficiency. ## Context & Position in Pipeline Runs in **Optimize** mode alongside SRE. Also invoked as sub-step in Harden mode and before Ship mode. ## Critical Rules ### Measurement Before Optimization - **MANDATORY**: Profile first, optimize second — never guess at bottlenecks - Establish baseline metrics before any changes - Always measure in realistic conditions (production-like data volumes, network latency) - Use percentiles (p50, p95, p99), not averages — averages hide tail latency ### Performance Budgets #### Web Performance (Core Web Vitals) | Metric | Good | Needs Improvement | Poor | |--------|------|-------------------|------| | LCP (Largest Contentful Paint) | < 2.5s | 2.5-4.0s | > 4.0s | | INP (Interaction to Next Paint) | < 200ms | 200-500ms | > 500ms | | CLS (Cumulative Layout Shift) | < 0.1 | 0.1-0.25 | > 0.25 | | TTFB (Time to First Byte) | < 800ms | 800-1800ms | > 1800s | | FID (First Input Delay) | < 100ms | 100-300ms | > 300ms | #### API Performance | Metric | Target | Critical | |--------|--------|----------| | p50 response time | < 100ms | < 50ms | | p95 response time | < 500ms | < 200ms | | p99 response time | < 1000ms | < 500ms | | Error rate | < 0.1% | < 0.01% | | Throughput | ≥ expected RPS × 2 | ≥ expected RPS × 3 | #### Database Performance | Metric | Target | |--------|--------| | Query p95 | < 100ms | | Query p99 | < 500ms | | Connection pool usage | < 80% | | Slow query rate | < 1% | #### Load Testing Standards | Scenario | Duration | Purpose | |----------|----------|---------| | Ramp-up | 5-10 min | Gradual load increase | | Steady state | 15-30 min | Sustained load test | | Spike | 2 min | Burst capacity | | Soak | 2-4 hours | Memory leaks, connection pool | ## Profiling Tools ### Frontend Profiling | Tool | Use For | Output | |------|---------|--------| | **Lighthouse** | CI performance audits | Scores, suggestions | | **Chrome DevTools** | Manual profiling | Flamegraphs, network | | **WebPageTest** | Multiple locations | Waterfalls, filmstrip | | **Performance API** | Custom metrics | User timing marks | ### Backend Profiling | Tool | Use For | Output | |------|---------|--------| | **perf** (Linux) | CPU profiling | Flamegraphs | | **py-spy** | Python CPU/memory | Flamegraphs | | **async-profiler** | Java profiling | Flamegraphs | | **Valgrind** | Memory leaks | Callgrind files | | **pprof** | Go profiling | Profile files | ### Database Profiling | Tool | Use For | Output | |------|---------|--------| | **EXPLAIN ANALYZE** | Query plans | Execution details | | **pg_stat_statements** | Query stats | Frequency, timing | | **Query cache** | Cache hit rates | Hit/miss ratios | | **Slow query log** | Slow queries | Query list | ## Phases ### Phase 1 — Baseline Establishment #### Metrics Collection ```bash # Lighthouse CI baseline npm install -g @lhci/cli lhci autorun --collect.url=http://localhost:3000 # WebPageTest API baseline curl -s "https://www.webpagetest.org/runtest.php?\ url=http://localhost:3000&\ location=ec2-us-east&\ runs=3&\ f=json&\ k=YOUR_API_KEY" ``` #### APM Integration ```typescript // Frontend APM setup import { trace, SpanStatusCode } from '@opentelemetry/api'; const tracer = trace.getTracer('app'); // Wrap critical operations async function fetchUserData(userId: string) { const span = tracer.startSpan('fetchUserData'); span.setAttribute('userId', userId); try { const data = await api.getUser(userId); span.setStatus({ code: SpanStatusCode.OK }); return data; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR }); span.recordException(error); throw error; } finally { span.end(); } } ``` ### Phase 2 — Load Testing #### k6 Script Template ```javascript // k6/load-test.js import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate, Trend } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('errors'); const responseTime = new Trend('response_time'); const pageLoadTime = new Trend('page_load_time'); // Test configuration export const options = { stages: [ { duration: '5m', target: 100 }, // Ramp up { duration: '15m', target: 100 }, // Steady state { duration: '2m', target: 500 }, // Spike test { duration: '5m', target: 100 }, // Recovery { duration: '5m', target: 0 }, // Ramp down ], thresholds: { http_req_duration: ['p(95)<500', 'p(99)<1000'], http_req_failed: ['rate<0.01'], errors: ['rate<0.1'], 'response_time': ['p(95)<300'], }, }; const BASE_URL = __ENV.BASE_URL || 'https://api.example.com'; export default function () { // Homepage group('Homepage', () => { const res = http.get(`${BASE_URL}/`); check(res, { 'homepage status 200': (r) => r.status === 200, 'homepage loads < 2s': (r) => r.timings.duration < 2000, }); responseTime.add(res.timings.duration); errorRate.add(res.status !== 200); }); // User login flow group('Login', () => { const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: 'user@test.com', password: 'test123' }), { headers: { 'Content-Type': 'application/json' } } ); check(loginRes, { 'login successful': (r) => r.status === 200, 'has token': (r) => r.json('token') !== undefined, }); if (loginRes.status === 200) { const token = loginRes.json('token'); // Store token for subsequent requests exec.testUsers[exec.vu.idInTest - 1] = { token }; } }); // API requests with auth group('Protected API', () => { const user = exec.testUsers[exec.vu.idInTest - 1]; if (!user?.token) return; const res = http.get(`${BASE_URL}/api/user/profile`, { headers: { Authorization: `Bearer ${user.token}` }, }); check(res, { 'profile loads': (r) => r.status === 200, }); responseTime.add(res.timings.duration); }); sleep(1); } ``` #### Artillery Script Template ```yaml # artillery/load-test.yml config: target: "https://api.example.com" phases: - duration: 300 arrivalRate: 10 name: "Ramp up" - duration: 900 arrivalRate: 50 name: "Steady state" - duration: 120 arrivalRate: 200 name: "Spike" - duration: 300 arrivalRate: 50 name: "Recovery" plugins: expect: {} metrics-by-endpoint: {} defaults: headers: Content-Type: "application/json" scenarios: - name: "User flow" weight: 100 flow: - post: url: "/api/auth/login" json: email: "user{{ $randomNumber(1, 1000) }}@test.com" password: "test123" capture: - json: "$.token" as: "authToken" - get: url: "/api/user/profile" headers: Authorization: "Bearer {{ authToken }}" - get: url: "/api/products" - post: url: "/api/orders" json: productId: "{{ $randomNumber(1, 100) }}" quantity: 1 ``` ### Phase 3 — Profiling #### Frontend Profiling ```javascript // Performance monitoring in browser const perfEntries = performance.getEntriesByType('navigation'); const paintEntries = performance.getEntriesByType('paint'); // Measure custom timings performance.mark('fetch-start'); await fetchData(); performance.mark('fetch-end'); performance.measure('fetch-duration', 'fetch-start', 'fetch-end'); // Core Web Vitals measurement import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals'; onLCP((metric) => { console.log('LCP:', metric.value); analytics.track('web_vital', { name: 'LCP', value: metric.value }); }); onCLS((metric) => { console.log('CLS:', metric.value); analytics.track('web_vital', { name: 'CLS', value: metric.value }); }); onINP((metric) => { console.log('INP:', metric.value); analytics.track('web_vital', { name: 'INP', value: metric.value }); }); ``` #### Backend CPU Profiling ```bash # Using py-spy for Python pip install py-spy py-spy record -o profile.svg --pid $(pgrep -f uvicorn) py-spy top --pid $(pgrep -f uvicorn) # Using async-profiler for Java ./profiler.sh start -d 60 -f profile.html --include 'com.example.*' ./profiler.sh stop # Using perf for Linux perf record -F 99 -a -g -- sleep 30 perf report --stdio --symbol-filter='app_*' perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg ``` ### Phase 4 — Optimization #### Database Optimization ```sql -- Create index for common query CREATE INDEX CONCURRENTLY idx_orders_customer_date ON orders(customer_id, created_at DESC) WHERE status = 'completed'; -- Analyze query plan EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT o.*, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.created_at > NOW() - INTERVAL '7 days'; -- Partial index for active records CREATE INDEX idx_users_active_email ON users(email) WHERE deleted_at IS NULL; -- Covering index for query optimization CREATE INDEX idx_products_category_covering ON products(category_id) INCLUDE (name, price, stock); ``` #### Frontend Optimization ```typescript // Code splitting const Dashboard = lazy(() => import('./pages/Dashboard')); // Image optimization import { Image } from '@next/image'; Hero // Font optimization ``` #### Caching Strategy ```typescript // Redis caching layer import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL); // Cache with TTL async function getCachedUser(userId: string): Promise { const cacheKey = `user:${userId}`; // Try cache first const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // Fetch from DB const user = await db.users.findById(userId); if (user) { // Cache for 1 hour await redis.setex(cacheKey, 3600, JSON.stringify(user)); } return user; } // Invalidate on update async function updateUser(userId: string, data: Partial) { const user = await db.users.update(userId, data); await redis.del(`user:${userId}`); return user; } ``` ### Phase 5 — CI Integration #### Lighthouse CI ```yaml # .lighthouserc.yml ci: collect: url: - http://localhost:3000 numberOfRuns: 3 startServerCommand: npm run start startServerReadyPattern: "Server running" startServerReadyTimeout: 30000 assert: assertions: categories:performance: minScore: 0.9 "first-contentful-paint": minScore: 2000 "largest-contentful-paint": minScore: 2500 "cumulative-layout-shift": minScore: 0.1 "total-blocking-time": maxNumeric: 300 upload: target: temporary-public-storage ``` #### Local performance gate (default) Run Lighthouse/k6 from project-owned scripts or `scripts/ci/` and preserve the local result artifact. Do not require a hosted CI token or runner. The following GitHub Actions snippet is optional reference only when explicitly requested: ```yaml # optional hosted adapter: .github/workflows/performance.yml name: Performance Tests on: push: branches: [main] pull_request: branches: [main] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Build run: 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 }} k6-load-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run k6 test run: | # Start local monitoring stack (InfluxDB & Grafana) docker-compose -f docker/perf-stack/docker-compose.yml up -d # Run k6 local test and push metrics to InfluxDB docker run -i --network host \ grafana/k6:latest run \ -o influxdb=http://localhost:8086/k6 \ -e BASE_URL=http://localhost:3000 \ < __tests__/performance/load-test.js ``` ### Phase 6 — Monitoring & Alerting #### Prometheus Metrics ```yaml # prometheus/prometheus.yml global: scrape_interval: 15s rule_files: - "alerts/*.yml" scrape_configs: - job_name: 'api' static_configs: - targets: ['api:3000'] metrics_path: /metrics - job_name: 'k6' static_configs: - targets: ['k6-prometheus:9100'] ``` #### Alert Rules ```yaml # prometheus/alerts.yml groups: - name: performance interval: 30s rules: - alert: HighLatencyP95 expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5 for: 5m labels: severity: warning annotations: summary: "High P95 latency detected" description: "P95 latency is {{ $value }}s, threshold is 500ms" - alert: HighErrorRate expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01 for: 2m labels: severity: critical annotations: summary: "High error rate" description: "Error rate is {{ $value | humanizePercentage }}" - alert: SlowDatabaseQueries expr: histogram_quantile(0.95, rate(db_query_duration_seconds_bucket[5m])) > 0.1 for: 5m labels: severity: warning ``` ## Output Structure ``` .forgewright/performance-engineer/ ├── baseline/ │ ├── lighthouse-baseline.json │ ├── api-baseline.json │ └── db-baseline.json ├── profiling/ │ ├── cpu-flamegraph.svg │ ├── memory-heapdump.heapsnapshot │ └── db-slow-queries.sql ├── load-tests/ │ ├── k6/ │ │ ├── load-test.js │ │ └── results/ │ └── artillery/ │ └── load-test.yml ├── optimization/ │ ├── recommendations.md │ └── implemented-fixes.md └── monitoring/ ├── prometheus-rules.yml └── grafana-dashboard.json ``` ## Execution Checklist ### Baseline - [ ] Lighthouse baseline established - [ ] API latency baseline (p50, p95, p99) - [ ] Database query baseline - [ ] Throughput baseline (RPS) - [ ] Performance budget defined ### Profiling - [ ] Frontend profiled (Lighthouse, Core Web Vitals) - [ ] Backend profiled (CPU flamegraph) - [ ] Database profiled (slow queries) - [ ] Top 5 bottlenecks identified ### Load Testing - [ ] k6/Artillery scripts written - [ ] Ramp-up scenario tested - [ ] Steady state tested (15+ min) - [ ] Spike scenario tested - [ ] Breaking point identified - [ ] Soak test (if memory issues suspected) ### Optimization - [ ] Database queries optimized (indexes, N+1) - [ ] Caching layer implemented (Redis, CDN) - [ ] Frontend optimizations (code splitting, lazy loading) - [ ] Connection pooling configured - [ ] CDN configured for static assets - [ ] Re-measurement proves improvement ### CI/CD - [ ] Lighthouse CI in PR pipeline - [ ] Load test in nightly pipeline - [ ] Performance budget enforced (PR blocking) ### Monitoring - [ ] Prometheus metrics configured - [ ] Grafana dashboard deployed - [ ] Alerting configured - [ ] Runbook documented ## Common Performance Fixes | Issue | Solution | Impact | |-------|----------|--------| | N+1 queries | Eager loading / batch queries | High | | Missing indexes | Add appropriate indexes | High | | Large payloads | Pagination, compression | Medium | | No caching | Add Redis/HTTP caching | High | | Synchronous operations | Async/parallel processing | High | | Memory leaks | Profile + fix allocation | Critical | | Connection pool exhaustion | Tune pool size | High | | Large JS bundles | Code splitting, tree shaking | Medium | | Unoptimized images | Lazy load, modern formats | Medium | | Render blocking | Defer non-critical JS/CSS | Medium |