diff --git a/SKILL.md b/SKILL.md index 7477cfd..d251831 100644 --- a/SKILL.md +++ b/SKILL.md @@ -180,6 +180,7 @@ Use labels to indicate priority: ## Additional Resources - [Architecture Review Guide](reference/architecture-review-guide.md) - 架构设计审查指南(SOLID、反模式、耦合度) +- [Performance Review Guide](reference/performance-review-guide.md) - 性能审查指南(Web Vitals、N+1、复杂度) - [Common Bugs Checklist](reference/common-bugs-checklist.md) - 按语言分类的常见错误清单 - [Security Review Guide](reference/security-review-guide.md) - 安全审查指南 - [Code Review Best Practices](reference/code-review-best-practices.md) - 代码审查最佳实践 diff --git a/reference/performance-review-guide.md b/reference/performance-review-guide.md new file mode 100644 index 0000000..87a8ba7 --- /dev/null +++ b/reference/performance-review-guide.md @@ -0,0 +1,752 @@ +# 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 图片懒加载 - 延迟关键内容 + + +// ✅ LCP 图片立即加载 + + +// ❌ 未优化的图片格式 + // PNG 文件过大 + +// ✅ 现代图片格式 + 响应式 + + + + Hero + +``` + +**审查要点:** +- [ ] LCP 元素是否设置 `fetchpriority="high"`? +- [ ] 是否使用 WebP/AVIF 格式? +- [ ] 是否有服务端渲染或静态生成? +- [ ] CDN 是否配置正确? + +### FCP 优化检查 + +```html + + + + + + + + +@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 ( + + ); // 10000 条数据 = 10000 个 DOM 节点 +} + +// ✅ 虚拟列表 - 只渲染可见项 +import { FixedSizeList } from 'react-window'; + +function VirtualList({ items }) { + return ( + + {({ index, style }) => ( +
{items[index].name}
+ )} +
+ ); +} +``` + +**大数据审查要点:** +- [ ] 列表超过 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/)