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

17 KiB
Raw Blame History

Performance Review Guide

性能审查指南,覆盖前端、后端、数据库、算法复杂度和 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 优化检查

// ❌ 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 优化检查

<!-- ❌ 阻塞渲染的 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 优化检查

// ❌ 长任务阻塞主线程
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 优化检查

/* ❌ 未指定尺寸的媒体 */
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 性能

代码分割与懒加载

// ❌ 一次性加载所有代码
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 体积优化

// ❌ 导入整个库
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
  • 是否有未使用的依赖?

列表渲染优化

// ❌ 渲染大列表
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. 未清理的事件监听

// ❌ 组件卸载后事件仍在监听
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// ✅ 清理事件监听
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

2. 未清理的定时器

// ❌ 定时器未清理
useEffect(() => {
  setInterval(fetchData, 5000);
}, []);

// ✅ 清理定时器
useEffect(() => {
  const timer = setInterval(fetchData, 5000);
  return () => clearInterval(timer);
}, []);

3. 闭包引用

// ❌ 闭包持有大对象引用
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. 未清理的订阅

// ❌ WebSocket/EventSource 未关闭
useEffect(() => {
  const ws = new WebSocket('wss://...');
  ws.onmessage = handleMessage;
}, []);

// ✅ 清理连接
useEffect(() => {
  const ws = new WebSocket('wss://...');
  ws.onmessage = handleMessage;
  return () => ws.close();
}, []);

内存审查清单

- [ ] useEffect 是否都有清理函数?
- [ ] 事件监听是否在组件卸载时移除?
- [ ] 定时器是否被清理?
- [ ] WebSocket/SSE 连接是否关闭?
- [ ] 大对象是否及时释放?
- [ ] 是否有全局变量累积数据?

检测工具

工具 用途
Chrome DevTools Memory 堆快照分析
MemLab (Meta) 自动化内存泄漏检测
Performance Monitor 实时内存监控

数据库性能

N+1 查询问题

# ❌ 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()
// 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'],
});

索引优化

-- ❌ 全表扫描
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%';

查询优化

-- ❌ 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),))

数据库审查清单

🔴 必须检查:
- [ ] 是否存在 N+1 查询?
- [ ] WHERE 子句列是否有索引?
- [ ] 是否避免了 SELECT *
- [ ] 大表查询是否有 LIMIT

🟡 建议检查:
- [ ] 是否使用了 EXPLAIN 分析查询计划?
- [ ] 复合索引列顺序是否正确?
- [ ] 是否有未使用的索引?
- [ ] 是否有慢查询日志监控?

API 性能

分页实现

// ❌ 返回全部数据
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),
    },
  });
});

缓存策略

// ✅ 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);
});

响应压缩

// ✅ 启用 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);
});

限流保护

// ✅ 速率限制
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 审查清单

- [ ] 列表接口是否有分页?
- [ ] 是否限制了每页最大数量?
- [ ] 热点数据是否有缓存?
- [ ] 是否启用了响应压缩?
- [ ] 是否有速率限制?
- [ ] 是否只返回必要字段?

算法复杂度

常见复杂度对比

复杂度 名称 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 递归斐波那契

代码审查中的识别

// ❌ 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];
}
// ❌ 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)];
}
// ❌ 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)
}

空间复杂度考虑

// ⚠️ 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;
}

复杂度审查问题

💡 "这个嵌套循环的复杂度是 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 Core Web Vitals 测试
WebPageTest 详细性能分析
webpack-bundle-analyzer Bundle 分析
Chrome DevTools Performance 运行时性能分析

内存检测

工具 用途
MemLab 自动化内存泄漏检测
Chrome Memory Tab 堆快照分析

后端性能

工具 用途
EXPLAIN 数据库查询计划分析
pganalyze PostgreSQL 性能监控
New Relic / Datadog APM 监控

参考资源