SKILL.md
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
- 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.
- Identify. Find the actual bottleneck, not the assumed one. Use the routing table below.
- Fix. Address that specific bottleneck. Code patterns for the common anti-patterns are in references/anti-patterns.md, read it when applying a fix.
- Verify. Measure again under the same conditions. Confirm the number moved and tests still pass.
- 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.