mcpField
P
performance-engineering

performance-engineering

github.com/travisjneuman开发106139 次安装1 天前更新
代码仓库

关于 performance-engineering

performance-engineering 是面向 Web 应用开发者的性能测量与优化指南,强调先分析再优化并衡量效果。内容涵盖 Core Web Vitals、代码拆分、缓存、数据库查询和负载测试。

主要功能

  1. 01测量 Core Web Vitals
  2. 02分析 JavaScript 包并拆分代码
  3. 03优化图片和关键加载路径
  4. 04配置浏览器、服务端及 CDN 缓存
  5. 05分析数据库查询性能
  6. 06使用 k6 和 Artillery 进行负载测试

适用场景

  1. 01排查 Core Web Vitals 未达标
  2. 02缩减不断增长的前端包
  3. 03改善页面加载和交互速度
  4. 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 得分低于目标、包体积不断增长、页面加载时间变长,或准备应对流量激增时,请使用本技能。性能工作应以测量为依据:先分析,再优化,最后测量影响。


核心原则

  1. 优化前先测量 - 编写任何优化代码前,先用真实数据(RUM、Lighthouse、DevTools)进行分析。对性能瓶颈的直觉判断通常并不准确。
  2. 为所有指标设置预算 - 为性能设定预算:包体积(JS < 200kb)、LCP(< 2.5s)、INP(< 200ms)、CLS(< 0.1)。在代码审查中检查变更是否符合预算。
  3. 优化关键路径 - 重点关注哪些因素会阻止用户查看内容并与之交互。其他内容都可以稍后加载。
  4. 积极缓存,精确失效 - 为静态资源使用不可变哈希,为动态内容设置较短的 TTL,并使用 stale-while-revalidate,兼顾两者的优势。
  5. 基于真实规模进行评估 - 根据接近生产环境的数据量和流量评估设计,而不是使用只有 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.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

反模式

反模式不良之处更好的做法
未经测量就进行优化在非瓶颈处浪费精力先进行性能分析,再优化已测出的热点
一开始就加载所有 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 性能模式)

评论

支持 Markdown