Performance Optimization
Most "slow" problems are diagnosed by guesswork. This skill replaces guessing with a
measure-first workflow: profile, fix minimally, re-measure against a budget. Covers backend
(database, caching, concurrency), frontend (bundles, images, rendering), network (CDN,
compression, HTTP), and memory, with concrete tools and before/after examples.
Quick Reference
| Topic |
Reference |
| Profiling tools per runtime and reading flame graphs |
profiling-tools.md |
| EXPLAIN, index design, N+1 fixes, caching, pooling |
backend-optimization.md |
| Bundles, images, React rendering, Core Web Vitals |
frontend-performance.md |
Core Workflow
1. Measure First
Never optimize before you can answer: "how do I know it got faster?"
- Reproduce — get a repeatable trigger (endpoint, page, script, load profile).
- Baseline — record current latency distribution, throughput, and resource usage.
- Budget — set an explicit target (e.g., p95 < 200 ms, initial JS < 170 KB gzipped).
- Profile — find where time actually goes (CPU, I/O wait, dependency latency, GC).
- Hypothesize — one bottleneck, one predicted fix, one measurable effect.
- Fix minimally — the smallest change that tests the hypothesis.
- Re-measure — same tool, load, and environment; keep the fix only if it moves the metric.
2. The Triage Loop (symptom → cause)
| Symptom |
Likely cause |
Quick check |
| High p95 but healthy p50 |
Queueing, GC pauses, one slow dependency |
Percentile breakdown; GC logs; sample slow requests |
| Slow only under load |
Contention, pool exhaustion, saturation |
Load test at increasing concurrency; watch pool waits |
| Slow in prod, fast locally |
Cold cache, missing index on prod-size data |
EXPLAIN on prod data; cache hit ratio |
| ORM makes dozens of queries |
N+1 lazy loading |
Log query counts; enable ORM query logging |
| High CPU |
CPU-bound loop, regex, serialization |
cProfile / py-spy dump |
| Memory grows monotonically |
Leak or unbounded cache |
Heap snapshots; cache size/capacity audit |
| Slow page load, fast API |
Render-blocking JS/CSS, oversized images |
Lighthouse waterfall; DevTools Network |
| High TTFB |
Slow backend or no CDN |
curl -w timing breakdown |
Choosing the Right Metrics
Track percentiles, not just the mean — a mean hides whether 5% of users wait 10x longer.
| Metric |
What it tells you |
Pitfall |
| Mean latency |
Average experience |
Skewed by outliers; hides the long tail |
| p50 latency |
Typical experience |
Misses everything bad |
| p95 / p99 latency |
Worst-case experience |
Noisy at low traffic volume |
| Throughput (RPS) |
Capacity |
Meaningless without a latency target |
| Error rate |
Stability under load |
Track alongside latency, never alone |
| Concurrency (in-flight) |
Queueing behavior |
High values mean requests are piling up |
Turn metrics into SLOs (e.g., "99% of GET /api/orders < 300 ms", "error rate < 0.1%") and
load test with k6 or locust before production does:
import http from "k6/http";
export const options = {
stages: [{ duration: "30s", target: 20 }, { duration: "1m", target: 100 }],
thresholds: { http_req_duration: ["p(95)<250"] },
};
export default function () { http.get("http://localhost:8000/api/orders/42"); }
Backend Optimization
Database
-- Red flags: Seq Scan on large tables, rows far higher than returned, missing indexes
EXPLAIN ANALYZE
SELECT o.*, i.* FROM orders o JOIN order_items i ON i.order_id = o.id
WHERE o.customer_id = 123 ORDER BY o.created_at DESC LIMIT 20;
| Technique |
What it fixes |
Cost |
| Index on filter columns |
Full table scans |
Slower writes; keep index count sane |
| Composite index (column order matters) |
Multi-column filters |
More storage |
| Covering index |
Avoids fetching table rows |
Storage |
Keyset pagination (WHERE id > ?) |
Slow OFFSET on deep pages |
Cursor in the API response |
| Query batching / eager loading |
N+1 round trips |
More complex ORM code |
| Denormalization (cached counts) |
Expensive aggregates on hot paths |
Invariants to maintain |
N+1 — one query for the parent, one per child — is the most common backend perf bug:
# BAD: 1 + N queries
for order in session.query(Order).filter_by(customer_id=123):
for item in order.items: # lazy load -> one query per order
...
# GOOD: 2 queries
from sqlalchemy.orm import selectinload
orders = session.query(Order).options(selectinload(Order.items)).filter_by(customer_id=123)
Caching
Cache-aside (read-through on miss, delete on write) is the default pattern:
def get_order(order_id: int):
cached = redis.get(f"order:{order_id}")
if cached is not None:
return json.loads(cached) # cache hit — no DB
order = db.query(Order).get(order_id) # miss — DB
redis.setex(f"order:{order_id}", 60, json.dumps(order.to_dict())) # TTL 60s
return order
Rules that prevent cache bugs:
- Set a TTL on everything — short TTLs make correctness bugs self-heal.
- Invalidate on write — delete the key in the same flow that mutates the row.
- Cap cache size (LRU /
maxmemory-policy allkeys-lru) — an unbounded cache is a leak.
- Guard against stampedes — single-flight lock so concurrent misses don't all hit the DB
(example in backend-optimization.md).
Concurrency & I/O
- Pool your connections (DB, HTTP, Redis) — establishing connections is expensive; size
pools to cores x expected concurrency (pgbouncer, explicit pool bounds).
- Batch external calls — fan out parallel requests instead of serial ones:
# BAD: 3 round trips, serial — latency adds up
a = api_a.get(x); b = api_b.get(x); c = api_c.get(x)
# GOOD: 3 round trips, parallel — latency of the slowest
import asyncio
a, b, c = await asyncio.gather(api_a.get(x), api_b.get(x), api_c.get(x))
- Async I/O beats threads for I/O-bound work; threads or multiprocessing beat async for
CPU-bound work (see Concurrency Models below).
Frontend Optimization
Bundle size
- Code splitting — load route/feature chunks on demand with
React.lazy + Suspense or
dynamic import().
- Tree shaking — import named exports, avoid side-effectful imports; verify with
webpack-bundle-analyzer / rollup-plugin-visualizer.
- Budget the initial JS (roughly 170 KB gzipped) and fail CI over it with
size-limit.
const OrdersPage = React.lazy(() => import("./pages/OrdersPage"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<OrdersPage />
</Suspense>
);
}
Images & asset delivery
- Serve modern formats (AVIF > WebP > JPEG) via CDN resizing or build-time conversion.
- Use
srcset/sizes so mobile users download small files; set width/height or
aspect-ratio to prevent layout shift (CLS).
loading="lazy" below the fold, fetchpriority="high" for the LCP image.
- Inline critical CSS; load JS with
defer (or async where order doesn't matter).
preconnect to CDNs/APIs hit early; preload the LCP image or font.
- Hashed assets:
Cache-Control: public, max-age=31536000, immutable; no-cache on HTML.
React rendering
React.memo to skip re-renders of expensive subtrees; useMemo/useCallback only where
identity/cost actually matters — don't wrap everything.
- Virtualize long lists (
react-window) instead of rendering 10k rows.
- Avoid layout thrash — batch DOM reads then writes; prefer CSS transforms over JS layout.
- Profile with React DevTools Profiler first — if renders are cheap, the problem is elsewhere.
Network Optimization
| Technique |
What it improves |
Notes |
| CDN |
TTFB, cacheable static content |
Edge-cache assets and even API responses |
| Compression (Brotli > gzip) |
Transfer size (often 60-80% smaller) |
Never compress already-encoded data (images) |
| HTTP/2 |
Multiplexing, one connection, header compression |
Requires HTTPS |
| HTTP/3 (QUIC) |
Connection setup, loss resilience |
Best on mobile/lossy networks; needs CDN/LB support |
| Keep-alive |
Reuse connections, skip per-request handshakes |
Default in most stacks; verify with curl -v |
| Prefetch / preconnect |
Perceived speed for the next page / first request |
dns-prefetch, preconnect, prefetch |
curl -sS -o /dev/null -w "DNS %{time_namelookup} | TCP %{time_connect} | TLS %{time_appconnect} | TTFB %{time_starttransfer} | Total %{time_total}\n" https://example.com/api/orders/42
Concurrency Models
| Model |
Best for |
Gotcha |
| Threads |
CPU-bound (non-GIL langs), blocking I/O |
Python: the GIL serializes CPU work |
| Async I/O (event loop) |
I/O-bound at high concurrency |
One blocking call stalls everything |
| Multiprocessing / workers |
CPU-bound in Python; isolation |
Memory per process; IPC cost |
| Worker threads |
Parallel CPU work in Node |
No shared memory; message passing |
Python note: the GIL means threads only help I/O-bound work — use multiprocessing for
CPU-bound. Node's event loop is the mirror image: offload sync CPU-heavy work from request
handlers to worker_threads or a job queue.
Memory
- Leaks — JS: retained listeners, detached DOM nodes, module-level caches, closures.
Python: unclosed files/connections, global caches, cyclic refs with
__del__. Prove with
heap snapshots (tracemalloc, Chrome Memory panel) taken at two points and diffed.
- Unbounded caches — every cache needs a size cap (LRU) or a TTL:
lru_cache(maxsize=...),
maxmemory-policy allkeys-lru.
- Object churn — allocating in hot loops forces GC pressure. Reuse buffers; prefer typed
arrays over per-iteration string concatenation.
Worked Example: Optimizing GET /api/orders/{id}
Baseline (k6, 50 concurrent users): p50 210 ms, p95 480 ms, error rate 0.4%.
Step 1 — profile. Attach py-spy; the dump shows ~380 ms spent in the DB driver:
Thread 0x10a (idle 3ms):
File "app/orders.py:42" _fetch_order
File "sqlalchemy/orm/query.py:310" execute
File "psycopg2/extensions.py:512" poll <- waiting on DB (~370ms)
Step 2 — hypothesis. The latency is DB time, not Python. Confirm with EXPLAIN:
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC;
-- Seq Scan on orders (cost=0.00..1420.00 rows=1 actual rows=3800) <- full scan
Step 3 — fix the missing customer_id index and the N+1 (3,800 queries/request); cache
hot orders for 60s:
CREATE INDEX idx_orders_customer ON orders (customer_id, created_at DESC);
order = db.query(Order).options(selectinload(Order.items)).get(order_id)
cached = redis.get(f"order:{order_id}")
if cached is not None:
return json.loads(cached)
redis.setex(f"order:{order_id}", 60, json.dumps(order.to_dict()))
Step 4 — re-measure with the identical k6 script:
| Metric |
Before |
After |
| p50 |
210 ms |
9 ms |
| p95 |
480 ms |
28 ms |
| Error rate |
0.4% |
0.0% |
| DB queries per request |
3,802 |
2 (or 0 on cache hit) |
The index removed the scan (380 ms → ~15 ms), eager loading removed 3,800 round trips, and
the cache removed the DB for repeated hits — each change measured in isolation.
Anti-Patterns to Avoid
- Optimizing before measuring — "fast" micro-optimizations that move nothing.
- Fixing symptoms, not bottlenecks — adding a cache without reading the plan; sometimes
the index alone is the whole fix.
- Mean-only dashboards — a healthy mean hides a terrible p99 and the long tail.
- Unbounded caches — no TTL, no size cap: today's cache is tomorrow's OOM.
- Premature concurrency — threads/async add complexity; apply only after profiling shows
I/O wait is the bottleneck.
- Blindly memoizing React —
useMemo/React.memo add overhead; measure render cost first.
When to Use / Not Use
Use when:
- The user reports slow endpoints, slow page loads, high latency, or poor throughput.
- You need to profile, load test, or establish performance budgets/SLOs.
- The task involves DB query optimization, caching, bundle/image optimization, or tuning
concurrency — and you can measure the effect.
Do NOT use when:
- The task is a pure correctness bug, feature, or refactor with no performance concern.
- The user only needs a one-off metric readout, not a systematic optimization pass.
- Performance already meets the budget — resist optimizing without a target.
1---2name: performance-optimization3description: Systematically find and fix performance problems in applications, frontend and backend, by measuring first. Use this skill whenever the user reports slow endpoints, high latency, poor throughput, memory leaks, large bundles, or slow page loads; wants to profile with cProfile, py-spy, Chrome DevTools, Node --cpu-prof, or perf; run load tests with locust or k6; set performance budgets or SLOs; optimize database queries (EXPLAIN, indexes, N+1); add caching (Redis); improve frontend performance (code splitting, lazy loading, image optimization, memoization, virtualization); or tune the network (CDNs, gzip/br, HTTP/2/3). Also trigger for p50/p95/p99 latency, throughput, error rate, connection pooling, async vs threads, GIL, and memory leak investigations.4---56# Performance Optimization78Most "slow" problems are diagnosed by guesswork. This skill replaces guessing with a9measure-first workflow: profile, fix minimally, re-measure against a budget. Covers backend10(database, caching, concurrency), frontend (bundles, images, rendering), network (CDN,11compression, HTTP), and memory, with concrete tools and before/after examples.1213## Quick Reference1415| Topic | Reference |16|-------|-----------|17| Profiling tools per runtime and reading flame graphs | [profiling-tools.md](references/profiling-tools.md) |18| EXPLAIN, index design, N+1 fixes, caching, pooling | [backend-optimization.md](references/backend-optimization.md) |19| Bundles, images, React rendering, Core Web Vitals | [frontend-performance.md](references/frontend-performance.md) |2021## Core Workflow2223### 1. Measure First2425Never optimize before you can answer: *"how do I know it got faster?"*26271. **Reproduce** — get a repeatable trigger (endpoint, page, script, load profile).282. **Baseline** — record current latency distribution, throughput, and resource usage.293. **Budget** — set an explicit target (e.g., p95 < 200 ms, initial JS < 170 KB gzipped).304. **Profile** — find where time actually goes (CPU, I/O wait, dependency latency, GC).315. **Hypothesize** — one bottleneck, one predicted fix, one measurable effect.326. **Fix minimally** — the smallest change that tests the hypothesis.337. **Re-measure** — same tool, load, and environment; keep the fix only if it moves the metric.3435### 2. The Triage Loop (symptom → cause)3637| Symptom | Likely cause | Quick check |38|---------|--------------|-------------|39| High p95 but healthy p50 | Queueing, GC pauses, one slow dependency | Percentile breakdown; GC logs; sample slow requests |40| Slow only under load | Contention, pool exhaustion, saturation | Load test at increasing concurrency; watch pool waits |41| Slow in prod, fast locally | Cold cache, missing index on prod-size data | EXPLAIN on prod data; cache hit ratio |42| ORM makes dozens of queries | N+1 lazy loading | Log query counts; enable ORM query logging |43| High CPU | CPU-bound loop, regex, serialization | cProfile / py-spy dump |44| Memory grows monotonically | Leak or unbounded cache | Heap snapshots; cache size/capacity audit |45| Slow page load, fast API | Render-blocking JS/CSS, oversized images | Lighthouse waterfall; DevTools Network |46| High TTFB | Slow backend or no CDN | `curl -w` timing breakdown |4748## Choosing the Right Metrics4950Track **percentiles, not just the mean** — a mean hides whether 5% of users wait 10x longer.5152| Metric | What it tells you | Pitfall |53|--------|-------------------|---------|54| Mean latency | Average experience | Skewed by outliers; hides the long tail |55| p50 latency | Typical experience | Misses everything bad |56| p95 / p99 latency | Worst-case experience | Noisy at low traffic volume |57| Throughput (RPS) | Capacity | Meaningless without a latency target |58| Error rate | Stability under load | Track *alongside* latency, never alone |59| Concurrency (in-flight) | Queueing behavior | High values mean requests are piling up |6061Turn metrics into **SLOs** (e.g., "99% of GET /api/orders < 300 ms", "error rate < 0.1%") and62load test with k6 or locust *before* production does:6364```javascript65import http from "k6/http";66export const options = {67 stages: [{ duration: "30s", target: 20 }, { duration: "1m", target: 100 }],68 thresholds: { http_req_duration: ["p(95)<250"] },69};70export default function () { http.get("http://localhost:8000/api/orders/42"); }71```7273## Backend Optimization7475### Database7677```sql78-- Red flags: Seq Scan on large tables, rows far higher than returned, missing indexes79EXPLAIN ANALYZE80SELECT o.*, i.* FROM orders o JOIN order_items i ON i.order_id = o.id81WHERE o.customer_id = 123 ORDER BY o.created_at DESC LIMIT 20;82```8384| Technique | What it fixes | Cost |85|-----------|---------------|------|86| Index on filter columns | Full table scans | Slower writes; keep index count sane |87| Composite index (column order matters) | Multi-column filters | More storage |88| Covering index | Avoids fetching table rows | Storage |89| Keyset pagination (`WHERE id > ?`) | Slow `OFFSET` on deep pages | Cursor in the API response |90| Query batching / eager loading | N+1 round trips | More complex ORM code |91| Denormalization (cached counts) | Expensive aggregates on hot paths | Invariants to maintain |9293N+1 — one query for the parent, one per child — is the most common backend perf bug:9495```python96# BAD: 1 + N queries97for order in session.query(Order).filter_by(customer_id=123):98 for item in order.items: # lazy load -> one query per order99 ...100# GOOD: 2 queries101from sqlalchemy.orm import selectinload102orders = session.query(Order).options(selectinload(Order.items)).filter_by(customer_id=123)103```104105### Caching106107**Cache-aside** (read-through on miss, delete on write) is the default pattern:108109```python110def get_order(order_id: int):111 cached = redis.get(f"order:{order_id}")112 if cached is not None:113 return json.loads(cached) # cache hit — no DB114 order = db.query(Order).get(order_id) # miss — DB115 redis.setex(f"order:{order_id}", 60, json.dumps(order.to_dict())) # TTL 60s116 return order117```118119Rules that prevent cache bugs:120121- **Set a TTL on everything** — short TTLs make correctness bugs self-heal.122- **Invalidate on write** — delete the key in the same flow that mutates the row.123- **Cap cache size** (LRU / `maxmemory-policy allkeys-lru`) — an unbounded cache is a leak.124- **Guard against stampedes** — single-flight lock so concurrent misses don't all hit the DB125 (example in [backend-optimization.md](references/backend-optimization.md)).126127### Concurrency & I/O128129- **Pool your connections** (DB, HTTP, Redis) — establishing connections is expensive; size130 pools to cores x expected concurrency (pgbouncer, explicit pool bounds).131- **Batch external calls** — fan out parallel requests instead of serial ones:132133```python134# BAD: 3 round trips, serial — latency adds up135a = api_a.get(x); b = api_b.get(x); c = api_c.get(x)136# GOOD: 3 round trips, parallel — latency of the slowest137import asyncio138a, b, c = await asyncio.gather(api_a.get(x), api_b.get(x), api_c.get(x))139```140141- **Async I/O beats threads for I/O-bound work**; threads or multiprocessing beat async for142 CPU-bound work (see Concurrency Models below).143144## Frontend Optimization145146### Bundle size147148- **Code splitting** — load route/feature chunks on demand with `React.lazy` + `Suspense` or149 dynamic `import()`.150- **Tree shaking** — import named exports, avoid side-effectful imports; verify with151 `webpack-bundle-analyzer` / `rollup-plugin-visualizer`.152- **Budget the initial JS** (roughly 170 KB gzipped) and fail CI over it with `size-limit`.153154```tsx155const OrdersPage = React.lazy(() => import("./pages/OrdersPage"));156function App() {157 return (158 <Suspense fallback={<Spinner />}>159 <OrdersPage />160 </Suspense>161 );162}163```164165### Images & asset delivery166167- Serve modern formats (AVIF > WebP > JPEG) via CDN resizing or build-time conversion.168- Use `srcset`/`sizes` so mobile users download small files; set `width`/`height` or169 `aspect-ratio` to prevent layout shift (CLS).170- `loading="lazy"` below the fold, `fetchpriority="high"` for the LCP image.171- Inline **critical CSS**; load JS with `defer` (or `async` where order doesn't matter).172- `preconnect` to CDNs/APIs hit early; `preload` the LCP image or font.173- Hashed assets: `Cache-Control: public, max-age=31536000, immutable`; no-cache on HTML.174175### React rendering176177- `React.memo` to skip re-renders of expensive subtrees; `useMemo`/`useCallback` only where178 identity/cost actually matters — don't wrap everything.179- **Virtualize long lists** (`react-window`) instead of rendering 10k rows.180- **Avoid layout thrash** — batch DOM reads then writes; prefer CSS transforms over JS layout.181- Profile with React DevTools Profiler first — if renders are cheap, the problem is elsewhere.182183## Network Optimization184185| Technique | What it improves | Notes |186|-----------|------------------|-------|187| CDN | TTFB, cacheable static content | Edge-cache assets and even API responses |188| Compression (Brotli > gzip) | Transfer size (often 60-80% smaller) | Never compress already-encoded data (images) |189| HTTP/2 | Multiplexing, one connection, header compression | Requires HTTPS |190| HTTP/3 (QUIC) | Connection setup, loss resilience | Best on mobile/lossy networks; needs CDN/LB support |191| Keep-alive | Reuse connections, skip per-request handshakes | Default in most stacks; verify with `curl -v` |192| Prefetch / preconnect | Perceived speed for the next page / first request | `dns-prefetch`, `preconnect`, `prefetch` |193194```bash195curl -sS -o /dev/null -w "DNS %{time_namelookup} | TCP %{time_connect} | TLS %{time_appconnect} | TTFB %{time_starttransfer} | Total %{time_total}\n" https://example.com/api/orders/42196```197198## Concurrency Models199200| Model | Best for | Gotcha |201|-------|----------|--------|202| Threads | CPU-bound (non-GIL langs), blocking I/O | Python: the GIL serializes CPU work |203| Async I/O (event loop) | I/O-bound at high concurrency | One blocking call stalls everything |204| Multiprocessing / workers | CPU-bound in Python; isolation | Memory per process; IPC cost |205| Worker threads | Parallel CPU work in Node | No shared memory; message passing |206207Python note: the **GIL** means threads only help I/O-bound work — use `multiprocessing` for208CPU-bound. Node's event loop is the mirror image: offload sync CPU-heavy work from request209handlers to `worker_threads` or a job queue.210211## Memory212213- **Leaks** — JS: retained listeners, detached DOM nodes, module-level caches, closures.214 Python: unclosed files/connections, global caches, cyclic refs with `__del__`. Prove with215 heap snapshots (`tracemalloc`, Chrome Memory panel) taken at two points and diffed.216- **Unbounded caches** — every cache needs a size cap (LRU) or a TTL: `lru_cache(maxsize=...)`,217 `maxmemory-policy allkeys-lru`.218- **Object churn** — allocating in hot loops forces GC pressure. Reuse buffers; prefer typed219 arrays over per-iteration string concatenation.220221## Worked Example: Optimizing `GET /api/orders/{id}`222223**Baseline** (k6, 50 concurrent users): p50 210 ms, p95 **480 ms**, error rate 0.4%.224**Step 1 — profile.** Attach `py-spy`; the dump shows ~380 ms spent in the DB driver:225226```text227Thread 0x10a (idle 3ms):228 File "app/orders.py:42" _fetch_order229 File "sqlalchemy/orm/query.py:310" execute230 File "psycopg2/extensions.py:512" poll <- waiting on DB (~370ms)231```232233**Step 2 — hypothesis.** The latency is DB time, not Python. Confirm with EXPLAIN:234235```sql236EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC;237-- Seq Scan on orders (cost=0.00..1420.00 rows=1 actual rows=3800) <- full scan238```239240**Step 3 — fix** the missing `customer_id` index and the N+1 (3,800 queries/request); cache241hot orders for 60s:242243```sql244CREATE INDEX idx_orders_customer ON orders (customer_id, created_at DESC);245```246247```python248order = db.query(Order).options(selectinload(Order.items)).get(order_id)249cached = redis.get(f"order:{order_id}")250if cached is not None:251 return json.loads(cached)252redis.setex(f"order:{order_id}", 60, json.dumps(order.to_dict()))253```254255**Step 4 — re-measure** with the identical k6 script:256257| Metric | Before | After |258|--------|--------|-------|259| p50 | 210 ms | 9 ms |260| p95 | 480 ms | **28 ms** |261| Error rate | 0.4% | 0.0% |262| DB queries per request | 3,802 | 2 (or 0 on cache hit) |263264The index removed the scan (380 ms → ~15 ms), eager loading removed 3,800 round trips, and265the cache removed the DB for repeated hits — each change measured in isolation.266267## Anti-Patterns to Avoid268269- **Optimizing before measuring** — "fast" micro-optimizations that move nothing.270- **Fixing symptoms, not bottlenecks** — adding a cache without reading the plan; sometimes271 the index alone is the whole fix.272- **Mean-only dashboards** — a healthy mean hides a terrible p99 and the long tail.273- **Unbounded caches** — no TTL, no size cap: today's cache is tomorrow's OOM.274- **Premature concurrency** — threads/async add complexity; apply only after profiling shows275 I/O wait is the bottleneck.276- **Blindly memoizing React** — `useMemo`/`React.memo` add overhead; measure render cost first.277278## When to Use / Not Use279280**Use** when:281- The user reports slow endpoints, slow page loads, high latency, or poor throughput.282- You need to profile, load test, or establish performance budgets/SLOs.283- The task involves DB query optimization, caching, bundle/image optimization, or tuning284 concurrency — and you can measure the effect.285286**Do NOT use** when:287- The task is a pure correctness bug, feature, or refactor with no performance concern.288- The user only needs a one-off metric readout, not a systematic optimization pass.289- Performance already meets the budget — resist optimizing without a target.