关于 performance-engineering
performance-engineering 是面向 Web 应用开发者的性能测量与优化指南,强调先分析再优化并衡量效果。内容涵盖 Core Web Vitals、代码拆分、缓存、数据库查询和负载测试。
主要功能
- 01测量 Core Web Vitals
- 02分析 JavaScript 包并拆分代码
- 03优化图片和关键加载路径
- 04配置浏览器、服务端及 CDN 缓存
- 05分析数据库查询性能
- 06使用 k6 和 Artillery 进行负载测试
适用场景
- 01排查 Core Web Vitals 未达标
- 02缩减不断增长的前端包
- 03改善页面加载和交互速度
- 04为流量高峰准备并进行负载测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ travisjneuman/ .claude --skill performance-engineering安装后在 Claude Code 中输入/performance-engineering确认已加载。
详细介绍
性能工程
概述
本技能涵盖全栈 Web 应用性能的测量、优化和维护。内容包括 Core Web Vitals(LCP、INP、CLS)、包分析与代码拆分、图像优化、浏览器和服务器端缓存、CDN 配置、数据库查询性能,以及使用 k6 和 Artillery 进行负载测试。
当 Core Web Vitals 得分低于目标、包体积不断增长、页面加载时间变长,或准备应对流量激增时,请使用本技能。性能工作应以测量为依据:先分析,再优化,最后测量影响。
核心原则
- 优化前先测量 - 编写任何优化代码前,先用真实数据(RUM、Lighthouse、DevTools)进行分析。对性能瓶颈的直觉判断通常并不准确。
- 为所有指标设置预算 - 为性能设定预算:包体积(JS < 200kb)、LCP(< 2.5s)、INP(< 200ms)、CLS(< 0.1)。在代码审查中检查变更是否符合预算。
- 优化关键路径 - 重点关注哪些因素会阻止用户查看内容并与之交互。其他内容都可以稍后加载。
- 积极缓存,精确失效 - 为静态资源使用不可变哈希,为动态内容设置较短的 TTL,并使用 stale-while-revalidate,兼顾两者的优势。
- 基于真实规模进行评估 - 根据接近生产环境的数据量和流量评估设计,而不是使用只有 10 个并发用户的玩具数据集;仅在用户要求时运行负载测试。
关键模式
模式 1:Core Web Vitals 监控
适用场景: 每个生产环境中的 Web 应用。这些指标会直接影响 SEO 排名和用户体验。
实现:
// Real User Monitoring (RUM) with web-vitals library
import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";
interface VitalMetric {
name: string;
value: number;
rating: "good" | "needs-improvement" | "poor";
navigationType: string;
}
function sendToAnalytics(metric: VitalMetric) {
// Send to your analytics endpoint
navigator.sendBeacon("/api/vitals", JSON.stringify({
...metric,
url: window.location.href,
userAgent: navigator.userAgent,
connectionType: (navigator as unknown as { connection?: { effectiveType: string } })
.connection?.effectiveType ?? "unknown",
timestamp: Date.now(),
}));
}
// Capture all Core Web Vitals
onLCP((metric) => sendToAnalytics({
name: "LCP",
value: metric.value,
rating: metric.rating,
navigationType: metric.navigationType,
}));
onINP((metric) => sendToAnalytics({
name: "INP",
value: metric.value,
rating: metric.rating,
navigationType: metric.navigationType,
}));
onCLS((metric) => sendToAnalytics({
name: "CLS",
value: metric.value,
rating: metric.rating,
navigationType: metric.navigationType,
}));
onFCP((metric) => sendToAnalytics({
name: "FCP",
value: metric.value,
rating: metric.rating,
navigationType: metric.navigationType,
}));
onTTFB((metric) => sendToAnalytics({
name: "TTFB",
value: metric.value,
rating: metric.rating,
navigationType: metric.navigationType,
}));
只有在用户要求时,才添加 Lighthouse CI 等新的 CI 作业。
// Lighthouse CI configuration
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: [
"http://localhost:3000/",
"http://localhost:3000/dashboard",
"http://localhost:3000/pricing",
],
numberOfRuns: 3,
},
assert: {
assertions: {
"categories:performance": ["error", { minScore: 0.9 }],
"categories:accessibility": ["error", { minScore: 0.95 }],
"first-contentful-paint": ["warn", { maxNumericValue: 1800 }],
"largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
"interactive": ["error", { maxNumericValue: 3800 }],
"cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }],
"total-byte-weight": ["warn", { maxNumericValue: 500000 }],
},
},
upload: {
target: "temporary-public-storage",
},
},
};
原因: 实验室指标(Lighthouse)可在部署前发现回归。现场指标(RUM)反映不同设备和网络环境下的真实用户体验。两者缺一不可:实验室指标用于预防,现场指标反映真实情况。
模式 2:包优化与代码拆分
适用场景: JavaScript 包的 gzip 压缩后体积超过 200kb,或初始加载包含用户尚未访问的路由代码。
实现:
// Next.js - Dynamic imports for route-based code splitting
import dynamic from "next/dynamic";
// Heavy component loaded only when needed
const RichTextEditor = dynamic(() => import("@/components/RichTextEditor"), {
loading: () => <div className="editor-skeleton" aria-busy="true">Loading editor...</div>,
ssr: false, // Client-only component
});
const ChartDashboard = dynamic(() => import("@/components/ChartDashboard"), {
loading: () => <ChartSkeleton />,
});
// Conditional feature loading
function ProjectPage({ project }: { project: Project }) {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<h1>{project.name}</h1>
<button onClick={() => setShowEditor(true)}>Edit Description</button>
{showEditor && <RichTextEditor content={project.description} />}
</div>
);
}
// webpack-bundle-analyzer integration
// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer({
// Analyze specific packages for tree-shaking
experimental: {
optimizePackageImports: ["lodash-es", "date-fns", "@mui/material", "lucide-react"],
},
});
# Analyze bundle
ANALYZE=true npm run build
# Check specific import costs
npx import-cost # VS Code extension alternative
npx source-map-explorer .next/static/chunks/*.js
原因: 每增加 1 KB 的 JavaScript,都会增加用户设备上的解析、编译和执行时间。代码拆分可确保用户只下载当前视图所需的代码。延迟加载体积较大的组件(编辑器、图表、地图),可保持初始包精简。
模式 3:图像优化
适用场景: 任何包含图像的页面(大多数页面都符合这一条件)。图像通常是页面上体积最大的资源。
实现:
// Next.js Image component with proper optimization
import Image from "next/image";
// Responsive hero image
function HeroSection() {
return (
<section>
<Image
src="/hero.jpg"
alt="Team collaborating on a project"
width={1920}
height={1080}
priority // LCP element - skip lazy loading
sizes="100vw"
quality={85}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQ..." // Low-quality placeholder
/>
</section>
);
}
// Responsive card image
function ProductCard({ product }: { product: Product }) {
return (
<article>
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="lazy" // Below the fold
/>
<h3>{product.name}</h3>
</article>
);
}
// Sharp-based image processing pipeline for user uploads
import sharp from "sharp";
interface ImageVariant {
width: number;
suffix: string;
quality: number;
}
const variants: ImageVariant[] = [
{ width: 320, suffix: "sm", quality: 80 },
{ width: 640, suffix: "md", quality: 80 },
{ width: 1280, suffix: "lg", quality: 85 },
{ width: 1920, suffix: "xl", quality: 85 },
];
async function processUploadedImage(
buffer: Buffer,
filename: string
): Promise<string[]> {
const urls: string[] = [];
for (const variant of variants) {
// Generate WebP (best compression)
const webp = await sharp(buffer)
.resize(variant.width, null, { withoutEnlargement: true })
.webp({ quality: variant.quality })
.toBuffer();
// Generate AVIF (even better compression, slower to encode)
const avif = await sharp(buffer)
.resize(variant.width, null, { withoutEnlargement: true })
.avif({ quality: variant.quality - 10 })
.toBuffer();
const webpUrl = await uploadToStorage(webp, `${filename}-${variant.suffix}.webp`);
const avifUrl = await uploadToStorage(avif, `${filename}-${variant.suffix}.avif`);
urls.push(webpUrl, avifUrl);
}
return urls;
}
原因: 在大多数网站中,图像占页面总大小的 50% 以上。现代格式(WebP、AVIF)比 JPEG 小 25%~50%。响应式 sizes 属性可避免移动设备下载桌面尺寸的图像。在 LCP 图像上设置 priority,可避免最重要的视觉元素受到延迟加载的影响。
模式 4:缓存策略
适用场景: 每个应用都能从缓存中获益。关键在于选择缓存层级和 TTL。
实现:
// HTTP cache headers for different asset types
// next.config.js
module.exports = {
async headers() {
return [
// Static assets with content hashes - cache forever
{
source: "/_next/static/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
// API responses - short cache with revalidation
{
source: "/api/:path*",
headers: [
{
key: "Cache-Control",
value: "public, s-maxage=60, stale-while-revalidate=300",
},
],
},
// HTML pages - always revalidate
{
source: "/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=0, must-revalidate",
},
],
},
];
},
};
// Server-side caching with Redis
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);
interface CacheOptions {
ttlSeconds: number;
staleSeconds?: number; // Serve stale while revalidating
}
async function cached<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions
): Promise<T> {
const cached = await redis.get(key);
if (cached) {
const { data, expiry } = JSON.parse(cached);
const isStale = Date.now() > expiry;
if (!isStale) {
return data as T;
}
// Stale-while-revalidate: return stale data, refresh in background
if (options.staleSeconds) {
const staleDeadline = expiry + options.staleSeconds * 1000;
if (Date.now() < staleDeadline) {
// Revalidate in background (fire-and-forget)
refreshCache(key, fetcher, options).catch(console.error);
return data as T;
}
}
}
return refreshCache(key, fetcher, options);
}
async function refreshCache<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions
): Promise<T> {
const data = await fetcher();
const expiry = Date.now() + options.ttlSeconds * 1000;
const totalTtl = options.ttlSeconds + (options.staleSeconds ?? 0);
await redis.setex(key, totalTtl, JSON.stringify({ data, expiry }));
return data;
}
// Usage
const products = await cached(
`products:category:${categoryId}`,
() => db.products.findMany({ where: { categoryId } }),
{ ttlSeconds: 300, staleSeconds: 600 }
);
原因: 缓存可避免重复计算和网络请求。带有内容哈希的不可变静态资源可以安全地永久缓存。stale-while-revalidate 可在后台保持数据最新的同时,为用户提供即时响应,从而带来最佳的感知性能。
模式 5:使用 k6 进行负载测试
适用场景: 用户要求进行负载测试时,例如在重大发布或迁移前,或为性能建立基准。
实现:
// load-test.js - k6 load test script
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate, Trend } from "k6/metrics";
const errorRate = new Rate("errors");
const apiDuration = new Trend("api_duration", true);
export const options = {
stages: [
{ duration: "2m", target: 50 }, // Ramp up
{ duration: "5m", target: 50 }, // Steady state
{ duration: "2m", target: 200 }, // Spike
{ duration: "5m", target: 200 }, // Sustained spike
{ duration: "2m", target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ["p(95)<500", "p(99)<1000"], // 95th < 500ms, 99th < 1s
errors: ["rate<0.01"], // Error rate < 1%
api_duration: ["avg<200"], // Average API < 200ms
},
};
const BASE_URL = __ENV.BASE_URL || "http://localhost:3000";
export default function () {
// Simulate realistic user journey
const homeRes = http.get(`${BASE_URL}/`);
check(homeRes, {
"home status 200": (r) => r.status === 200,
"home load < 2s": (r) => r.timings.duration < 2000,
});
errorRate.add(homeRes.status >= 400);
sleep(1);
// API call
const apiRes = http.get(`${BASE_URL}/api/products?category=electronics`, {
headers: { "Content-Type": "application/json" },
});
check(apiRes, {
"api status 200": (r) => r.status === 200,
"api has results": (r) => JSON.parse(r.body).length > 0,
});
apiDuration.add(apiRes.timings.duration);
errorRate.add(apiRes.status >= 400);
sleep(Math.random() * 3); // Random think time
}
# Run load test
k6 run load-test.js
# Run with custom base URL
k6 run -e BASE_URL=https://staging.example.com load-test.js
# Output results to JSON for analysis
k6 run --out json=results.json load-test.js
原因: 负载测试可以揭示仅在并发情况下才会出现的瓶颈:数据库连接池耗尽、内存泄漏、锁竞争和级联超时。使用符合实际情况的流量模式(逐步增加、持续负载、流量激增)进行测试,可确保系统能够应对真实环境。
性能预算参考
| 指标 | 良好 | 需要改进 | 较差 |
|---|---|---|---|
| LCP | < 2.5s | 2.5s - 4.0s | > 4.0s |
| INP | < 200ms | 200ms - 500ms | > 500ms |
| CLS | < 0.1 | 0.1 - 0.25 | > 0.25 |
| FCP | < 1.8s | 1.8s - 3.0s | > 3.0s |
| TTFB | < 800ms | 800ms - 1800ms | > 1800ms |
| JS 总量 | < 200kb gz | 200-400kb gz | > 400kb gz |
| 页面总大小 | < 1MB | 1-3MB | > 3MB |
反模式
| 反模式 | 不良之处 | 更好的做法 |
|---|---|---|
| 未经测量就进行优化 | 在非瓶颈处浪费精力 | 先进行性能分析,再优化已测出的热点 |
| 一开始就加载所有 JS | 未使用的代码会阻碍交互 | 按路由拆分代码,并延迟加载较大的组件 |
图片未设置 width/height | 会导致布局偏移(CLS) | 始终指定尺寸或使用 aspect-ratio |
对所有内容都设置 Cache-Control: no-cache | 每个请求都会访问源服务器 | API 使用 stale-while-revalidate,带哈希的资源使用 immutable |
| 同步加载 Web 字体 | 会阻塞文本渲染(FOIT) | 使用 font-display: swap 或 optional |
将第三方脚本放在 <head> 中 | 会阻塞页面渲染 | 使用 async 或 defer 加载,或在交互后加载 |
| N+1 数据库查询 | 延迟会随数据量增加而倍增 | 使用预加载(Prisma include),并批量查询 |
检查清单
- 在真实环境(RUM)和实验室环境(Lighthouse)中测量 Core Web Vitals
- 定义性能预算,并在评审中进行检查
- 分析打包内容,并按路由拆分代码
- 优化图片(WebP/AVIF、响应式尺寸、延迟加载)
- LCP 图片使用
priority/fetchpriority="high" - 按资源类型配置缓存标头
- 对开销较大的查询进行服务端缓存(Redis 或内存缓存)
- 审查第三方脚本,并以异步方式加载
- 分析数据库查询(避免 N+1,设置适当的索引)
- 针对符合实际情况的流量,推演负载行为(仅在提出请求时进行负载测试)
- 为自定义 Web 字体设置
font-display: swap
相关资源
- 技能:
monitoring-observability(性能监控)、serverless-development(冷启动优化) - 规则:
docs/reference/checklists/ui-visual-changes.md(视觉性能) - 规则:
docs/reference/stacks/react-typescript.md(React 性能模式)