SKILL.md
readonly只读
name
performance-optimization
description
优化前端、后端、查询和数据库的应用程序性能。当存在性能要求、怀疑性能回归、需要改进核心网页指标或加载时间、需要修复N+1查询模式或性能分析发现瓶颈时使用。
性能优化
概述
先测量再优化。没有测量的性能工作就是猜测——而猜测会导致过早优化,增加复杂性却没有改善真正重要的东西。先做性能分析,找出实际瓶颈,修复它,再次测量。只优化那些经测量证明重要的东西。
何时使用
- 规格说明中存在性能要求(加载时间预算、响应时间SLA)
- 用户或监控报告行为缓慢
- 核心网页指标得分低于阈值
- 怀疑某个更改引入了回归
- 构建处理大数据集或高流量的功能
何时不使用: 在你有证据表明存在问题之前不要优化。过早优化会增加复杂性,其成本超过性能提升带来的收益。
核心网页指标目标
| 指标 | 良好 | 需要改进 | 较差 |
|---|---|---|---|
| LCP(最大内容绘制) | ≤ 2.5秒 | ≤ 4.0秒 | > 4.0秒 |
| INP(与下一次绘制的交互) | ≤ 200毫秒 | ≤ 500毫秒 | > 500毫秒 |
| CLS(累积布局偏移) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
优化工作流程
1. 测量 → 使用真实数据建立基线
2. 识别 → 找到实际瓶颈(而非假设的)
3. 修复 → 解决特定瓶颈
4. 验证 → 再次测量,确认改进
5. 防护 → 添加监控或测试以防止回归
步骤1:测量
两种互补的方法——同时使用:
- 合成测试(Lighthouse、DevTools性能面板): 受控条件,可重现。最适合CI回归检测和隔离特定问题。
- 真实用户监控(web-vitals库、CrUX): 真实条件下的真实用户数据。需要验证修复是否真正改善了用户体验。
前端:
# 合成测试:Chrome DevTools中的Lighthouse(或CI)
# Chrome DevTools → 性能面板 → 录制
# Chrome DevTools MCP → 性能跟踪
# 真实用户监控:代码中的Web Vitals库
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
后端:
# 响应时间日志
# 应用程序性能监控(APM)
# 带计时的数据库查询日志
# 简单计时
console.time('db-query');
const result = await db.query(...);
console.timeEnd('db-query');
从何处开始测量
根据症状决定首先测量什么:
什么慢?
├── 首次页面加载
│ ├── 包太大? --> 测量包大小,检查代码分割
│ ├── 服务器响应慢? --> 在DevTools网络瀑布中测量TTFB
│ │ ├── DNS慢? --> 为已知来源添加dns-prefetch / preconnect
│ │ ├── TCP/TLS慢? --> 启用HTTP/2,检查边缘部署,keep-alive
│ │ └── 等待(服务器)慢? --> 分析后端,检查查询和缓存
│ └── 渲染阻塞资源? --> 检查网络瀑布中的CSS/JS阻塞
├── 交互感觉迟钝
│ ├── 点击时UI冻结? --> 分析主线程,查找长任务(>50毫秒)
│ ├── 表单输入延迟? --> 检查重新渲染,受控组件开销
│ └── 动画卡顿? --> 检查布局抖动,强制回流
├── 导航后的页面
│ ├── 数据加载? --> 测量API响应时间,检查瀑布流
│ └── 客户端渲染? --> 分析组件渲染时间,检查N+1请求
└── 后端 / API
├── 单个端点慢? --> 分析数据库查询,检查索引
├── 所有端点慢? --> 检查连接池、内存、CPU
└── 间歇性慢? --> 检查锁争用、GC暂停、外部依赖
步骤2:识别瓶颈
按类别划分的常见瓶颈:
前端:
| 症状 | 可能原因 | 调查方法 |
|---|---|---|
| LCP慢 | 大图片、渲染阻塞资源、服务器慢 | 检查网络瀑布、图片大小 |
| CLS高 | 没有尺寸的图片、延迟加载的内容、字体偏移 | 检查布局偏移归因 |
| INP差 | 主线程上的重JavaScript、大型DOM更新 | 检查性能跟踪中的长任务 |
| 初始加载慢 | 包大、网络请求多 | 检查包大小、代码分割 |
后端:
| 症状 | 可能原因 | 调查方法 |
|---|---|---|
| API响应慢 | N+1查询、缺少索引、未优化的查询 | 检查数据库查询日志 |
| 内存增长 | 泄漏的引用、无界缓存、大负载 | 堆快照分析 |
| CPU飙升 | 同步重计算、正则回溯 | CPU分析 |
| 高延迟 | 缺少缓存、冗余计算、网络跳数 | 跟踪请求在堆栈中的路径 |
步骤3:修复常见反模式
N+1查询(后端)
// 错误:N+1——每个任务查询一次所有者
const tasks = await db.tasks.findMany();
for (const task of tasks) {
task.owner = await db.users.findUnique({ where: { id: task.ownerId } });
}
// 正确:使用连接/包含的单个查询
const tasks = await db.tasks.findMany({
include: { owner: true },
});
无界数据获取
// 错误:获取所有记录
const allTasks = await db.tasks.findMany();
// 正确:带限制的分页
const tasks = await db.tasks.findMany({
take: 20,
skip: (page - 1) * 20,
orderBy: { createdAt: 'desc' },
});
缺少图片优化(前端)
<!-- 错误:没有尺寸,没有格式优化 -->
<img src="/hero.jpg" />
<!-- 正确:Hero / LCP图片——美术指导 + 分辨率切换,高优先级 -->
<!--
结合两种技术:
- 美术指导(media):每个断点不同的裁剪/构图
- 分辨率切换(srcset + sizes):根据屏幕密度选择正确的文件大小
-->
<picture>
<!-- 移动端:竖屏裁剪(8:10) -->
<source
media="(max-width: 767px)"
srcset="/hero-mobile-400.avif 400w, /hero-mobile-800.avif 800w"
sizes="100vw"
width="800"
height="1000"
type="image/avif"
/>
<source
media="(max-width: 767px)"
srcset="/hero-mobile-400.webp 400w, /hero-mobile-800.webp 800w"
sizes="100vw"
width="800"
height="1000"
type="image/webp"
/>
<!-- 桌面端:横屏裁剪(2:1) -->
<source
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w, /hero-1600.avif 1600w"
sizes="(max-width: 1200px) 100vw, 1200px"
width="1200"
height="600"
type="image/avif"
/>
<source
srcset="/hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
sizes="(max-width: 1200px) 100vw, 1200px"
width="1200"
height="600"
type="image/webp"
/>
<img
src="/hero-desktop.jpg"
width="1200"
height="600"
fetchpriority="high"
alt="Hero图片描述"
/>
</picture>
<!-- 正确:首屏下方图片——延迟加载 + 异步解码 -->
<img
src="/content.webp"
width="800"
height="400"
loading="lazy"
decoding="async"
alt="内容图片描述"
/>
不必要的重新渲染(React)
// 错误:每次渲染创建新对象,导致子组件重新渲染
function TaskList() {
return <TaskFilters options={{ sortBy: 'date', order: 'desc' }} />;
}
// 正确:稳定引用
const DEFAULT_OPTIONS = { sortBy: 'date', order: 'desc' } as const;
function TaskList() {
return <TaskFilters options={DEFAULT_OPTIONS} />;
}
// 对昂贵组件使用React.memo
const TaskItem = React.memo(function TaskItem({ task }: Props) {
return <div>{/* 昂贵的渲染 */}</div>;
});
// 对昂贵计算使用useMemo
function TaskStats({ tasks }: Props) {
const stats = useMemo(() => calculateStats(tasks), [tasks]);
return <div>{stats.completed} / {stats.total}</div>;
}
包体积过大
// 现代打包工具(Vite、webpack 5+)通过树摇自动处理命名导入,
// 前提是依赖项提供ESM并在package.json中标记为`sideEffects: false`。
// 在更改导入风格之前先做性能分析——真正的收益来自分割和懒加载。
// 正确:对重型、少用的功能使用动态导入
const ChartLibrary = lazy(() => import('./ChartLibrary'));
// 正确:路由级别的代码分割,包裹在Suspense中
const SettingsPage = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<SettingsPage />
</Suspense>
);
}
缺少缓存(后端)
// 缓存频繁读取、很少更改的数据
const CACHE_TTL = 5 * 60 * 1000; // 5分钟
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缓存头
app.use('/static', express.static('public', {
maxAge: '1y', // 缓存1年
immutable: true, // 永不重新验证(在文件名中使用内容哈希)
}));
// API响应的Cache-Control
res.set('Cache-Control', 'public, max-age=300'); // 5分钟
性能预算
设定预算并强制执行:
JavaScript包:< 200KB gzip压缩(初始加载)
CSS:< 50KB gzip压缩
图片:< 200KB每张(首屏上方)
字体:< 100KB总计
API响应时间:< 200毫秒(p95)
可交互时间:< 3.5秒(4G网络)
Lighthouse性能得分:≥ 90
在CI中强制执行:
# 包大小检查
npx bundlesize --config bundlesize.config.json
# Lighthouse CI
npx lhci autorun
参见
有关详细的性能检查清单、优化命令和反模式参考,请参见references/performance-checklist.md。
常见合理化借口
| 合理化借口 | 现实 |
|---|---|
| “我们以后会优化” | 性能债务会累积。现在修复明显的反模式,推迟微优化。 |
| “在我的机器上很快” | 你的机器不是用户的机器。在具有代表性的硬件和网络上做性能分析。 |
| “这个优化很明显” | 如果你没有测量,你就不知道。先做性能分析。 |
| “用户不会注意到100毫秒” | 研究表明100毫秒的延迟会影响转化率。用户比你想象的更敏感。 |
| “框架处理性能” | 框架可以防止一些问题,但无法修复N+1查询或过大的包。 |
红旗警告
- 没有性能分析数据支持的优化
- 数据获取中的N+1查询模式
- 没有分页的列表端点
- 没有尺寸、懒加载或响应式尺寸的图片
- 包大小未经审查不断增长
- 生产环境中没有性能监控
- 到处使用
React.memo和useMemo(过度使用和不足使用一样糟糕)
验证
在任何与性能相关的更改之后:
- [ ] 存在前后测量数据(具体数字)
- [ ] 已识别并解决了特定瓶颈
- [ ] 核心网页指标在“良好”阈值内
- [ ] 包大小没有显著增加
- [ ] 新的数据获取代码中没有N+1查询
- [ ] 性能预算在CI中通过(如果已配置)
- [ ] 现有测试仍然通过(优化没有破坏行为)






