performance-engineering

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Performance 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

核心原则

  1. Measure before optimizing - Profile with real data (RUM, Lighthouse, DevTools) before writing any optimization code. Intuition about performance bottlenecks is usually wrong.
  2. 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.
  3. Optimize the critical path - Focus on what blocks the user from seeing and interacting with content. Everything else can load later.
  4. 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.
  5. Test at realistic scale - Load test with production-like data volumes and traffic patterns, not toy datasets with 10 concurrent users.

  1. 优化前先测量 - 在编写任何优化代码前,先用真实数据(RUM、Lighthouse、DevTools)进行分析。凭直觉判断性能瓶颈通常是错误的。
  2. 为所有指标设预算 - 为包体积(< 200kb JS)、LCP(< 2.5s)、INP(< 200ms)、CLS(< 0.1)设置性能预算。在CI中强制执行,以便在合并代码前发现性能回退问题。
  3. 优化关键路径 - 专注于阻碍用户查看和交互内容的因素。其他资源可延后加载。
  4. 积极缓存,精准失效 - 为静态资源使用不可变哈希,为动态内容设置短TTL,同时使用stale-while-revalidate策略兼顾两者优势。
  5. 以真实规模测试 - 使用接近生产环境的数据量和流量模式进行负载测试,而非仅用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
undefined

Analyze 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
sizes
attribute prevents mobile devices from downloading desktop-sized images.
priority
on LCP images eliminates lazy-load delay for the most important visual element.

适用场景: 任何包含图片的页面(大多数页面均包含)。图片通常是页面中体积最大的资源。
实现方式:
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%。响应式
sizes
属性可避免移动设备下载桌面尺寸的图片。为LCP图片设置
priority
可消除最重要视觉元素的懒加载延迟。

Pattern 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.
stale-while-revalidate
gives users instant responses while keeping data fresh in the background, providing the best perceived performance.

适用场景: 所有应用均可从缓存中获益。问题在于选择哪一层缓存以及设置何种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-revalidate
策略可为用户提供即时响应,同时在后台更新数据,实现最佳的感知性能。

Pattern 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
undefined

Run 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

性能预算参考

MetricGoodNeeds WorkPoor
LCP< 2.5s2.5s - 4.0s> 4.0s
INP< 200ms200ms - 500ms> 500ms
CLS< 0.10.1 - 0.25> 0.25
FCP< 1.8s1.8s - 3.0s> 3.0s
TTFB< 800ms800ms - 1800ms> 1800ms
Total JS< 200kb gz200-400kb gz> 400kb gz
Total page weight< 1MB1-3MB> 3MB

指标良好需要优化较差
LCP< 2.5s2.5s - 4.0s> 4.0s
INP< 200ms200ms - 500ms> 500ms
CLS< 0.10.1 - 0.25> 0.25
FCP< 1.8s1.8s - 3.0s> 3.0s
TTFB< 800ms800ms - 1800ms> 1800ms
总JS体积< 200kb gz200-400kb gz> 400kb gz
总页面体积< 1MB1-3MB> 3MB

Anti-Patterns

反模式

Anti-PatternWhy It's BadBetter Approach
Optimizing without measuringWastes effort on non-bottlenecksProfile first, then optimize measured hotspots
Loading all JS upfrontBlocks interactivity for unused codeCode split by route, lazy load heavy components
Images without
width
/
height
Causes layout shift (CLS)Always specify dimensions or use aspect-ratio
Cache-Control: no-cache
on everything
Every request hits origin serverUse
stale-while-revalidate
for API,
immutable
for hashed assets
Loading web fonts synchronouslyBlocks text rendering (FOIT)Use
font-display: swap
or
optional
Third-party scripts in
<head>
Blocks page renderingLoad with
async
or
defer
, or after interaction
N+1 database queriesLatency multiplies with data sizeUse eager loading (Prisma
include
), batch queries

反模式危害更佳方案
未测量就优化在非瓶颈上浪费精力先分析,再针对测量出的热点进行优化
一次性加载所有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
)、批量查询

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
    priority
    /
    fetchpriority="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
  • font-display: swap
    set for custom web fonts

  • Core Web Vitals 已在真实环境(RUM)和实验室(Lighthouse)中测量
  • 性能预算已在CI中强制执行(Lighthouse CI或bundlesize)
  • 包已分析并按路由分割
  • 图片已优化(WebP/AVIF、响应式尺寸、懒加载)
  • LCP图片使用
    priority
    /
    fetchpriority="high"
  • 已按资源类型配置缓存头
  • 为耗时查询配置服务端缓存(Redis或内存缓存)
  • 第三方脚本已审计并异步加载
  • 数据库查询已分析(无N+1、索引正确)
  • 已使用真实流量模式进行负载测试
  • 自定义Web字体已设置
    font-display: swap

Related Resources

相关资源

  • Skills:
    monitoring-observability
    (performance monitoring),
    serverless-development
    (cold start optimization)
  • Rules:
    docs/reference/checklists/ui-visual-changes.md
    (visual performance)
  • Rules:
    docs/reference/stacks/react-typescript.md
    (React performance patterns)
  • 技能:
    monitoring-observability
    (性能监控)、
    serverless-development
    (冷启动优化)
  • 规则:
    docs/reference/checklists/ui-visual-changes.md
    (视觉性能)
  • 规则:
    docs/reference/stacks/react-typescript.md
    (React性能模式)