| name | performance-audit |
| description | Systematic performance investigation workflow. Use when performance regressions are suspected, before and after optimisation work, or as a pre-release gate for performance-critical features. Produces a baseline, identifies the real bottleneck, and validates improvement with hard numbers. |
Performance Audit
The Law
A PERFORMANCE CLAIM WITHOUT NUMBERS IS AN OPINION.
"I think it's faster" is not a result.
"p95 latency dropped from 480ms to 42ms on a 1000-item dataset" is a result.
When to Use
- Before any optimisation work (establish baseline first)
- When a performance regression is reported
- As a pre-release gate on performance-critical features
- After an optimisation to validate the improvement
Audit Workflow
Step 1: Define the Performance Goal
Before measuring anything, define what "good" looks like:
| Category | Example target |
|---|
| API latency | p95 < 200ms at 100 concurrent users |
| Page load | First Contentful Paint < 1.5s on 4G |
| Mobile frame rate | 60fps during scroll, 0 jank frames |
| Build time | Full build < 3 minutes |
| Memory | Peak heap < 512MB under load |
| Bundle size | JS bundle < 250KB gzipped |
If no target exists, the first audit establishes the baseline that becomes the target.
Step 2: Measure the Baseline
Run the benchmark before any changes. Record:
- Tool used and exact command
- Input size and conditions
- Raw numbers (min, p50, p95, p99, max)
- System state (CPU, memory, concurrent users)
## Baseline — YYYY-MM-DD
**Feature:** User search endpoint
**Tool:** k6
**Command:** `k6 run --vus 50 --duration 60s scripts/search-load-test.js`
**Input:** 1000-user dataset, query="test"
| Metric | Value |
|--------|-------|
| p50 | 45ms |
| p95 | 380ms |
| p99 | 720ms |
| Error rate | 0.2% |
| Throughput | 210 req/s |
Step 3: Profile — Find the Real Bottleneck
Use the appropriate tool for the stack. Read the output. Don't guess.
Web APIs
node --prof app.js
node --prof-process isolate-*.log > profile.txt
pip install py-spy
py-spy record -o profile.svg --pid $(pgrep -f "python app.py")
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT ...;
Frontend
npx webpack-bundle-analyzer stats.json
npx source-map-explorer dist/main.js
npx lhci autorun
Mobile
- Android: Android Studio → Profiler → CPU/Memory/Network
- iOS: Xcode → Instruments → Time Profiler
- Flutter:
flutter run --profile → DevTools → Performance overlay
Step 4: Report the Finding
State the bottleneck with evidence, not assumption:
## Bottleneck Found
**Location:** `UserSearchService.search()` — lines 45-67
**Evidence:** pprof shows 78% of CPU time in this function
**Root cause:** For each of 50 search results, a separate `getUserById()` DB call is made (N+1 query)
**Impact:** 47 sequential DB calls per request × ~8ms each = ~376ms per request
Step 5: Apply One Change
Single change. Document hypothesis and expected impact before applying:
## Change Applied
**Hypothesis:** Replacing per-item DB lookups with a single JOIN will eliminate 46 of the 47 DB calls
**Change:** `UserSearchService.search()` — replace loop + `getUserById()` with `getUsersByIds(ids)` JOIN
**Files modified:** `src/services/UserSearchService.ts`
**Expected impact:** p95 latency from 380ms to < 50ms
Step 6: Re-Measure and Compare
Run the exact same benchmark from Step 2.
## After Optimisation — YYYY-MM-DD
**Change:** N+1 query replaced with single JOIN
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| p50 | 45ms | 12ms | -73% |
| p95 | 380ms | 38ms | -90% |
| p99 | 720ms | 65ms | -91% |
| Error rate | 0.2% | 0.1% | -50% |
| Throughput | 210 req/s | 890 req/s | +324% |
**Verdict:** IMPROVEMENT CONFIRMED
**Root cause validated:** DB calls per request: 47 → 1
**No regression:** error rate stable, memory unchanged
Step 7: Verdict
| Verdict | Criteria |
|---|
| IMPROVEMENT CONFIRMED | Measurable improvement on target metric, no regression elsewhere |
| INCONCLUSIVE | No measurable change — hypothesis was wrong; identify next bottleneck |
| REGRESSION | Change made things worse — revert immediately, re-diagnose |
| GOAL MET | Target from Step 1 achieved — further optimisation not required |
Performance Budget (Pre-Release Gate)
For release-critical features, define performance budgets in docs/performance-budgets.md:
api_latency:
endpoint: POST /api/search
p95_ms: 200
measured_with: k6
load: 100 concurrent users
bundle_size:
entry: main.js
max_gzip_kb: 250
measured_with: webpack-bundle-analyzer
mobile_fps:
screen: SearchScreen
min_fps: 60
tool: Android Profiler
CI fails if any budget is exceeded. Performance is a feature — it regresses like any other feature.