---
name: performance-optimization
description: Profile-first performance work: find the real bottleneck, fix it, prove the improvement. Use when Core Web Vitals or load times miss targets, users report slowness, a change may have regressed performance, or perf budgets exist. Not for adding the telemetry that detects slowness: use observability-and-instrumentation. Not for general debugging of wrong behavior: use systematic-debugging.
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: rewritten from patterns in addyosmani/agent-skills (MIT)
  category: Quality & Security
---

# Performance Optimization

Measure before optimizing. Performance work without measurement is guessing, and guessing produces complexity without improving what users feel. Do not optimize before you have evidence of a problem.

## Workflow

1. **Measure.** Establish a baseline with numbers. Synthetic (Lighthouse, DevTools performance trace) for reproducible comparison and CI. RUM (web-vitals library, CrUX) to validate real users actually improved. Backend: response-time logging, APM, query timing. Both kinds, not one.
2. **Identify.** Find the actual bottleneck, not the assumed one. Use the routing table below.
3. **Fix.** Address that specific bottleneck. Code patterns for the common anti-patterns are in references/anti-patterns.md, read it when applying a fix.
4. **Verify.** Measure again under the same conditions. Confirm the number moved and tests still pass.
5. **Guard.** Add a CI budget check or monitoring so it cannot silently regress.

## Where to look, by symptom

| Symptom | Likely cause | First measurement |
|---|---|---|
| Slow first load | Large bundle, render-blocking CSS/JS | Bundle analysis, network waterfall |
| Slow TTFB | Server work, DNS, no edge/keep-alive | Waterfall timing breakdown, backend profile |
| Slow LCP | Oversized hero image, blocking resources, slow server | Waterfall, image sizes, fetchpriority |
| High CLS | Images without dimensions, late content, font swap | Layout shift attribution |
| Poor INP / frozen UI | Long main-thread tasks, giant DOM updates | Performance trace, tasks over 50ms |
| Form input lag | Re-renders, controlled-component overhead | React profiler |
| Slow after navigation | Request waterfalls, N+1 fetches | API timing, request graph |
| One endpoint slow | N+1 queries, missing index | DB query log with timing |
| All endpoints slow | Pool exhaustion, CPU, memory | Host and pool metrics |
| Intermittent | Lock contention, GC pauses, external deps | Traces across the stack |

## Targets and budgets

Core Web Vitals "good": LCP <= 2.5s, INP <= 200ms, CLS <= 0.1.

Set budgets and enforce them in CI (bundle-size check plus Lighthouse CI). Sane defaults: initial JS under 200KB gzipped, CSS under 50KB, above-the-fold images under 200KB each, API p95 under 200ms, Lighthouse performance 90+. Adjust to your product, but write them down and gate on them.

## Verification

Run the same measurement before and after the fix, same conditions, same hardware profile. Expect: the targeted metric improved by a stated amount, no other budget regressed, and the test suite passes. If the number did not move, the bottleneck was misidentified: revert the change and go back to step 2 rather than stacking more "optimizations".

## Good vs Bad

**Bad:** "The list page feels slow, I added React.memo to every component and useMemo everywhere." No measurement, shotgun fix, added complexity, unknown effect.

**Good:** "Profile showed 1.8s in the tasks endpoint; query log revealed one owner lookup per task (N+1, 300 queries). Switched to a join: p95 went 1.9s to 210ms. Added a query-count assertion to the endpoint test."

## Completion checklist

- [ ] Before and after numbers recorded, same measurement conditions
- [ ] Specific bottleneck named and addressed
- [ ] Core Web Vitals within "good", or documented why not yet
- [ ] Bundle size did not grow unnoticed
- [ ] No new N+1 or unpaginated queries introduced
- [ ] Budget enforced in CI or a regression guard added
- [ ] Existing tests pass

Any box unchecked: not done. Fix or say so.

## Footguns

- **Optimizing on your machine.** Dev hardware on fiber hides everything. Fix: throttled profiles and RUM from representative devices and networks.
- **Averages.** A 200ms average with a 4s p99 means 1% of users suffer while your dashboard smiles. Fix: histograms, read p95/p99.
- **Memoizing everything.** React.memo and useMemo everywhere adds comparison overhead and complexity for unmeasured gain. Fix: profile, memoize the proven-expensive paths only.
- **Trusting the framework.** Frameworks cannot fix N+1 queries or a 2MB bundle you imported. Fix: measure your app, not the framework's marketing.

## Red Flags

Stop if you hear yourself think any of these:

- "This optimization is obvious, no need to profile"
- "It's fast on my machine"
- "We'll optimize later"
- "Users won't notice 100ms"
- "The framework handles performance"

Every one of these means you are about to guess. The rule is absolute: no fix without a baseline number, no "done" without an after number. Fix obvious anti-patterns (N+1, unpaginated endpoints, undimensioned images) as you write code; defer everything else until measured.
