performance-engineering
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePerformance Engineering
性能工程
Overview
概述
This skill covers measuring, optimizing, and maintaining web application performance across the full stack. It addresses Core Web Vitals (LCP, INP, CLS), bundle analysis and code splitting, image optimization, browser and server-side caching, CDN configuration, database query performance, and load testing with k6 and Artillery.
Use this skill when Core Web Vitals scores are below targets, bundle sizes are growing, page load times are degrading, or when preparing for traffic spikes. Performance work should be measurement-driven: profile first, optimize second, measure the impact third.
本技能涵盖全栈层面Web应用性能的测量、优化与维护。内容包括Core Web Vitals(LCP、INP、CLS)、包分析与代码分割、图片优化、浏览器与服务端缓存、CDN配置、数据库查询性能,以及使用k6和Artillery进行负载测试。
当Core Web Vitals评分低于目标值、包体积持续增长、页面加载时间变长,或为流量峰值做准备时,可使用本技能。性能优化工作需以测量为驱动:先分析,再优化,最后测量优化效果。
Core Principles
核心原则
- Measure before optimizing - Profile with real data (RUM, Lighthouse, DevTools) before writing any optimization code. Intuition about performance bottlenecks is usually wrong.
- Budget everything - Set performance budgets for bundle size (< 200kb JS), LCP (< 2.5s), INP (< 200ms), CLS (< 0.1). Enforce in CI so regressions are caught before merge.
- Optimize the critical path - Focus on what blocks the user from seeing and interacting with content. Everything else can load later.
- Cache aggressively, invalidate precisely - Use immutable hashes for static assets, short TTLs for dynamic content, and stale-while-revalidate for the best of both worlds.
- Test at realistic scale - Load test with production-like data volumes and traffic patterns, not toy datasets with 10 concurrent users.
- 优化前先测量 - 在编写任何优化代码前,先用真实数据(RUM、Lighthouse、DevTools)进行分析。凭直觉判断性能瓶颈通常是错误的。
- 为所有指标设预算 - 为包体积(< 200kb JS)、LCP(< 2.5s)、INP(< 200ms)、CLS(< 0.1)设置性能预算。在CI中强制执行,以便在合并代码前发现性能回退问题。
- 优化关键路径 - 专注于阻碍用户查看和交互内容的因素。其他资源可延后加载。
- 积极缓存,精准失效 - 为静态资源使用不可变哈希,为动态内容设置短TTL,同时使用stale-while-revalidate策略兼顾两者优势。
- 以真实规模测试 - 使用接近生产环境的数据量和流量模式进行负载测试,而非仅用10个并发用户的测试数据集。
Key Patterns
关键模式
Pattern 1: Core Web Vitals Monitoring
模式1:Core Web Vitals 监控
When to use: Every production web application. These metrics directly affect SEO ranking and user experience.
Implementation:
typescript
// 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,
}));typescript
// 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",
},
},
};Why: Lab metrics (Lighthouse) catch regressions before deployment. Field metrics (RUM) show real user experience across diverse devices and networks. You need both: lab for prevention, field for truth.
适用场景: 所有生产环境Web应用。这些指标直接影响SEO排名和用户体验。
实现方式:
typescript
// 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,
}));typescript
// 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)展示不同设备和网络环境下的真实用户体验。两者缺一不可:实验室指标用于预防问题,真实用户指标反映实际情况。
Pattern 2: Bundle Optimization and Code Splitting
模式2:包优化与代码分割
When to use: When JavaScript bundle size exceeds 200kb gzipped, or when initial load includes code for routes the user hasn't visited.
Implementation:
typescript
// 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>
);
}javascript
// 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"],
},
});bash
undefined适用场景: 当JavaScript包体积超过200kb(gzip压缩后),或初始加载包含用户未访问路由的代码时。
实现方式:
typescript
// 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>
);
}javascript
// 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"],
},
});bash
undefinedAnalyze bundle
Analyze bundle
ANALYZE=true npm run build
ANALYZE=true npm run build
Check specific import costs
Check specific import costs
npx import-cost # VS Code extension alternative
npx source-map-explorer .next/static/chunks/*.js
**Why:** Every kilobyte of JavaScript costs parse time, compile time, and execution time on the user's device. Code splitting ensures users only download the code needed for their current view. Lazy loading heavy components (editors, charts, maps) keeps initial bundle lean.
---npx import-cost # VS Code extension alternative
npx source-map-explorer .next/static/chunks/*.js
**原因:** 每千字节的JavaScript都会占用用户设备的解析、编译和执行时间。代码分割确保用户仅下载当前视图所需的代码。懒加载重型组件(编辑器、图表、地图)可保持初始包体积精简。
---Pattern 3: Image Optimization
模式3:图片优化
When to use: Any page with images (which is most pages). Images are typically the largest assets on a page.
Implementation:
tsx
// 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>
);
}typescript
// 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;
}Why: Images account for 50%+ of page weight on most sites. Modern formats (WebP, AVIF) reduce size by 25-50% over JPEG. Responsive attribute prevents mobile devices from downloading desktop-sized images. on LCP images eliminates lazy-load delay for the most important visual element.
sizespriority适用场景: 任何包含图片的页面(大多数页面均包含)。图片通常是页面中体积最大的资源。
实现方式:
tsx
// 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>
);
}typescript
// 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%。响应式属性可避免移动设备下载桌面尺寸的图片。为LCP图片设置可消除最重要视觉元素的懒加载延迟。
sizespriorityPattern 4: Caching Strategy
模式4:缓存策略
When to use: Every application benefits from caching. The question is which layer and what TTL.
Implementation:
typescript
// 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",
},
],
},
];
},
};typescript
// 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 }
);Why: Caching eliminates redundant computation and network requests. Immutable static assets with content hashes can be cached forever safely. gives users instant responses while keeping data fresh in the background, providing the best perceived performance.
stale-while-revalidate适用场景: 所有应用均可从缓存中获益。问题在于选择哪一层缓存以及设置何种TTL。
实现方式:
typescript
// 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",
},
],
},
];
},
};typescript
// 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-revalidatePattern 5: Load Testing with k6
模式5:使用k6进行负载测试
When to use: Before any major launch, migration, or when establishing performance baselines.
Implementation:
javascript
// 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
}bash
undefined适用场景: 任何重大发布、迁移前,或建立性能基线时。
实现方式:
javascript
// 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
}bash
undefinedRun load test
Run load test
k6 run load-test.js
k6 run load-test.js
Run with custom base URL
Run with custom base URL
k6 run -e BASE_URL=https://staging.example.com load-test.js
k6 run -e BASE_URL=https://staging.example.com load-test.js
Output results to JSON for analysis
Output results to JSON for analysis
k6 run --out json=results.json load-test.js
**Why:** Load testing reveals bottlenecks that only appear under concurrency: database connection pool exhaustion, memory leaks, lock contention, and cascading timeouts. Testing with realistic traffic patterns (ramp-up, sustained load, spikes) ensures your system handles real-world conditions.
---k6 run --out json=results.json load-test.js
**原因:** 负载测试可揭示仅在并发场景下才会出现的瓶颈:数据库连接池耗尽、内存泄漏、锁竞争和级联超时。使用真实流量模式(逐步加压、持续负载、峰值)进行测试可确保系统能应对真实世界的情况。
---Performance Budget Reference
性能预算参考
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| 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 |
| Total JS | < 200kb gz | 200-400kb gz | > 400kb gz |
| Total page weight | < 1MB | 1-3MB | > 3MB |
| 指标 | 良好 | 需要优化 | 较差 |
|---|---|---|---|
| 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 |
Anti-Patterns
反模式
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
| Optimizing without measuring | Wastes effort on non-bottlenecks | Profile first, then optimize measured hotspots |
| Loading all JS upfront | Blocks interactivity for unused code | Code split by route, lazy load heavy components |
Images without | Causes layout shift (CLS) | Always specify dimensions or use aspect-ratio |
| Every request hits origin server | Use |
| Loading web fonts synchronously | Blocks text rendering (FOIT) | Use |
Third-party scripts in | Blocks page rendering | Load with |
| N+1 database queries | Latency multiplies with data size | Use eager loading (Prisma |
| 反模式 | 危害 | 更佳方案 |
|---|---|---|
| 未测量就优化 | 在非瓶颈上浪费精力 | 先分析,再针对测量出的热点进行优化 |
| 一次性加载所有JS | 因未使用的代码阻碍交互 | 按路由分割代码,懒加载重型组件 |
图片未指定 | 导致布局偏移(CLS) | 始终指定尺寸或使用aspect-ratio |
所有资源设置 | 每个请求都命中源服务器 | API使用 |
| 同步加载Web字体 | 阻止文本渲染(FOIT) | 使用 |
| 阻止页面渲染 | 使用 |
| N+1数据库查询 | 延迟随数据量倍增 | 使用预加载(Prisma |
Checklist
检查清单
- Core Web Vitals measured in field (RUM) and lab (Lighthouse)
- Performance budgets enforced in CI (Lighthouse CI or bundlesize)
- Bundle analyzed and code-split by route
- Images optimized (WebP/AVIF, responsive sizes, lazy loading)
- LCP image uses /
priorityfetchpriority="high" - Cache headers configured per asset type
- Server-side caching for expensive queries (Redis or in-memory)
- Third-party scripts audited and loaded asynchronously
- Database queries analyzed (no N+1, proper indexes)
- Load testing done with realistic traffic patterns
- set for custom web fonts
font-display: swap
- Core Web Vitals 已在真实环境(RUM)和实验室(Lighthouse)中测量
- 性能预算已在CI中强制执行(Lighthouse CI或bundlesize)
- 包已分析并按路由分割
- 图片已优化(WebP/AVIF、响应式尺寸、懒加载)
- LCP图片使用/
priorityfetchpriority="high" - 已按资源类型配置缓存头
- 为耗时查询配置服务端缓存(Redis或内存缓存)
- 第三方脚本已审计并异步加载
- 数据库查询已分析(无N+1、索引正确)
- 已使用真实流量模式进行负载测试
- 自定义Web字体已设置
font-display: swap
Related Resources
相关资源
- Skills: (performance monitoring),
monitoring-observability(cold start optimization)serverless-development - Rules: (visual performance)
docs/reference/checklists/ui-visual-changes.md - Rules: (React performance patterns)
docs/reference/stacks/react-typescript.md
- 技能: (性能监控)、
monitoring-observability(冷启动优化)serverless-development - 规则: (视觉性能)
docs/reference/checklists/ui-visual-changes.md - 规则: (React性能模式)
docs/reference/stacks/react-typescript.md