performance-optimization

Tối ưu hóa hiệu suất ứng dụng trên frontend, backend, truy vấn và cơ sở dữ liệu. Sử dụng khi có yêu cầu về hiệu suất, khi nghi ngờ có suy giảm hiệu suất, khi cần cải thiện Core Web Vitals hoặc thời gian tải, khi cần sửa các mẫu truy vấn N+1, hoặc khi hồ sơ hiệu suất phát hiện điểm nghẽn.

npx skills add https://github.com/addyosmani/agent-skills --skill performance-optimization

Performance Optimization

Overview

Measure before optimizing. Performance work without measurement is guessing — and guessing leads to premature optimization that adds complexity without improving what matters. Profile first, identify the actual bottleneck, fix it, measure again. Optimize only what measurements prove matters.

When to Use

  • Performance requirements exist in the spec (load time budgets, response time SLAs)
  • Users or monitoring report slow behavior
  • Core Web Vitals scores are below thresholds
  • You suspect a change introduced a regression
  • Building features that handle large datasets or high traffic

When NOT to use: Don't optimize before you have evidence of a problem. Premature optimization adds complexity that costs more than the performance it gains.

Core Web Vitals Targets

MetricGoodNeeds ImprovementPoor
LCP (Largest Contentful Paint)≤ 2.5s≤ 4.0s> 4.0s
INP (Interaction to Next Paint)≤ 200ms≤ 500ms> 500ms
CLS (Cumulative Layout Shift)≤ 0.1≤ 0.25> 0.25

The Optimization Workflow

1. MEASURE  → Establish baseline with real data
2. IDENTIFY → Find the actual bottleneck (not assumed)
3. FIX      → Address the specific bottleneck
4. VERIFY   → Measure again; keep or revert
5. GUARD    → Add monitoring or tests to prevent regression

Step 1: Measure

Two complementary approaches — use both:

  • Synthetic (Lighthouse, DevTools Performance tab): Controlled conditions, reproducible. Best for CI regression detection and isolating specific issues.
  • RUM (web-vitals library, CrUX): Real user data in real conditions. Required to validate that a fix actually improved user experience.

Frontend:

# Synthetic: Lighthouse in Chrome DevTools (or CI)
# Chrome DevTools → Performance tab → Record
# Chrome DevTools MCP → Performance trace

# RUM: Web Vitals library in code
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

Backend:

# Response time logging
# Application Performance Monitoring (APM)
# Database query logging with timing

# Simple timing
console.time('db-query');
const result = await db.query(...);
console.timeEnd('db-query');

Where to Start Measuring

Use the symptom to decide what to measure first:

What is slow?
├── First page load
│   ├── Large bundle? --> Measure bundle size, check code splitting
│   ├── Slow server response? --> Measure TTFB in DevTools Network waterfall
│   │   ├── DNS long? --> Add dns-prefetch / preconnect for known origins
│   │   ├── TCP/TLS long? --> Enable HTTP/2, check edge deployment, keep-alive
│   │   └── Waiting (server) long? --> Profile backend, check queries and caching
│   └── Render-blocking resources? --> Check network waterfall for CSS/JS blocking
├── Interaction feels sluggish
│   ├── UI freezes on click? --> Profile main thread, look for long tasks (>50ms)
│   ├── Form input lag? --> Check re-renders, controlled component overhead
│   └── Animation jank? --> Check layout thrashing, forced reflows
├── Page after navigation
│   ├── Data loading? --> Measure API response times, check for waterfalls
│   └── Client rendering? --> Profile component render time, check for N+1 fetches
└── Backend / API
    ├── Single endpoint slow? --> Profile database queries, check indexes
    ├── All endpoints slow? --> Check connection pool, memory, CPU
    └── Intermittent slowness? --> Check for lock contention, GC pauses, external deps

Step 2: Identify the Bottleneck

Common bottlenecks by category:

Frontend:

SymptomLikely CauseInvestigation
Slow LCPLarge images, render-blocking resources, slow serverCheck network waterfall, image sizes
High CLSImages without dimensions, late-loading content, font shiftsCheck layout shift attribution
Poor INPHeavy JavaScript on main thread, large DOM updatesCheck long tasks in Performance trace
Slow initial loadLarge bundle, many network requestsCheck bundle size, code splitting

Backend:

SymptomLikely CauseInvestigation
Slow API responsesN+1 queries, missing indexes, unoptimized queriesCheck database query log
Memory growthLeaked references, unbounded caches, large payloadsHeap snapshot analysis
CPU spikesSynchronous heavy computation, regex backtrackingCPU profiling
High latencyMissing caching, redundant computation, network hopsTrace requests through the stack

Step 3: Fix Common Anti-Patterns

N+1 Queries (Backend)

// BAD: N+1 — one query per task for the owner
const tasks = await db.tasks.findMany();
for (const task of tasks) {
  task.owner = await db.users.findUnique({ where: { id: task.ownerId } });
}

// GOOD: Single query with join/include
const tasks = await db.tasks.findMany({
  include: { owner: true },
});

Unbounded Data Fetching

// BAD: Fetching all records
const allTasks = await db.tasks.findMany();

// GOOD: Paginated with limits
const tasks = await db.tasks.findMany({
  take: 20,
  skip: (page - 1) * 20,
  orderBy: { createdAt: 'desc' },
});

Missing Image Optimization (Frontend)

<!-- BAD: No dimensions, no format optimization -->
<img src="/hero.jpg" />

<!-- GOOD: Hero / LCP image — art direction + resolution switching, high priority -->
<!--
  Two techniques combined:
  - Art direction (media): different crop/composition per breakpoint
  - Resolution switching (srcset + sizes): right file size per screen density
-->
<picture>
  <!-- Mobile: portrait crop (8:10) -->
  <source
    media="(max-width: 767px)"
    srcset="/hero-mobile-400.avif 400w, /hero-mobile-800.avif 800w"
    sizes="100vw"
    width="800"
    height="1000"
    type="image/avif"
  />
  <source
    media="(max-width: 767px)"
    srcset="/hero-mobile-400.webp 400w, /hero-mobile-800.webp 800w"
    sizes="100vw"
    width="800"
    height="1000"
    type="image/webp"
  />
  <!-- Desktop: landscape crop (2:1) -->
  <source
    srcset="/hero-800.avif 800w, /hero-1200.avif 1200w, /hero-1600.avif 1600w"
    sizes="(max-width: 1200px) 100vw, 1200px"
    width="1200"
    height="600"
    type="image/avif"
  />
  <source
    srcset="/hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
    sizes="(max-width: 1200px) 100vw, 1200px"
    width="1200"
    height="600"
    type="image/webp"
  />
  <img
    src="/hero-desktop.jpg"
    width="1200"
    height="600"
    fetchpriority="high"
    alt="Hero image description"
  />
</picture>

<!-- GOOD: Below-the-fold image — lazy loaded + async decoding -->
<img
  src="/content.webp"
  width="800"
  height="400"
  loading="lazy"
  decoding="async"
  alt="Content image description"
/>

Unnecessary Re-renders (React)

// BAD: Creates new object on every render, causing children to re-render
function TaskList() {
  return <TaskFilters options={{ sortBy: 'date', order: 'desc' }} />;
}

// GOOD: Stable reference
const DEFAULT_OPTIONS = { sortBy: 'date', order: 'desc' } as const;
function TaskList() {
  return <TaskFilters options={DEFAULT_OPTIONS} />;
}

// Use React.memo for expensive components
const TaskItem = React.memo(function TaskItem({ task }: Props) {
  return <div>{/* expensive render */}</div>;
});

// Use useMemo for expensive computations
function TaskStats({ tasks }: Props) {
  const stats = useMemo(() => calculateStats(tasks), [tasks]);
  return <div>{stats.completed} / {stats.total}</div>;
}

Large Bundle Size

// Modern bundlers (Vite, webpack 5+) handle named imports with tree-shaking automatically,
// provided the dependency ships ESM and is marked `sideEffects: false` in package.json.
// Profile before changing import styles — the real gains come from splitting and lazy loading.

// GOOD: Dynamic import for heavy, rarely-used features
const ChartLibrary = lazy(() => import('./ChartLibrary'));

// GOOD: Route-level code splitting wrapped in Suspense
const SettingsPage = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <SettingsPage />
    </Suspense>
  );
}

Missing Caching (Backend)

// Cache frequently-read, rarely-changed data
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
let cachedConfig: AppConfig | null = null;
let cacheExpiry = 0;

async function getAppConfig(): Promise<AppConfig> {
  if (cachedConfig && Date.now() < cacheExpiry) {
    return cachedConfig;
  }
  cachedConfig = await db.config.findFirst();
  cacheExpiry = Date.now() + CACHE_TTL;
  return cachedConfig;
}

// HTTP caching headers for static assets
app.use('/static', express.static('public', {
  maxAge: '1y',           // Cache for 1 year
  immutable: true,        // Never revalidate (use content hashing in filenames)
}));

// Cache-Control for API responses
res.set('Cache-Control', 'public, max-age=300'); // 5 minutes

Step 4: Verify (Keep or Revert)

A fix is a hypothesis until you re-measure. This step decides whether it survives.

Re-measure the way you measured the baseline: same command, same conditions, same fixed budget (wall-clock, sample count, or request count). A baseline taken on a cold cache against a result taken on a warm one measures the cache, not your change.

Change one thing at a time. Three optimizations landed together produce one number, and you cannot attribute it. If they must ship together, measure each in isolation first.

Beat the noise, not just the mean. Repeat the measurement and compare the delta against run-to-run variance. A 3% gain inside ±5% variance is not a gain; it is a different sample.

Then decide, strictly:

Result vs. baselineAction
Past the threshold, tests greenKeep. Commit with the before/after numbers in the message.
Within noise (no measurable change)Revert.
WorseRevert.
Improved, but a test went redRevert. A regression wearing a win's clothing.

"Neutral" is a revert, not a keep. This is the step teams skip: the change is already written, throwing it away feels wasteful, so it lands unmeasured, and the codebase accretes complexity that never bought anything. Code you keep, you maintain forever. Make it pay for itself.

Correctness gates the metric. The suite stays green and the number moves. An "optimization" that wins by dropping work the product needed (skipping a validation, caching something that must be fresh, removing an await that was load-bearing) is a regression, not a win.

Log every attempt, including the reverted ones

Reverted work leaves no trace in git history, which is exactly why the same dead idea gets tried again next quarter. Keep a short ledger so a discarded idea stays discarded:

IdeaBaseline → ResultVerdictWhy
Memoize the row componentINP 240ms → 235msrevertedInside noise (±15ms). Rows weren't the bottleneck.
Virtualize the listINP 240ms → 90mskeptLong tasks gone from the trace.
Preconnect to the API originLCP 2.8s → 2.8srevertedAlready same-origin.

A section in the PR description or a PERF.md in the repo both work. What matters is that the next person (or the next agent) reads it before proposing an experiment, and doesn't re-run one that already failed.

Performance Budget

Set budgets and enforce them:

JavaScript bundle: < 200KB gzipped (initial load)
CSS: < 50KB gzipped
Images: < 200KB per image (above the fold)
Fonts: < 100KB total
API response time: < 200ms (p95)
Time to Interactive: < 3.5s on 4G
Lighthouse Performance score: ≥ 90

Enforce in CI:

# Bundle size check
npx bundlesize --config bundlesize.config.json

# Lighthouse CI
npx lhci autorun

See Also

For detailed performance checklists, optimization commands, and anti-pattern reference, see references/performance-checklist.md.

Common Rationalizations

RationalizationReality
"We'll optimize later"Performance debt compounds. Fix obvious anti-patterns now, defer micro-optimizations.
"It's fast on my machine"Your machine isn't the user's. Profile on representative hardware and networks.
"This optimization is obvious"If you didn't measure, you don't know. Profile first.
"Users won't notice 100ms"Research shows 100ms delays impact conversion rates. Users notice more than you think.
"The framework handles performance"Frameworks prevent some issues but can't fix N+1 queries or oversized bundles.
"It didn't help much, but it doesn't hurt"Neutral changes are a revert. You pay maintenance on them forever and got nothing back.
"We already wrote it, may as well keep it"Sunk cost. The measurement doesn't care how long the change took to write.
"The improvement is obvious, no need to re-measure"Then re-measuring is cheap and proves it. Unmeasured wins are how neutral complexity lands.

Red Flags

  • Optimization without profiling data to justify it
  • N+1 query patterns in data fetching
  • List endpoints without pagination
  • Images without dimensions, lazy loading, or responsive sizes
  • Bundle size growing without review
  • No performance monitoring in production
  • React.memo and useMemo everywhere (overusing is as bad as underusing)
  • Optimizations kept without a re-measurement that justifies them
  • Several optimizations bundled into one measurement, so no single change can be attributed
  • A "win" that required a test to be changed, skipped, or deleted
  • The same failed optimization attempted more than once because nobody recorded the first attempt

Verification

After any performance-related change:

  • Before and after measurements exist (specific numbers)
  • The result was re-measured the same way as the baseline (same command, same conditions)
  • The improvement exceeds run-to-run variance, not just the mean
  • Changes that didn't beat the baseline were reverted, not kept as neutral
  • Attempts are logged, kept and reverted alike, so a dead idea isn't re-run
  • The specific bottleneck is identified and addressed
  • Core Web Vitals are within "Good" thresholds
  • Bundle size hasn't increased significantly
  • No N+1 queries in new data fetching code
  • Performance budget passes in CI (if configured)
  • Existing tests still pass (optimization didn't break behavior)

Thêm skills từ addyosmani

accessibility
addyosmani
Kiểm tra và cải thiện khả năng truy cập web theo hướng dẫn WCAG 2.2. Sử dụng khi được yêu cầu "cải thiện khả năng truy cập", "kiểm tra a11y", "tuân thủ WCAG", "hỗ trợ trình đọc màn hình", "điều hướng bàn phím" hoặc "làm cho có thể truy cập".
developmenttestingcode-review
web-quality-audit
addyosmani
Kiểm tra chất lượng web toàn diện bao gồm hiệu suất, khả năng tiếp cận, SEO và các phương pháp tốt nhất. Sử dụng khi được yêu cầu "kiểm tra trang web của tôi", "đánh giá chất lượng web", "chạy kiểm tra lighthouse", "kiểm tra chất lượng trang" hoặc "tối ưu hóa trang web của tôi".
developmenttestingresearch
seo
addyosmani
Tối ưu hóa khả năng hiển thị và thứ hạng trên công cụ tìm kiếm. Sử dụng khi được yêu cầu "cải thiện SEO", "tối ưu hóa cho tìm kiếm", "sửa thẻ meta", "thêm dữ liệu có cấu trúc", "tối ưu hóa sơ đồ trang web" hoặc "tối ưu hóa công cụ tìm kiếm".
marketingresearchdevelopment
performance
addyosmani
Tối ưu hiệu suất web để tải nhanh hơn và cải thiện trải nghiệm người dùng. Sử dụng khi được yêu cầu "tăng tốc trang web của tôi", "tối ưu hiệu suất", "giảm thời gian tải", "sửa tải chậm", "cải thiện tốc độ trang" hoặc "kiểm tra hiệu suất".
developmenttesting
code-review-and-quality
addyosmani
Thực hiện đánh giá mã nguồn đa chiều. Sử dụng trước khi hợp nhất bất kỳ thay đổi nào. Sử dụng khi xem xét mã do chính bạn, một tác nhân khác hoặc con người viết. Sử dụng khi bạn cần đánh giá chất lượng mã trên nhiều khía cạnh trước khi nó được đưa vào nhánh chính.
developmentcode-review
frontend-ui-engineering
addyosmani
Xây dựng giao diện người dùng chất lượng sản xuất, có khả năng truy cập và phản hồi. Sử dụng khi xây dựng hoặc sửa đổi giao diện và trang, tạo thành phần, triển khai bố cục, đáp ứng yêu cầu truy cập WCAG, quản lý trạng thái, hoặc khi đầu ra cần trông và cảm nhận chất lượng sản xuất thay vì do AI tạo ra.
developmentdesign
security-and-hardening
addyosmani
Tăng cường bảo mật mã nguồn chống lại các lỗ hổng. Sử dụng khi xử lý đầu vào người dùng, xác thực, lưu trữ dữ liệu hoặc tích hợp bên ngoài. Sử dụng khi xây dựng bất kỳ tính năng nào chấp nhận dữ liệu không đáng tin cậy, quản lý phiên người dùng hoặc tương tác với dịch vụ bên thứ ba.
spec-driven-development
addyosmani
Tạo đặc tả trước khi viết mã. Sử dụng khi bắt đầu một dự án mới, tính năng mới hoặc thay đổi quan trọng và chưa có đặc tả nào tồn tại. Sử dụng khi yêu cầu chưa rõ ràng, mơ hồ hoặc chỉ tồn tại dưới dạng ý tưởng mơ hồ.
developmentdocumentproject-management