AI-consumed reference. Optimized for Claude to read during execution.
Human-readable explanation: see docs/architecture/HIERARCHICAL_PLANNING.md
or docs/getting-started/ depending on topic.
Performance Profiling
Rule 0: Don't optimize what you haven't measured. Almost all "obvious" optimizations are wrong.
The Protocol
Step 1 — Define "fast enough"
Performance is relative. Before optimizing:
- Current metric (p50, p95, p99 latency / MB memory / % CPU)
- Target metric (what's the SLO/SLA/user expectation?)
- Gap — what needs to close
If no target → ask the user. Don't optimize without a goal — you'll polish forever.
Step 2 — Measure baseline
Use the appropriate profiler:
| Language |
Profiler |
| Node.js |
node --prof, clinic.js, 0x |
| Browser JS |
Chrome DevTools Performance tab |
| Python |
cProfile, py-spy, memory_profiler |
| Go |
pprof (built-in) |
| Rust |
cargo flamegraph, perf |
| CLI |
hyperfine |
| HTTP |
wrk, k6, vegeta |
Run the profiler under realistic load, not trivial input. Capture for 30–60s minimum — short captures miss long-tail events.
Step 3 — Analyze (flamegraph / top consumers)
Identify the top 3 functions by:
- Self time (function's own CPU, excluding descendants)
- Total time (self + descendants)
- Call count (high-frequency cold functions add up)
Pareto check: Is there a clear 80/20?
- Top function takes 50%+ of time → that's your target
- Time spread evenly across 100 functions → hard to optimize; likely needs architectural change
Step 4 — Form optimization hypothesis
For the bottleneck:
- What's it doing? (read the code)
- Why is it slow? (algorithm, I/O, allocations, lock contention, cache miss)
- What's a realistic improvement target?
Common bottleneck classes + fixes:
| Bottleneck |
Fix |
| O(n²) on large n |
Change to O(n log n) or O(n) |
| Sync I/O in hot path |
Async, batch, or remove the I/O |
| Allocations in loop |
Pre-allocate, object pool, reuse |
| Redundant computation |
Memoize, cache result |
| Lock contention |
Lock-free structure, sharding, immutable data |
| Large object serialization |
Streaming, lazy fields, columnar format |
| Database N+1 |
Join, prefetch, dataloader pattern |
| Cold cache / page fault |
Warm up, prefetch, locality |
Step 5 — Implement one change, re-measure
One change at a time. Multiple simultaneous changes = unknown which one helped.
Measure the same workload after the change. Compute speedup. If < 10% improvement on the targeted bottleneck: wrong hypothesis — revert, try next.
Step 6 — Verify no regression elsewhere
Optimization can break correctness OR slow other paths. Run:
- Full test suite (correctness)
- Broader benchmark (other metrics didn't degrade — memory, throughput, p99)
Step 7 — Document
In PR or commit message:
- Baseline metric
- Change made
- New metric
- Speedup factor
- Why this change worked (root cause of slowness, not "just faster")
Anti-Patterns
- Micro-optimization without measurement — replacing
map with a for loop "for speed" when the bottleneck is elsewhere
- Optimizing cold code — code that runs rarely. Total impact = 0 even if 10× faster per call
- Premature async — async has overhead. Sync code often wins for CPU-bound small tasks
- Cache everything — caches add memory, invalidation bugs, staleness. Cache when measured benefit > cost
- One benchmark run — variance is huge. Run 10+ times, take p50 + std dev
- Microbenchmarking in isolation — function is fast in a benchmark, slow in production due to cache/JIT/load differences
When Architecture Change Is Needed
If profiling shows a flat distribution (no single bottleneck), OR if the target is 10× faster: individual optimizations won't close the gap. Options:
- Algorithm change — different approach entirely (not a tweak)
- Move work to a different tier — client → server, server → precomputed, online → offline
- Different data structure — array → tree, hash → trie, sync → event stream
- Different runtime — interpreted → compiled, single-threaded → parallel, CPU → GPU
These are architectural calls. Use self-consistency for trade-off analysis, tree-of-thoughts for exploring design branches.
Optimization Playbook by Layer
Once profiling (above) has named the bottleneck, apply the fix for its layer. Still one change at a time, re-measure each. Don't apply these blind — they're the fix menu, not a checklist to run through.
Profiler → metric map
| Layer |
Tools |
Metrics to watch |
| Frontend |
Lighthouse, DevTools Performance |
FCP, LCP, TTI, CLS |
| Backend |
APM, language profilers |
Response time, throughput |
| Database |
EXPLAIN ANALYZE, slow query log |
Query time, index usage |
| Memory |
Heap snapshots |
Allocation rate, leaks |
Frontend
Core Web Vitals targets: LCP < 2.5s · FID/INP < 100ms · CLS < 0.1.
Quick wins once the profile points here:
loading="lazy" on below-the-fold images; serve WebP/AVIF.
- Route/component code splitting:
lazy(() => import(...)) so first paint ships less JS.
debounce/throttle high-frequency handlers (scroll, resize, input).
useMemo/memoization for expensive re-computations; avoid re-render storms.
- Cache headers on static assets;
preload critical fonts.
Backend
| Issue |
Fix |
| N+1 queries |
Eager loading, batching, dataloader |
| Missing indexes |
Add appropriate index (see Database) |
| Unbounded queries |
Pagination, LIMIT |
| Sync blocking in hot path |
Async / parallel processing |
| No caching of hot data |
Cache with a measured TTL (see Caching) |
Database
EXPLAIN ANALYZE the slow query. Seq Scan on a large table = bad; Index Scan = good. Match the index type to the access pattern:
| Query pattern |
Index type |
| Exact match |
B-tree |
| Range |
B-tree |
| Full-text |
GIN / GiST |
| JSON containment |
GIN |
Caching (any layer)
| Level |
Typical TTL |
Use case |
| Browser |
Hours–Days |
Static assets |
| CDN |
Minutes–Hours |
API responses |
| Application |
Seconds–Minutes |
Computed / hot data |
Invalidation strategy: time-based, event-based, or version-based. Remember the anti-pattern above — cache only when measured benefit > memory + invalidation cost.
Memory leaks
| Leak cause |
Fix |
| Uncleaned event listeners |
Clean up in useEffect return / destroy |
| Retaining closures |
Null out references when done |
| Ever-growing collections |
WeakMap, or clear periodically |
| Dangling timers |
clearInterval / clearTimeout |
Output Format
## Performance Profile: [task / endpoint / function]
**Baseline:** p50=Xms, p95=Yms, p99=Zms, memory=N MB
**Target:** p95 < Wms (source: [SLO / user request])
**Gap:** Yms − Wms = Kms to close
**Bottleneck:** [function/query/operation] — X% of total time
**Root Cause:** [why it's slow]
**Optimization Applied:** [single change]
**After:** p50=X'ms, p95=Y'ms, p99=Z'ms
**Speedup:** [factor]
**Correctness:** [tests pass]
**Broader Impact:** [other metrics checked, no regressions]
Tie-Ins
skills/self-consistency/SKILL.md — for architectural decisions
skills/tree-of-thoughts/SKILL.md — explore optimization branches
rules/core/verification.md — measure before + after, not just after
commands/check.md — /check perf uses this skill
rules/core/simplicity-over-complexity.md — the winning optimization is usually "do less," not "do the same thing with a clever structure"
1---2name: perf-profiling3description: Systematic performance profiling and optimization across frontend (Core Web Vitals, code splitting, lazy loading), backend (N+1 queries, async), and database (EXPLAIN ANALYZE, indexing) layers. Use when the user reports slow code, latency, memory leaks, needs to benchmark, or wants to speed up an application. Measure first, optimize second. Applies Pareto principle — find the 20% of code causing 80% of slowness, fix that, not the rest.4---56> **AI-consumed reference.** Optimized for Claude to read during execution.7> Human-readable explanation: see [docs/architecture/HIERARCHICAL_PLANNING.md](../../../docs/architecture/HIERARCHICAL_PLANNING.md)8> or [docs/getting-started/](../../../docs/getting-started/) depending on topic.91011# Performance Profiling1213**Rule 0:** Don't optimize what you haven't measured. Almost all "obvious" optimizations are wrong.1415---1617## The Protocol1819### Step 1 — Define "fast enough"2021Performance is relative. Before optimizing:2223- **Current metric** (p50, p95, p99 latency / MB memory / % CPU)24- **Target metric** (what's the SLO/SLA/user expectation?)25- **Gap** — what needs to close2627If no target → **ask the user**. Don't optimize without a goal — you'll polish forever.2829### Step 2 — Measure baseline3031Use the appropriate profiler:3233| Language | Profiler |34|----------|----------|35| Node.js | `node --prof`, `clinic.js`, `0x` |36| Browser JS | Chrome DevTools Performance tab |37| Python | `cProfile`, `py-spy`, `memory_profiler` |38| Go | `pprof` (built-in) |39| Rust | `cargo flamegraph`, `perf` |40| CLI | `hyperfine` |41| HTTP | `wrk`, `k6`, `vegeta` |4243Run the profiler under **realistic load**, not trivial input. Capture for 30–60s minimum — short captures miss long-tail events.4445### Step 3 — Analyze (flamegraph / top consumers)4647Identify the top 3 functions by:48- **Self time** (function's own CPU, excluding descendants)49- **Total time** (self + descendants)50- **Call count** (high-frequency cold functions add up)5152**Pareto check:** Is there a clear 80/20?53- Top function takes 50%+ of time → that's your target54- Time spread evenly across 100 functions → hard to optimize; likely needs architectural change5556### Step 4 — Form optimization hypothesis5758For the bottleneck:59- What's it doing? (read the code)60- Why is it slow? (algorithm, I/O, allocations, lock contention, cache miss)61- What's a realistic improvement target?6263Common bottleneck classes + fixes:6465| Bottleneck | Fix |66|-----------|-----|67| O(n²) on large n | Change to O(n log n) or O(n) |68| Sync I/O in hot path | Async, batch, or remove the I/O |69| Allocations in loop | Pre-allocate, object pool, reuse |70| Redundant computation | Memoize, cache result |71| Lock contention | Lock-free structure, sharding, immutable data |72| Large object serialization | Streaming, lazy fields, columnar format |73| Database N+1 | Join, prefetch, dataloader pattern |74| Cold cache / page fault | Warm up, prefetch, locality |7576### Step 5 — Implement one change, re-measure7778**One change at a time.** Multiple simultaneous changes = unknown which one helped.7980Measure the same workload after the change. Compute speedup. If < 10% improvement on the targeted bottleneck: wrong hypothesis — revert, try next.8182### Step 6 — Verify no regression elsewhere8384Optimization can break correctness OR slow other paths. Run:85- Full test suite (correctness)86- Broader benchmark (other metrics didn't degrade — memory, throughput, p99)8788### Step 7 — Document8990In PR or commit message:91- Baseline metric92- Change made93- New metric94- Speedup factor95- **Why** this change worked (root cause of slowness, not "just faster")9697---9899## Anti-Patterns100101- **Micro-optimization without measurement** — replacing `map` with a `for` loop "for speed" when the bottleneck is elsewhere102- **Optimizing cold code** — code that runs rarely. Total impact = 0 even if 10× faster per call103- **Premature async** — async has overhead. Sync code often wins for CPU-bound small tasks104- **Cache everything** — caches add memory, invalidation bugs, staleness. Cache when measured benefit > cost105- **One benchmark run** — variance is huge. Run 10+ times, take p50 + std dev106- **Microbenchmarking in isolation** — function is fast in a benchmark, slow in production due to cache/JIT/load differences107108---109110## When Architecture Change Is Needed111112If profiling shows a flat distribution (no single bottleneck), OR if the target is 10× faster: individual optimizations won't close the gap. Options:113114- **Algorithm change** — different approach entirely (not a tweak)115- **Move work to a different tier** — client → server, server → precomputed, online → offline116- **Different data structure** — array → tree, hash → trie, sync → event stream117- **Different runtime** — interpreted → compiled, single-threaded → parallel, CPU → GPU118119These are architectural calls. Use `self-consistency` for trade-off analysis, `tree-of-thoughts` for exploring design branches.120121---122123## Optimization Playbook by Layer124125Once profiling (above) has named the bottleneck, apply the fix for *its* layer. Still one change at a time, re-measure each. Don't apply these blind — they're the fix menu, not a checklist to run through.126127### Profiler → metric map128129| Layer | Tools | Metrics to watch |130|-------|-------|------------------|131| Frontend | Lighthouse, DevTools Performance | FCP, LCP, TTI, CLS |132| Backend | APM, language profilers | Response time, throughput |133| Database | `EXPLAIN ANALYZE`, slow query log | Query time, index usage |134| Memory | Heap snapshots | Allocation rate, leaks |135136### Frontend137138**Core Web Vitals targets:** LCP < 2.5s · FID/INP < 100ms · CLS < 0.1.139140Quick wins once the profile points here:141- `loading="lazy"` on below-the-fold images; serve WebP/AVIF.142- Route/component code splitting: `lazy(() => import(...))` so first paint ships less JS.143- `debounce`/`throttle` high-frequency handlers (scroll, resize, input).144- `useMemo`/memoization for expensive re-computations; avoid re-render storms.145- Cache headers on static assets; `preload` critical fonts.146147### Backend148149| Issue | Fix |150|-------|-----|151| N+1 queries | Eager loading, batching, dataloader |152| Missing indexes | Add appropriate index (see Database) |153| Unbounded queries | Pagination, `LIMIT` |154| Sync blocking in hot path | Async / parallel processing |155| No caching of hot data | Cache with a measured TTL (see Caching) |156157### Database158159`EXPLAIN ANALYZE` the slow query. **Seq Scan on a large table = bad; Index Scan = good.** Match the index type to the access pattern:160161| Query pattern | Index type |162|---------------|------------|163| Exact match | B-tree |164| Range | B-tree |165| Full-text | GIN / GiST |166| JSON containment | GIN |167168### Caching (any layer)169170| Level | Typical TTL | Use case |171|-------|-------------|----------|172| Browser | Hours–Days | Static assets |173| CDN | Minutes–Hours | API responses |174| Application | Seconds–Minutes | Computed / hot data |175176Invalidation strategy: time-based, event-based, or version-based. Remember the anti-pattern above — cache only when measured benefit > memory + invalidation cost.177178### Memory leaks179180| Leak cause | Fix |181|------------|-----|182| Uncleaned event listeners | Clean up in `useEffect` return / `destroy` |183| Retaining closures | Null out references when done |184| Ever-growing collections | `WeakMap`, or clear periodically |185| Dangling timers | `clearInterval` / `clearTimeout` |186187---188189## Output Format190191```markdown192## Performance Profile: [task / endpoint / function]193194**Baseline:** p50=Xms, p95=Yms, p99=Zms, memory=N MB195**Target:** p95 < Wms (source: [SLO / user request])196**Gap:** Yms − Wms = Kms to close197198**Bottleneck:** [function/query/operation] — X% of total time199**Root Cause:** [why it's slow]200201**Optimization Applied:** [single change]202**After:** p50=X'ms, p95=Y'ms, p99=Z'ms203**Speedup:** [factor]204205**Correctness:** [tests pass]206**Broader Impact:** [other metrics checked, no regressions]207```208209---210211## Tie-Ins212213- `skills/self-consistency/SKILL.md` — for architectural decisions214- `skills/tree-of-thoughts/SKILL.md` — explore optimization branches215- `rules/core/verification.md` — measure before + after, not just after216- `commands/check.md` — `/check perf` uses this skill217- `rules/core/simplicity-over-complexity.md` — the winning optimization is usually "do less," not "do the same thing with a clever structure"