addyosmani--agent-skills
46f377adde
Review phase skills covering OWASP Top 10 prevention, input validation, Core Web Vitals, and measure-before-optimize discipline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 行
7.9 KiB
Markdown
259 行
7.9 KiB
Markdown
---
|
|
name: performance-optimization
|
|
description: Use when performance requirements exist, when you suspect performance regressions, or when Core Web Vitals or load times need improvement. Use when profiling reveals bottlenecks that need fixing.
|
|
---
|
|
|
|
# Performance Optimization
|
|
|
|
## Overview
|
|
|
|
Measure before optimizing. Performance work without measurement is guessing — and guessing leads to premature optimization that adds complexity without improving what matters. Profile first, identify the actual bottleneck, fix it, measure again. Optimize only what measurements prove matters.
|
|
|
|
## When to Use
|
|
|
|
- Performance requirements exist in the spec (load time budgets, response time SLAs)
|
|
- Users or monitoring report slow behavior
|
|
- Core Web Vitals scores are below thresholds
|
|
- You suspect a change introduced a regression
|
|
- Building features that handle large datasets or high traffic
|
|
|
|
**When NOT to use:** Don't optimize before you have evidence of a problem. Premature optimization adds complexity that costs more than the performance it gains.
|
|
|
|
## Core Web Vitals Targets
|
|
|
|
| Metric | Good | Needs Improvement | Poor |
|
|
|--------|------|-------------------|------|
|
|
| **LCP** (Largest Contentful Paint) | ≤ 2.5s | ≤ 4.0s | > 4.0s |
|
|
| **INP** (Interaction to Next Paint) | ≤ 200ms | ≤ 500ms | > 500ms |
|
|
| **CLS** (Cumulative Layout Shift) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
|
|
|
|
## The Optimization Workflow
|
|
|
|
```
|
|
1. MEASURE → Establish baseline with real data
|
|
2. IDENTIFY → Find the actual bottleneck (not assumed)
|
|
3. FIX → Address the specific bottleneck
|
|
4. VERIFY → Measure again, confirm improvement
|
|
5. GUARD → Add monitoring or tests to prevent regression
|
|
```
|
|
|
|
### Step 1: Measure
|
|
|
|
**Frontend:**
|
|
```bash
|
|
# Lighthouse in Chrome DevTools (or CI)
|
|
# Chrome DevTools → Performance tab → Record
|
|
# Chrome DevTools MCP → Performance trace
|
|
|
|
# Web Vitals library in code
|
|
import { onLCP, onINP, onCLS } from 'web-vitals';
|
|
|
|
onLCP(console.log);
|
|
onINP(console.log);
|
|
onCLS(console.log);
|
|
```
|
|
|
|
**Backend:**
|
|
```bash
|
|
# Response time logging
|
|
# Application Performance Monitoring (APM)
|
|
# Database query logging with timing
|
|
|
|
# Simple timing
|
|
console.time('db-query');
|
|
const result = await db.query(...);
|
|
console.timeEnd('db-query');
|
|
```
|
|
|
|
### Step 2: Identify the Bottleneck
|
|
|
|
Common bottlenecks by category:
|
|
|
|
**Frontend:**
|
|
|
|
| Symptom | Likely Cause | Investigation |
|
|
|---------|-------------|---------------|
|
|
| Slow LCP | Large images, render-blocking resources, slow server | Check network waterfall, image sizes |
|
|
| High CLS | Images without dimensions, late-loading content, font shifts | Check layout shift attribution |
|
|
| Poor INP | Heavy JavaScript on main thread, large DOM updates | Check long tasks in Performance trace |
|
|
| Slow initial load | Large bundle, many network requests | Check bundle size, code splitting |
|
|
|
|
**Backend:**
|
|
|
|
| Symptom | Likely Cause | Investigation |
|
|
|---------|-------------|---------------|
|
|
| Slow API responses | N+1 queries, missing indexes, unoptimized queries | Check database query log |
|
|
| Memory growth | Leaked references, unbounded caches, large payloads | Heap snapshot analysis |
|
|
| CPU spikes | Synchronous heavy computation, regex backtracking | CPU profiling |
|
|
| High latency | Missing caching, redundant computation, network hops | Trace requests through the stack |
|
|
|
|
### Step 3: Fix Common Anti-Patterns
|
|
|
|
#### N+1 Queries (Backend)
|
|
|
|
```typescript
|
|
// BAD: N+1 — one query per task for the owner
|
|
const tasks = await db.tasks.findMany();
|
|
for (const task of tasks) {
|
|
task.owner = await db.users.findUnique({ where: { id: task.ownerId } });
|
|
}
|
|
|
|
// GOOD: Single query with join/include
|
|
const tasks = await db.tasks.findMany({
|
|
include: { owner: true },
|
|
});
|
|
```
|
|
|
|
#### Unbounded Data Fetching
|
|
|
|
```typescript
|
|
// BAD: Fetching all records
|
|
const allTasks = await db.tasks.findMany();
|
|
|
|
// GOOD: Paginated with limits
|
|
const tasks = await db.tasks.findMany({
|
|
take: 20,
|
|
skip: (page - 1) * 20,
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
```
|
|
|
|
#### Missing Image Optimization (Frontend)
|
|
|
|
```html
|
|
<!-- BAD: No dimensions, no lazy loading, no responsive sizes -->
|
|
<img src="/hero.jpg" />
|
|
|
|
<!-- GOOD: Responsive, lazy-loaded, properly sized -->
|
|
<img
|
|
src="/hero.jpg"
|
|
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
|
|
sizes="(max-width: 768px) 100vw, 50vw"
|
|
width="1200"
|
|
height="600"
|
|
loading="lazy"
|
|
alt="Hero image description"
|
|
/>
|
|
```
|
|
|
|
#### Unnecessary Re-renders (React)
|
|
|
|
```tsx
|
|
// BAD: Creates new object on every render, causing children to re-render
|
|
function TaskList() {
|
|
return <TaskFilters options={{ sortBy: 'date', order: 'desc' }} />;
|
|
}
|
|
|
|
// GOOD: Stable reference
|
|
const DEFAULT_OPTIONS = { sortBy: 'date', order: 'desc' } as const;
|
|
function TaskList() {
|
|
return <TaskFilters options={DEFAULT_OPTIONS} />;
|
|
}
|
|
|
|
// Use React.memo for expensive components
|
|
const TaskItem = React.memo(function TaskItem({ task }: Props) {
|
|
return <div>{/* expensive render */}</div>;
|
|
});
|
|
|
|
// Use useMemo for expensive computations
|
|
function TaskStats({ tasks }: Props) {
|
|
const stats = useMemo(() => calculateStats(tasks), [tasks]);
|
|
return <div>{stats.completed} / {stats.total}</div>;
|
|
}
|
|
```
|
|
|
|
#### Large Bundle Size
|
|
|
|
```typescript
|
|
// BAD: Importing entire library
|
|
import { format } from 'date-fns';
|
|
|
|
// GOOD: Tree-shakable import (if the library supports it)
|
|
import { format } from 'date-fns/format';
|
|
|
|
// GOOD: Dynamic import for heavy, rarely-used features
|
|
const ChartLibrary = lazy(() => import('./ChartLibrary'));
|
|
```
|
|
|
|
#### Missing Caching (Backend)
|
|
|
|
```typescript
|
|
// Cache frequently-read, rarely-changed data
|
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
let cachedConfig: AppConfig | null = null;
|
|
let cacheExpiry = 0;
|
|
|
|
async function getAppConfig(): Promise<AppConfig> {
|
|
if (cachedConfig && Date.now() < cacheExpiry) {
|
|
return cachedConfig;
|
|
}
|
|
cachedConfig = await db.config.findFirst();
|
|
cacheExpiry = Date.now() + CACHE_TTL;
|
|
return cachedConfig;
|
|
}
|
|
|
|
// HTTP caching headers for static assets
|
|
app.use('/static', express.static('public', {
|
|
maxAge: '1y', // Cache for 1 year
|
|
immutable: true, // Never revalidate (use content hashing in filenames)
|
|
}));
|
|
|
|
// Cache-Control for API responses
|
|
res.set('Cache-Control', 'public, max-age=300'); // 5 minutes
|
|
```
|
|
|
|
## Performance Budget
|
|
|
|
Set budgets and enforce them:
|
|
|
|
```
|
|
JavaScript bundle: < 200KB gzipped (initial load)
|
|
CSS: < 50KB gzipped
|
|
Images: < 200KB per image (above the fold)
|
|
Fonts: < 100KB total
|
|
API response time: < 200ms (p95)
|
|
Time to Interactive: < 3.5s on 4G
|
|
Lighthouse Performance score: ≥ 90
|
|
```
|
|
|
|
**Enforce in CI:**
|
|
```bash
|
|
# Bundle size check
|
|
npx bundlesize --config bundlesize.config.json
|
|
|
|
# Lighthouse CI
|
|
npx lhci autorun
|
|
```
|
|
|
|
## Common Rationalizations
|
|
|
|
| Rationalization | Reality |
|
|
|---|---|
|
|
| "We'll optimize later" | Performance debt compounds. Fix obvious anti-patterns now, defer micro-optimizations. |
|
|
| "It's fast on my machine" | Your machine isn't the user's. Profile on representative hardware and networks. |
|
|
| "This optimization is obvious" | If you didn't measure, you don't know. Profile first. |
|
|
| "Users won't notice 100ms" | Research shows 100ms delays impact conversion rates. Users notice more than you think. |
|
|
| "The framework handles performance" | Frameworks prevent some issues but can't fix N+1 queries or oversized bundles. |
|
|
|
|
## Red Flags
|
|
|
|
- Optimization without profiling data to justify it
|
|
- N+1 query patterns in data fetching
|
|
- List endpoints without pagination
|
|
- Images without dimensions, lazy loading, or responsive sizes
|
|
- Bundle size growing without review
|
|
- No performance monitoring in production
|
|
- `React.memo` and `useMemo` everywhere (overusing is as bad as underusing)
|
|
|
|
## Verification
|
|
|
|
After any performance-related change:
|
|
|
|
- [ ] Before and after measurements exist (specific numbers)
|
|
- [ ] The specific bottleneck is identified and addressed
|
|
- [ ] Core Web Vitals are within "Good" thresholds
|
|
- [ ] Bundle size hasn't increased significantly
|
|
- [ ] No N+1 queries in new data fetching code
|
|
- [ ] Performance budget passes in CI (if configured)
|
|
- [ ] Existing tests still pass (optimization didn't break behavior)
|