Entry/Exit
- Entry: Performance below target (LCP > 2.5s, slow page response), pre-launch performance review
- Exit: Quantifiable performance improvement + regression tests
- Points to: After optimization, return to
verify-workflow-revieworship-workflow-ship - Preload: CANON.md
- Output path: Performance improvement report →
verify-workflow-reviewreview
When not to use
- No quantifiable performance target, baseline, or user-perceivable slow point
- Just routine code cleanup or readability optimization
- The problem is first and foremost a functional bug, security risk, or unclear architectural boundary
Iron Law
Process: MEASURE → IDENTIFY → FIX → VERIFY → GUARD
MEASURE → 收集指标(Lighthouse、RUM、profiler)
IDENTIFY → 定位瓶颈(找出最大的一个瓶颈)
FIX → 一次改一个(只修测量证明有问题的)
VERIFY → 重新测量(对比 before/after)
GUARD → 加回归测试或性能预算
Core Web Vitals targets
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | 2.5-4.0s | > 4.0s |
| INP (Interaction to Next Paint) | ≤ 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1-0.25 | > 0.25 |
TTFB (Time to First Byte): ≤ 800ms is good.
Bottleneck identification
前端瓶颈:
├── 大 JS Bundle (> 200KB gzipped) → 代码拆分/lazy loading
├── 大图片 (> 500KB) → 压缩/WebP/响应式尺寸
├── 布局偏移 → 显式 width/height
├── 长任务 (> 50ms) → 拆分或 Web Worker
└── 未缓存 → 设置 Cache-Control header
后端瓶颈:
├── 慢查询 → EXPLAIN ANALYZE + 加索引
├── N+1 Queries → JOIN/batch 加载
├── 无分页 → 游标或偏移分页
├── 缺失缓存 → Redis/内存缓存热点数据
└── 同步阻塞 → 异步/队列
Common fixes
N+1 Queries
// Bad: N+1 —— 每个 task 产生一个额外查询
const tasks = await db.tasks.findMany();
for (const t of tasks) {
t.assignee = await db.users.findById(t.assigneeId); // +1 query per task
}
// Good: 一次 JOIN 加载
const tasks = await db.tasks.findMany({ include: { assignee: true } });
Unbounded queries
// Bad: 返回全部数据
app.get('/tasks', async (req, res) => {
const tasks = await db.tasks.findMany();
res.json(tasks);
});
// Good: 分页 + 限制
app.get('/tasks', async (req, res) => {
const { page = 1, pageSize = 20 } = req.query;
const tasks = await db.tasks.findMany({
skip: (page - 1) * pageSize,
take: Math.min(pageSize, 100), // 硬上限
});
res.json({ data: tasks, pagination: { page, pageSize } });
});
Image optimization
<!-- 响应式 + 现代格式 + 懒加载 -->
<img
srcset="hero-640.webp 640w, hero-1280.webp 1280w"
sizes="(max-width: 768px) 100vw, 50vw"
src="hero-1280.webp"
alt="产品图片"
width="1280"
height="720"
loading="lazy"
/>
Missing caching
Cache-Control 策略:
├── 静态资源 (JS/CSS/Fonts) → max-age=31536000, immutable
├── 图片 → max-age=86400
├── API 响应 (变化慢) → max-age=60, stale-while-revalidate=300
└── API 响应 (实时) → no-store
Performance budget
{
"budgets": [
{ "resourceType": "script", "budget": 200 }, // KB gzipped
{ "resourceType": "style", "budget": 50 },
{ "metric": "LCP", "budget": 2500 }, // ms
{ "metric": "CLS", "budget": 0.1 }
]
}
Verification evidence
The output or record must include:
- Input/Source: The spec, plan, code, feedback, or release context that was read.
- Actions taken: The checks, generation, fixes, exports, or release steps actually completed.
- Verification results: Commands, review conclusions, artifact paths, screenshots, or manual confirmation.
- Blocker/rollback: Failed items, rollback paths, or issues that need a human partner decision.
Common claims
| Claim | Reality | Consequence |
|---|---|---|
| "It's fast on my machine" | Your MacBook Pro ≠ the user's 4G Android. Measure with Lighthouse (simulated 4G + mid-tier CPU). | Fast locally ≠ fast for users → after launch, LCP > 4s on 4G devices → user bounce rate increases 30-50%. |
| "Just add more caching" | Caching masks the problem. Fix the query/loading first, then cache. | Caching masks slow queries → slow queries are instantly exposed when the cache expires → user experience drops off a cliff during peak periods. |
| "I added React.memo + useMemo everywhere" | Over-memoizing is as harmful as under-memoizing. Every memo has comparison cost. Measure clearly where it is needed. | Over-memoizing → comparison cost > render cost → total time instead increases by 5-15%; unmeasured memo optimization direction is random. |
| "Ship first, optimize performance later" | Performance problems after launch = users have already left. Set a budget from day one. | Launching without a budget → users' first experience is slow → 7-day retention drops 20-30%, and "optimize later" on average = never. |
| "Optimize all bottlenecks" | Optimize the biggest bottleneck. Unless measurement proves the rest are worth it—don't fix them. | Optimize everything → change scope too large → regression risk uncontrollable → one optimization introducing a bug will invalidate all optimizations. |
Red Flags — STOP
- Starting "optimization" without measurement
- Bundling multiple performance fixes together (don't know which one works)
- Not re-measuring after optimization (don't know what you changed)
- Premature optimization ("in case it's needed fast later" — is it needed now?)
- N+1 queries appearing in a loop
- List endpoint has no pagination
Verification Checklist
- Before and After measurement data exists
- Performance improvement quantified (LCP from Xms → Yms)
- No new performance regressions introduced
- Added performance regression tests or CI budget checks
- Only modified the parts that measurements proved problematic
- N+1 query detection passes
- List endpoint has pagination limits
Verification Failure Handling
| Failure Scenario | Handling |
|---|---|
| Starting optimization without measurement data | STOP. Go back to the MEASURE step and collect Lighthouse/RUM/profiler data. No baseline = guessing. |
| No performance improvement after optimization | Revert the fix and re-identify the bottleneck. You may have found the wrong bottleneck—go back to IDENTIFY. |
| Optimization introduces a functional regression | Revert the optimization, fix the regression first, then re-measure. Performance optimization must not sacrifice functional correctness. |
| Unable to reproduce the performance issue | Simulate production conditions: 4G network + mid-tier CPU + real data volume. Local test conditions are not the same as production. |
| Multiple bottlenecks exist at the same time | Fix only the largest one. After fixing, re-measure before looking at the next one. Do not batch optimize—you cannot tell which one works. |
Good and Bad Examples
Good — quantified before/after + bottleneck evidence
Lighthouse measures LCP 4.2s → IDENTIFY pinpoints N+1 queries as the bottleneck → FIX changes one thing at a time → VERIFY re-measures LCP 1.8s (57% improvement) → GUARD adds a performance budget. Every step has measurement data, not guesses.
Bad — "feels faster" with no measurement
"I added caching, the page should be faster" — no before/after data, no bottleneck identification, no re-measurement. You may have optimized an unimportant bottleneck, or introduced a new regression without discovering it.
Output Template
After performance optimization is complete, the output must include:
## Performance Optimization Report
### Baseline
- LCP: [X]ms → [Y]ms (改善 [Z]%)
- INP: [X]ms → [Y]ms
- CLS: [X] → [Y]
### Bottleneck Identified
- 位置: [文件:行号]
- 类型: N+1 / 无界查询 / 大 bundle / 未缓存 / 长任务
- 测量证据: [profiler/screenshot 数据]
### Fix Applied
- 改动: [描述]
- 文件: [路径]
- 变更行数: [N] 行
### Verification
- Before/after 测量对比: [数据]
- 无功能回归: ✅ / ❌
- 性能预算检查: ✅ / ❌