Profile Before Optimizing
A measurement-first optimization loop — profile under realistic load, fix exactly one hot spot, re-measure, and keep the numbers in the commit.
Use when: Use when something is slow or resource-hungry and the urge is to start rewriting code that merely looks expensive.
The skill file
Claude Code · SKILL.mdSKILL.md3164 chars
# Profile Before Optimizing
Optimizations without measurements are guesses, and most guesses about hot paths are wrong. No code change until a profile names the culprit; no merged change without a before/after number.
## Step 1 — Define the metric and budget
- Pick one number: p95 request latency, wall-clock of a batch job, peak RSS, queries per request. "Feels slow" is not a metric.
- Record the current value and the target ("p95 from 1.8s to <500ms"). The target tells you when to stop — open-ended optimization never ends.
## Step 2 — Measure under realistic conditions
- Profile with production-like data volume. Code that's fine at 100 rows is quadratic at 100k; a dev DB with 50 records hides every N+1.
- Use the right tool for the layer:
- CPU: a sampling profiler with flame graph output (`py-spy`, `node --cpu-prof` + speedscope, `perf`, pprof).
- Database: query logs with timings, `EXPLAIN ANALYZE` on the slow queries, a counter for queries-per-request.
- Memory: heap snapshots diffed across the operation; allocation profilers for churn.
- Frontend: browser performance panel, Web Vitals — measured on a throttled profile, not your dev machine.
- Run the measurement at least 3 times after a warm-up run; record median and spread. A single timing is noise.
## Step 3 — Read the profile, name one culprit
- Walk the flame graph top-down by inclusive time. Ignore anything under ~5% of total — it cannot give you a meaningful win even if reduced to zero.
- Distinguish on-CPU from waiting. If the process is idle while the wall clock runs, the fix is I/O-shaped (batching, caching, parallelism, indexes), not algorithmic.
- Classic culprits to check before exotic theories: N+1 queries, missing index (seq scan in EXPLAIN), serialization of large payloads, synchronous calls in a loop that could be batched, accidental O(n²) via `includes`/`in` inside a loop, re-reading or re-parsing the same file/config per item.
- Write the diagnosis down: "62% of wall time is 1,400 sequential SELECTs from rendering badges per row."
## Step 4 — Fix one thing
- Apply the single change that addresses the named culprit. Prefer, in order: do less work (cache, skip, precompute) → do it in bulk (batch query, vectorize) → do it concurrently → micro-optimize. Micro-optimizations come last because they cost readability for single-digit percents.
- Do not bundle a second "while I'm in here" optimization. One change per measurement, or you can't attribute the result.
## Step 5 — Re-measure and decide
- Re-run the exact Step 2 measurement. Compare medians.
- Improvement below the noise floor → revert. Keep no complexity that isn't paid for by a measured win.
- Target met → stop, even if you can see more opportunities. File them instead.
- Target not met → back to Step 3 with a fresh profile; the hot path has moved.
## Commit etiquette
- The commit/PR body includes: metric, before, after, measurement method, and a link to or summary of the profile. "perf: batch badge query (p95 1.8s → 410ms, 1400 queries → 3)".
- Add a regression guard where feasible: a query-count assertion in a test, or a benchmark in CI for the critical path.
Install
drop into your repoSave this to your project or home directory so Claude Code can load it.
path~/.claude/skills/performance-profiling-before-optimizing/SKILL.md
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…