$npx -y skills add UnpaidAttention/fable5-methodology --skill performance-optimizationMake code measurably faster or leaner without breaking it — define a target, measure a baseline, profile to find the ACTUAL bottleneck, apply optimizations in a fixed order of leverage, and re-verify correctness after each change. Trigger this when the user reports slowness, high
| 1 | # Performance Optimization |
| 2 | |
| 3 | Optimization without measurement is guessing with extra steps. Intuition about bottlenecks is |
| 4 | wrong often enough that acting on it unmeasured wastes the time it was meant to save. The |
| 5 | number rules everything: define it, baseline it, move it, prove it. |
| 6 | |
| 7 | ## Step 1: Define the target and the workload |
| 8 | |
| 9 | 1. Pin the metric AND the goal: "p95 of `/search` under 300 ms at 50 rps", "import of the |
| 10 | 1M-row file under 2 min", "RSS under 512 MB". No target = no definition of done = optimization |
| 11 | that never ends. If the user gave none, propose one and get a nod. |
| 12 | 2. Pin the workload: realistic data at realistic scale. A query fast on 100 dev rows and slow |
| 13 | on 10M production rows is the NORMAL case, not an edge case — measuring on toy data |
| 14 | measures nothing. |
| 15 | |
| 16 | ## Step 2: Baseline before any change |
| 17 | |
| 18 | Measure the current number, three runs (warm), record median and spread in your notes. |
| 19 | Every later claim of improvement is relative to this recorded baseline — "feels faster" is |
| 20 | not a result. |
| 21 | |
| 22 | ## Step 3: Profile — find where the time actually goes |
| 23 | |
| 24 | Use the stack's profiler, not code reading: |
| 25 | - Rust: `cargo flamegraph` / `perf`; `--release` always — debug-build numbers are fiction. |
| 26 | - Node/TS: `node --prof` or `0x`/`clinic flame`; `console.time` brackets for coarse cuts. |
| 27 | - Python: `py-spy record` (no code change) or `cProfile` + `snakeviz`. |
| 28 | - SQL: `EXPLAIN (ANALYZE, BUFFERS)` on the real query with real data volume. |
| 29 | - Anything network-y: count the requests first — chattiness dominates before CPU does. |
| 30 | |
| 31 | Read the profile for the top consumer. **Rule: no optimization is applied to code the profile |
| 32 | didn't indict.** If the profile surprises you (it usually does), that surprise just saved you |
| 33 | a week of optimizing the wrong thing. |
| 34 | |
| 35 | ## Step 4: Apply strategies in order of leverage |
| 36 | |
| 37 | Try each level; only descend when the level above is exhausted for the indicted hotspot: |
| 38 | |
| 39 | 1. **Don't do the work:** cache repeated computation, dedupe repeated calls, early-exit, |
| 40 | skip unused fields, paginate instead of load-all. |
| 41 | 2. **Do the work fewer times / in bulk:** fix N+1 queries (join or batch), batch writes, |
| 42 | buffer I/O, debounce, move loop-invariant work out of the loop. |
| 43 | 3. **Do the work better:** right algorithm/data structure — dict lookup over list scan, |
| 44 | sort-then-merge over nested loops, the missing database index. |
| 45 | 4. **Tune the machinery:** connection pools, buffer sizes, compiler/runtime flags, |
| 46 | `--release`. |
| 47 | 5. **Parallelize:** only now — parallel versions of wasteful work multiply the waste and add |
| 48 | race conditions to it. |
| 49 | 6. **Micro-optimize:** last, rarely, and only with the profile still pointing there. |
| 50 | |
| 51 | ## Step 5: One change, two measurements |
| 52 | |
| 53 | For EACH optimization: apply it alone → re-run the metric → re-run the correctness suite. |
| 54 | Keep a running table in your notes: |
| 55 | |
| 56 | | Change | p95 before | p95 after | Tests | |
| 57 | |---|---|---|---| |
| 58 | | Add index on `orders(user_id, created_at)` | 1400 ms | 310 ms | green | |
| 59 | | Batch the price lookups | 310 ms | 180 ms | green | |
| 60 | |
| 61 | A change that doesn't move the number gets REVERTED, even if it "should" help — dead |
| 62 | optimizations are complexity with no dividend. A change that breaks a test is a bug, not a |
| 63 | trade-off, unless the user explicitly accepts the trade. |
| 64 | |
| 65 | ## Step 6: Stop at the target |
| 66 | |
| 67 | When the target from Step 1 is met: stop. Further optimization is scope creep with a |
| 68 | performance costume. Report the table, the final number vs. target, and any identified-but- |
| 69 | unneeded next levers ("materialized view would take it to ~80 ms if ever needed"). |
| 70 | |
| 71 | ## Worked example |
| 72 | |
| 73 | "The dashboard takes 8 s to load; get it under 2 s." |
| 74 | |
| 75 | - Target: p95 page-data endpoint < 2 s on the staging dataset (4M rows). Baseline: 8.1 s. |
| 76 | - Profile (EXPLAIN + request log): intuition said "slow rendering"; reality: 41 sequential |
| 77 | queries (N+1 on widgets) + one seq-scan aggregate. Rendering was 90 ms — untouched. |
| 78 | - Level 2: batch widget query → 41 calls → 2. 8.1 s → 2.9 s. Suite green. |
| 79 | - Level 3: index for the aggregate's filter → seq scan → index scan. 2.9 s → 1.3 s. Green. |
| 80 | - Target met — STOP. Cache layer (level 1) noted as unneeded next lever, not built. |
| 81 | - Report: table of both changes with numbers, final 1.3 s vs 2 s target, suite green. |
| 82 | |
| 83 | ## Done when |
| 84 | |
| 85 | The target metric from Step 1 is met and demonstrated by recorded before/after measurements |
| 86 | on the realistic workload; every ap |