AgentSkillsCN

performance-audit

在诊断或修复性能问题时使用此功能。涵盖React渲染优化、Bundle分析、懒加载、数据库查询优化、缓存、N+1查询,以及连接池管理。

SKILL.md
--- frontmatter
name: performance-audit
description: Use when diagnosing or fixing performance issues. Covers React rendering optimization, bundle analysis, lazy loading, database query optimization, caching, N+1 queries, and connection pooling.

Performance Optimization

React Rendering Optimization

React.memo - Prevent Unnecessary Re-Renders

tsx
// Only use memo when:
// 1. Component re-renders frequently with the same props
// 2. Component is expensive to render (large tree, heavy computation)

const ExpensiveList = React.memo(function ExpensiveList({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});

// NEVER memo everything by default. Profile first, optimize second.

useMemo - Cache Expensive Computations

tsx
function Dashboard({ transactions }: { transactions: Transaction[] }) {
  // GOOD: expensive computation that depends on data
  const summary = useMemo(() => {
    return transactions.reduce((acc, t) => ({
      total: acc.total + t.amount,
      count: acc.count + 1,
      average: (acc.total + t.amount) / (acc.count + 1),
    }), { total: 0, count: 0, average: 0 });
  }, [transactions]);

  // BAD: trivial computation, useMemo adds overhead
  // const fullName = useMemo(() => `${first} ${last}`, [first, last]);

  return <SummaryCard data={summary} />;
}

useCallback - Stable Function References

tsx
function TodoList({ todos }: { todos: Todo[] }) {
  // Stable reference prevents MemoizedTodoItem from re-rendering
  const handleToggle = useCallback((id: string) => {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }, []);

  return todos.map(todo => (
    <MemoizedTodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
  ));
}

Key Rendering Rules

tsx
// 1. Lift state up only as far as needed. Keep state close to where it is used.
// 2. Split components at render boundaries. If part of the UI changes often, isolate it.
// 3. Use children pattern to avoid re-rendering wrappers.

// BAD: Parent re-renders on every mouse move, re-rendering ExpensiveTree
function Parent() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
      <Cursor pos={pos} />
      <ExpensiveTree />  {/* re-renders unnecessarily */}
    </div>
  );
}

// GOOD: Extract the changing part
function MouseTracker({ children }: { children: React.ReactNode }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
      <Cursor pos={pos} />
      {children}  {/* children reference is stable, no re-render */}
    </div>
  );
}

Bundle Analysis

bash
# Analyze bundle size
npx vite-bundle-visualizer   # Vite projects
npx @next/bundle-analyzer    # Next.js projects

# Check individual package sizes before adding
npx bundlephobia <package-name>

Code Splitting and Lazy Loading

tsx
// Route-level splitting
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Component-level splitting for heavy features
const Chart = React.lazy(() => import('./components/Chart'));
const MarkdownEditor = React.lazy(() => import('./components/MarkdownEditor'));

Database Query Optimization

N+1 Query Problem

ts
// BAD: N+1 queries (1 query for projects + N queries for tasks)
const projects = await db.select().from(projectsTable);
for (const project of projects) {
  project.tasks = await db.select().from(tasksTable).where(eq(tasksTable.projectId, project.id));
}

// GOOD: Single query with join
const result = await db
  .select()
  .from(projectsTable)
  .leftJoin(tasksTable, eq(tasksTable.projectId, projectsTable.id))
  .where(eq(projectsTable.ownerId, userId));

// GOOD: Two queries with IN clause
const projects = await db.select().from(projectsTable).where(eq(projectsTable.ownerId, userId));
const projectIds = projects.map(p => p.id);
const tasks = await db.select().from(tasksTable).where(inArray(tasksTable.projectId, projectIds));

Indexing for Query Performance

ts
// Index columns used in WHERE, JOIN, and ORDER BY
pgTable('tasks', { ... }, (table) => ({
  projectIdx: index('tasks_project_id_idx').on(table.projectId),
  statusIdx: index('tasks_status_idx').on(table.status),
  // Composite index for common query patterns
  projectStatusIdx: index('tasks_project_status_idx').on(table.projectId, table.status),
}));

// Analyze slow queries
// EXPLAIN ANALYZE SELECT * FROM tasks WHERE project_id = 1 AND status = 'todo';

Pagination

ts
// Offset pagination (simple, suitable for small datasets)
const page = 1, limit = 20;
const data = await db.select().from(tasks)
  .limit(limit).offset((page - 1) * limit)
  .orderBy(desc(tasks.createdAt));

// Cursor pagination (better for large datasets)
const data = await db.select().from(tasks)
  .where(lt(tasks.createdAt, cursorDate))
  .limit(limit)
  .orderBy(desc(tasks.createdAt));

Caching Strategies

ts
// In-memory cache for hot data (small, read-heavy)
import { LRUCache } from 'lru-cache';

const userCache = new LRUCache<string, User>({ max: 1000, ttl: 5 * 60 * 1000 });

async function getUser(id: string): Promise<User> {
  const cached = userCache.get(id);
  if (cached) return cached;
  const user = await db.select().from(users).where(eq(users.id, id));
  userCache.set(id, user);
  return user;
}

// Redis for shared/distributed cache
async function getCachedProjects(userId: string): Promise<Project[]> {
  const cached = await redis.get(`projects:${userId}`);
  if (cached) return JSON.parse(cached);
  const projects = await db.select().from(projectsTable).where(eq(projectsTable.ownerId, userId));
  await redis.set(`projects:${userId}`, JSON.stringify(projects), 'EX', 300);
  return projects;
}

// Invalidate on write
async function createProject(data: NewProject) {
  const project = await db.insert(projectsTable).values(data).returning();
  await redis.del(`projects:${data.ownerId}`); // invalidate cache
  return project;
}

Connection Pooling

ts
// Always use connection pools, never single connections
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,                    // max connections
  idleTimeoutMillis: 30000,   // close idle connections after 30s
  connectionTimeoutMillis: 5000,
});

// For serverless (Vercel, Lambda), use connection pooler like PgBouncer
// or Neon's connection pooling endpoint

Anti-Patterns

  • NEVER optimize without profiling first. Use React DevTools Profiler and database EXPLAIN.
  • NEVER cache mutable data without an invalidation strategy.
  • NEVER use SELECT * when you only need a few columns.
  • NEVER skip pagination on list endpoints. Always limit results.
  • NEVER open a new database connection per request. Always use a pool.
  • NEVER add indexes blindly. Each index slows writes and consumes storage.