Files
agent-skills/skills/code-review-excellence/reference/performance-review-guide.md
Jason Woltje d9bcdc4a8d feat: Initial agent-skills repo — 4 adapted skills for Mosaic Stack
Skills included:
- pr-reviewer: Adapted for Gitea/GitHub via platform-aware scripts
  (dropped fetch_pr_data.py and add_inline_comment.py, kept generate_review_files.py)
- code-review-excellence: Methodology and checklists (React, TS, Python, etc.)
- vercel-react-best-practices: 57 rules for React/Next.js performance
- tailwind-design-system: Tailwind CSS v4 patterns, CVA, design tokens

New shell scripts added to ~/.claude/scripts/git/:
- pr-diff.sh: Get PR diff (GitHub gh / Gitea API)
- pr-metadata.sh: Get PR metadata as normalized JSON

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:03:39 -06:00

753 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Performance Review Guide
性能审查指南,覆盖前端、后端、数据库、算法复杂度和 API 性能。
## 目录
- [前端性能 (Core Web Vitals)](#前端性能-core-web-vitals)
- [JavaScript 性能](#javascript-性能)
- [内存管理](#内存管理)
- [数据库性能](#数据库性能)
- [API 性能](#api-性能)
- [算法复杂度](#算法复杂度)
- [性能审查清单](#性能审查清单)
---
## 前端性能 (Core Web Vitals)
### 2024 核心指标
| 指标 | 全称 | 目标值 | 含义 |
|------|------|--------|------|
| **LCP** | Largest Contentful Paint | ≤ 2.5s | 最大内容绘制时间 |
| **INP** | Interaction to Next Paint | ≤ 200ms | 交互响应时间2024 年替代 FID|
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | 累积布局偏移 |
| **FCP** | First Contentful Paint | ≤ 1.8s | 首次内容绘制 |
| **TBT** | Total Blocking Time | ≤ 200ms | 主线程阻塞时间 |
### LCP 优化检查
```javascript
// ❌ LCP 图片懒加载 - 延迟关键内容
<img src="hero.jpg" loading="lazy" />
// ✅ LCP 图片立即加载
<img src="hero.jpg" fetchpriority="high" />
// ❌ 未优化的图片格式
<img src="hero.png" /> // PNG 文件过大
// ✅ 现代图片格式 + 响应式
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="Hero" />
</picture>
```
**审查要点:**
- [ ] LCP 元素是否设置 `fetchpriority="high"`
- [ ] 是否使用 WebP/AVIF 格式?
- [ ] 是否有服务端渲染或静态生成?
- [ ] CDN 是否配置正确?
### FCP 优化检查
```html
<!-- ❌ 阻塞渲染的 CSS -->
<link rel="stylesheet" href="all-styles.css" />
<!-- ✅ 关键 CSS 内联 + 异步加载其余 -->
<style>/* 首屏关键样式 */</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<!-- ❌ 阻塞渲染的字体 -->
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
}
<!-- ✅ 字体显示优化 -->
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
font-display: swap; /* 先用系统字体,加载后切换 */
}
```
### INP 优化检查
```javascript
// ❌ 长任务阻塞主线程
button.addEventListener('click', () => {
// 耗时 500ms 的同步操作
processLargeData(data);
updateUI();
});
// ✅ 拆分长任务
button.addEventListener('click', async () => {
// 让出主线程
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
// 分批处理
for (const chunk of chunks) {
processChunk(chunk);
await scheduler.yield?.();
}
updateUI();
});
// ✅ 使用 Web Worker 处理复杂计算
const worker = new Worker('heavy-computation.js');
worker.postMessage(data);
worker.onmessage = (e) => updateUI(e.data);
```
### CLS 优化检查
```css
/* ❌ 未指定尺寸的媒体 */
img { width: 100%; }
/* ✅ 预留空间 */
img {
width: 100%;
aspect-ratio: 16 / 9;
}
/* ❌ 动态插入内容导致布局偏移 */
.ad-container { }
/* ✅ 预留固定高度 */
.ad-container {
min-height: 250px;
}
```
**CLS 审查清单:**
- [ ] 图片/视频是否有 width/height 或 aspect-ratio
- [ ] 字体加载是否使用 `font-display: swap`
- [ ] 动态内容是否预留空间?
- [ ] 是否避免在现有内容上方插入内容?
---
## JavaScript 性能
### 代码分割与懒加载
```javascript
// ❌ 一次性加载所有代码
import { HeavyChart } from './charts';
import { PDFExporter } from './pdf';
import { AdminPanel } from './admin';
// ✅ 按需加载
const HeavyChart = lazy(() => import('./charts'));
const PDFExporter = lazy(() => import('./pdf'));
// ✅ 路由级代码分割
const routes = [
{
path: '/dashboard',
component: lazy(() => import('./pages/Dashboard')),
},
{
path: '/admin',
component: lazy(() => import('./pages/Admin')),
},
];
```
### Bundle 体积优化
```javascript
// ❌ 导入整个库
import _ from 'lodash';
import moment from 'moment';
// ✅ 按需导入
import debounce from 'lodash/debounce';
import { format } from 'date-fns';
// ❌ 未使用 Tree Shaking
export default {
fn1() {},
fn2() {}, // 未使用但被打包
};
// ✅ 命名导出支持 Tree Shaking
export function fn1() {}
export function fn2() {}
```
**Bundle 审查清单:**
- [ ] 是否使用动态 import() 进行代码分割?
- [ ] 大型库是否按需导入?
- [ ] 是否分析过 bundle 大小webpack-bundle-analyzer
- [ ] 是否有未使用的依赖?
### 列表渲染优化
```javascript
// ❌ 渲染大列表
function List({ items }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
); // 10000 条数据 = 10000 个 DOM 节点
}
// ✅ 虚拟列表 - 只渲染可见项
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
```
**大数据审查要点:**
- [ ] 列表超过 100 项是否使用虚拟滚动?
- [ ] 表格是否支持分页或虚拟化?
- [ ] 是否有不必要的全量渲染?
---
## 内存管理
### 常见内存泄漏
#### 1. 未清理的事件监听
```javascript
// ❌ 组件卸载后事件仍在监听
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
// ✅ 清理事件监听
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
```
#### 2. 未清理的定时器
```javascript
// ❌ 定时器未清理
useEffect(() => {
setInterval(fetchData, 5000);
}, []);
// ✅ 清理定时器
useEffect(() => {
const timer = setInterval(fetchData, 5000);
return () => clearInterval(timer);
}, []);
```
#### 3. 闭包引用
```javascript
// ❌ 闭包持有大对象引用
function createHandler() {
const largeData = new Array(1000000).fill('x');
return function handler() {
// largeData 被闭包引用,无法被回收
console.log(largeData.length);
};
}
// ✅ 只保留必要数据
function createHandler() {
const largeData = new Array(1000000).fill('x');
const length = largeData.length; // 只保留需要的值
return function handler() {
console.log(length);
};
}
```
#### 4. 未清理的订阅
```javascript
// ❌ WebSocket/EventSource 未关闭
useEffect(() => {
const ws = new WebSocket('wss://...');
ws.onmessage = handleMessage;
}, []);
// ✅ 清理连接
useEffect(() => {
const ws = new WebSocket('wss://...');
ws.onmessage = handleMessage;
return () => ws.close();
}, []);
```
### 内存审查清单
```markdown
- [ ] useEffect 是否都有清理函数?
- [ ] 事件监听是否在组件卸载时移除?
- [ ] 定时器是否被清理?
- [ ] WebSocket/SSE 连接是否关闭?
- [ ] 大对象是否及时释放?
- [ ] 是否有全局变量累积数据?
```
### 检测工具
| 工具 | 用途 |
|------|------|
| Chrome DevTools Memory | 堆快照分析 |
| MemLab (Meta) | 自动化内存泄漏检测 |
| Performance Monitor | 实时内存监控 |
---
## 数据库性能
### N+1 查询问题
```python
# ❌ N+1 问题 - 1 + N 次查询
users = User.objects.all() # 1 次查询
for user in users:
print(user.profile.bio) # N 次查询(每个用户一次)
# ✅ Eager Loading - 2 次查询
users = User.objects.select_related('profile').all()
for user in users:
print(user.profile.bio) # 无额外查询
# ✅ 多对多关系用 prefetch_related
posts = Post.objects.prefetch_related('tags').all()
```
```javascript
// TypeORM 示例
// ❌ N+1 问题
const users = await userRepository.find();
for (const user of users) {
const posts = await user.posts; // 每次循环都查询
}
// ✅ Eager Loading
const users = await userRepository.find({
relations: ['posts'],
});
```
### 索引优化
```sql
-- ❌ 全表扫描
SELECT * FROM orders WHERE status = 'pending';
-- ✅ 添加索引
CREATE INDEX idx_orders_status ON orders(status);
-- ❌ 索引失效:函数操作
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- ✅ 范围查询可用索引
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- ❌ 索引失效LIKE 前缀通配符
SELECT * FROM products WHERE name LIKE '%phone%';
-- ✅ 前缀匹配可用索引
SELECT * FROM products WHERE name LIKE 'phone%';
```
### 查询优化
```sql
-- ❌ SELECT * 获取不需要的列
SELECT * FROM users WHERE id = 1;
-- ✅ 只查询需要的列
SELECT id, name, email FROM users WHERE id = 1;
-- ❌ 大表无 LIMIT
SELECT * FROM logs WHERE type = 'error';
-- ✅ 分页查询
SELECT * FROM logs WHERE type = 'error' LIMIT 100 OFFSET 0;
-- ❌ 在循环中执行查询
for id in user_ids:
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
-- ✅ 批量查询
cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
```
### 数据库审查清单
```markdown
🔴 必须检查:
- [ ] 是否存在 N+1 查询?
- [ ] WHERE 子句列是否有索引?
- [ ] 是否避免了 SELECT *
- [ ] 大表查询是否有 LIMIT
🟡 建议检查:
- [ ] 是否使用了 EXPLAIN 分析查询计划?
- [ ] 复合索引列顺序是否正确?
- [ ] 是否有未使用的索引?
- [ ] 是否有慢查询日志监控?
```
---
## API 性能
### 分页实现
```javascript
// ❌ 返回全部数据
app.get('/users', async (req, res) => {
const users = await User.findAll(); // 可能返回 100000 条
res.json(users);
});
// ✅ 分页 + 限制最大数量
app.get('/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
const offset = (page - 1) * limit;
const { rows, count } = await User.findAndCountAll({
limit,
offset,
order: [['id', 'ASC']],
});
res.json({
data: rows,
pagination: {
page,
limit,
total: count,
totalPages: Math.ceil(count / limit),
},
});
});
```
### 缓存策略
```javascript
// ✅ Redis 缓存示例
async function getUser(id) {
const cacheKey = `user:${id}`;
// 1. 检查缓存
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. 查询数据库
const user = await db.users.findById(id);
// 3. 写入缓存(设置过期时间)
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
// ✅ HTTP 缓存头
app.get('/static-data', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=86400', // 24 小时
'ETag': 'abc123',
});
res.json(data);
});
```
### 响应压缩
```javascript
// ✅ 启用 Gzip/Brotli 压缩
const compression = require('compression');
app.use(compression());
// ✅ 只返回必要字段
// 请求: GET /users?fields=id,name,email
app.get('/users', async (req, res) => {
const fields = req.query.fields?.split(',') || ['id', 'name'];
const users = await User.findAll({
attributes: fields,
});
res.json(users);
});
```
### 限流保护
```javascript
// ✅ 速率限制
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 分钟
max: 100, // 最多 100 次请求
message: { error: 'Too many requests, please try again later.' },
});
app.use('/api/', limiter);
```
### API 审查清单
```markdown
- [ ] 列表接口是否有分页?
- [ ] 是否限制了每页最大数量?
- [ ] 热点数据是否有缓存?
- [ ] 是否启用了响应压缩?
- [ ] 是否有速率限制?
- [ ] 是否只返回必要字段?
```
---
## 算法复杂度
### 常见复杂度对比
| 复杂度 | 名称 | 10 条 | 1000 条 | 100 万条 | 示例 |
|--------|------|-------|---------|----------|------|
| O(1) | 常数 | 1 | 1 | 1 | 哈希查找 |
| O(log n) | 对数 | 3 | 10 | 20 | 二分查找 |
| O(n) | 线性 | 10 | 1000 | 100 万 | 遍历数组 |
| O(n log n) | 线性对数 | 33 | 10000 | 2000 万 | 快速排序 |
| O(n²) | 平方 | 100 | 100 万 | 1 万亿 | 嵌套循环 |
| O(2ⁿ) | 指数 | 1024 | ∞ | ∞ | 递归斐波那契 |
### 代码审查中的识别
```javascript
// ❌ O(n²) - 嵌套循环
function findDuplicates(arr) {
const duplicates = [];
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
duplicates.push(arr[i]);
}
}
}
return duplicates;
}
// ✅ O(n) - 使用 Set
function findDuplicates(arr) {
const seen = new Set();
const duplicates = new Set();
for (const item of arr) {
if (seen.has(item)) {
duplicates.add(item);
}
seen.add(item);
}
return [...duplicates];
}
```
```javascript
// ❌ O(n²) - 每次循环都调用 includes
function removeDuplicates(arr) {
const result = [];
for (const item of arr) {
if (!result.includes(item)) { // includes 是 O(n)
result.push(item);
}
}
return result;
}
// ✅ O(n) - 使用 Set
function removeDuplicates(arr) {
return [...new Set(arr)];
}
```
```javascript
// ❌ O(n) 查找 - 每次都遍历
const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, ...];
function getUser(id) {
return users.find(u => u.id === id); // O(n)
}
// ✅ O(1) 查找 - 使用 Map
const userMap = new Map(users.map(u => [u.id, u]));
function getUser(id) {
return userMap.get(id); // O(1)
}
```
### 空间复杂度考虑
```javascript
// ⚠️ O(n) 空间 - 创建新数组
const doubled = arr.map(x => x * 2);
// ✅ O(1) 空间 - 原地修改(如果允许)
for (let i = 0; i < arr.length; i++) {
arr[i] *= 2;
}
// ⚠️ 递归深度过大可能栈溢出
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // O(n) 栈空间
}
// ✅ 迭代版本 O(1) 空间
function factorial(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
```
### 复杂度审查问题
```markdown
💡 "这个嵌套循环的复杂度是 O(n²),数据量大时会有性能问题"
🔴 "这里用 Array.includes() 在循环中,整体是 O(n²),建议用 Set"
🟡 "这个递归深度可能导致栈溢出,建议改为迭代或尾递归"
```
---
## 性能审查清单
### 🔴 必须检查(阻塞级)
**前端:**
- [ ] LCP 图片是否懒加载?(不应该)
- [ ] 是否有 `transition: all`
- [ ] 是否动画 width/height/top/left
- [ ] 列表 >100 项是否虚拟化?
**后端:**
- [ ] 是否存在 N+1 查询?
- [ ] 列表接口是否有分页?
- [ ] 是否有 SELECT * 查大表?
**通用:**
- [ ] 是否有 O(n²) 或更差的嵌套循环?
- [ ] useEffect/事件监听是否有清理?
### 🟡 建议检查(重要级)
**前端:**
- [ ] 是否使用代码分割?
- [ ] 大型库是否按需导入?
- [ ] 图片是否使用 WebP/AVIF
- [ ] 是否有未使用的依赖?
**后端:**
- [ ] 热点数据是否有缓存?
- [ ] WHERE 列是否有索引?
- [ ] 是否有慢查询监控?
**API**
- [ ] 是否启用响应压缩?
- [ ] 是否有速率限制?
- [ ] 是否只返回必要字段?
### 🟢 优化建议(建议级)
- [ ] 是否分析过 bundle 大小?
- [ ] 是否使用 CDN
- [ ] 是否有性能监控?
- [ ] 是否做过性能基准测试?
---
## 性能度量阈值
### 前端指标
| 指标 | 好 | 需改进 | 差 |
|------|-----|--------|-----|
| LCP | ≤ 2.5s | 2.5-4s | > 4s |
| INP | ≤ 200ms | 200-500ms | > 500ms |
| CLS | ≤ 0.1 | 0.1-0.25 | > 0.25 |
| FCP | ≤ 1.8s | 1.8-3s | > 3s |
| Bundle Size (JS) | < 200KB | 200-500KB | > 500KB |
### 后端指标
| 指标 | 好 | 需改进 | 差 |
|------|-----|--------|-----|
| API 响应时间 | < 100ms | 100-500ms | > 500ms |
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
| 页面加载 | < 3s | 3-5s | > 5s |
---
## 工具推荐
### 前端性能
| 工具 | 用途 |
|------|------|
| [Lighthouse](https://developer.chrome.com/docs/lighthouse/) | Core Web Vitals 测试 |
| [WebPageTest](https://www.webpagetest.org/) | 详细性能分析 |
| [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) | Bundle 分析 |
| [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) | 运行时性能分析 |
### 内存检测
| 工具 | 用途 |
|------|------|
| [MemLab](https://github.com/facebookincubator/memlab) | 自动化内存泄漏检测 |
| Chrome Memory Tab | 堆快照分析 |
### 后端性能
| 工具 | 用途 |
|------|------|
| EXPLAIN | 数据库查询计划分析 |
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
---
## 参考资源
- [Core Web Vitals - web.dev](https://web.dev/articles/vitals)
- [Optimizing Core Web Vitals - Vercel](https://vercel.com/guides/optimizing-core-web-vitals-in-2024)
- [MemLab - Meta Engineering](https://engineering.fb.com/2022/09/12/open-source/memlab/)
- [Big O Cheat Sheet](https://www.bigocheatsheet.com/)
- [N+1 Query Problem - Stack Overflow](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping)
- [API Performance Optimization](https://algorithmsin60days.com/blog/optimizing-api-performance/)