skills/doyoonear/skills-and-agents/perf-bottleneck-finder

perf-bottleneck-finder

SKILL.md

Performance Bottleneck Finder

성능 병목 지점을 찾고 구간별 시간을 정량적으로 측정하는 스킬입니다.

원칙: 직감을 믿지 말고 측정하라. 개발자가 병목이라고 추측한 지점이 실제 병목인 경우는 절반도 되지 않는다.


전체 흐름

"느리다" 제보
[Phase 1] 문제 정의 — "무엇이 느린가?" 를 구체화
[Phase 2] 자동 측정 — Playwright로 페이지 성능 자동 수집
[Phase 3] 구간 분해 — 전체 시간을 프론트/네트워크/서버/DB로 분리
[Phase 4] 병목 판단 — 비율 분석으로 우선순위 결정
[Output] 병목 보고서 — 구간별 시간 + 비율 + 개선 우선순위

Phase 1: 문제 정의

"느리다"는 주관적 표현이다. 반드시 아래 항목을 확인하여 측정 대상을 특정한다.

확인 항목

항목 질문 예시
어디서 어떤 페이지/화면에서? "대시보드 페이지"
언제 초기 로딩? 인터랙션 중? "페이지 진입 시"
어떻게 어떤 동작이 느린가? "데이터가 표시되기까지"
얼마나 체감 몇 초? "약 4~5초"
재현 항상? 특정 조건? "데이터가 많을 때"

재현 시나리오 확정

시나리오: [페이지/기능 이름]
트리거: [사용자 액션]
종료 조건: [완료 시점]
환경: [브라우저, 디바이스, 네트워크, 데이터 규모]

Phase 2: Playwright 자동 측정

스크립트 실행

python scripts/measure_page_performance.py --url "http://localhost:3000/dashboard" --runs 3

스크립트 옵션:

  • --url: 측정 대상 URL (필수)
  • --runs: 반복 측정 횟수 (기본값 3, 평균 산출)
  • --output: 결과 저장 파일 경로 (기본값: stdout)
  • --wait-for: 측정 종료를 판단할 셀렉터 (예: [data-loaded="true"])
  • --auth-cookie: 인증이 필요한 경우 쿠키 값

자동 측정 항목:

  • Page Load 전체 시간 (navigationStart → loadEventEnd)
  • DOMContentLoaded 시간
  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Time to Interactive (TTI)
  • API 요청별 TTFB 및 총 소요 시간
  • 응답 크기 (bytes)
  • JavaScript 실행 시간
  • 렌더링/페인팅 시간

MCP Playwright로 수동 측정 (스크립트 대안)

스크립트 실행이 어려운 경우 MCP Playwright 도구를 활용한다:

1. browser_navigate: 대상 페이지로 이동
2. browser_evaluate: Performance API로 타이밍 데이터 수집
   → performance.getEntriesByType('navigation')
   → performance.getEntriesByType('resource')
   → performance.getEntriesByType('paint')
3. browser_network_requests: API 요청 목록 + 타이밍 수집
4. browser_console_messages: 에러/경고 확인
5. browser_take_screenshot: 시각적 상태 기록

Performance API 측정 코드:

// browser_evaluate로 실행
() => {
  const nav = performance.getEntriesByType('navigation')[0];
  const paint = performance.getEntriesByType('paint');
  const resources = performance.getEntriesByType('resource')
    .filter(r => r.initiatorType === 'fetch' || r.initiatorType === 'xmlhttprequest');

  return {
    pageLoad: {
      total: Math.round(nav.loadEventEnd - nav.startTime),
      domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
      domInteractive: Math.round(nav.domInteractive - nav.startTime),
      ttfb: Math.round(nav.responseStart - nav.requestStart),
    },
    paint: paint.map(p => ({ name: p.name, time: Math.round(p.startTime) })),
    apiRequests: resources.map(r => ({
      name: r.name.split('/').pop(),
      url: r.name,
      ttfb: Math.round(r.responseStart - r.requestStart),
      total: Math.round(r.responseEnd - r.startTime),
      size: r.transferSize,
    })),
  };
}

Phase 3: 구간 분해

전체 소요 시간을 다음 구간으로 분리한다.

프론트엔드 구간

사용자 액션 발생
  ├─[A] JS 번들 로딩 + 파싱 ────── ?ms
  ├─[B] 컴포넌트 마운트 ─────── ?ms
  ├─[C] API 요청 발송 대기 ────── ?ms
  ├─[D] 응답 수신 후 상태 업데이트 ─ ?ms
  └─[E] 렌더링 + 페인트 ──────── ?ms

측정 도구:

  • Chrome DevTools → Performance 탭 녹화
  • React Profiler (React 프로젝트)
  • Performance API marks/measures

React Profiler 활용:

1. React DevTools → Profiler 탭
2. 녹화 시작 → 시나리오 수행 → 녹화 중지
3. 확인: 각 컴포넌트 렌더 횟수, 렌더 소요 시간, 불필요 리렌더 여부

네트워크 구간

API 요청 전송
  ├─[F] DNS Lookup ──────── ?ms
  ├─[G] TCP Connection ──── ?ms
  ├─[H] TLS Handshake ───── ?ms
  ├─[I] TTFB (서버 처리) ─── ?ms  ← 핵심 지표
  └─[J] Content Download ─── ?ms

측정 도구:

  • Chrome DevTools → Network 탭 → 각 요청의 Timing 탭
  • performance.getEntriesByType('resource') API

TTFB 판단 기준:

  • < 200ms: 양호
  • 200~600ms: 보통
  • 600ms~2s: 느림 (서버 최적화 필요)
  • 2s: 심각 (DB 쿼리 또는 서버 로직 점검 필수)

서버 사이드 구간

API 요청 수신
  ├─[K] 미들웨어 처리 ───── ?ms
  ├─[L] 비즈니스 로직 ───── ?ms
  ├─[M] DB 쿼리 실행 ────── ?ms  ← 빈번한 병목
  ├─[N] 외부 API 호출 ───── ?ms
  └─[O] 응답 직렬화 ─────── ?ms

측정 방법:

서버 코드에 타이밍 로그를 삽입한다:

# Python/FastAPI 예시
import time

@app.get("/api/dashboard")
async def get_dashboard():
    timings = {}
    t0 = time.perf_counter()

    t1 = time.perf_counter()
    users = await db.query("SELECT ...")
    timings["db_users"] = (time.perf_counter() - t1) * 1000

    t1 = time.perf_counter()
    orders = await db.query("SELECT ...")
    timings["db_orders"] = (time.perf_counter() - t1) * 1000

    t1 = time.perf_counter()
    result = aggregate(users, orders)
    timings["processing"] = (time.perf_counter() - t1) * 1000

    timings["total"] = (time.perf_counter() - t0) * 1000
    logger.info(f"Dashboard API timings: {timings}")
    return result
// Node.js/Express 예시
app.get('/api/dashboard', async (req, res) => {
  const timings: Record<string, number> = {};
  const t0 = performance.now();

  let t1 = performance.now();
  const users = await db.query('SELECT ...');
  timings.db_users = performance.now() - t1;

  t1 = performance.now();
  const orders = await db.query('SELECT ...');
  timings.db_orders = performance.now() - t1;

  t1 = performance.now();
  const result = aggregate(users, orders);
  timings.processing = performance.now() - t1;

  timings.total = performance.now() - t0;
  console.log('Dashboard API timings:', timings);
  res.json(result);
});

DB 쿼리 구간

측정 방법:

  • EXPLAIN ANALYZE (PostgreSQL) / EXPLAIN (MySQL)
  • ORM 쿼리 로깅 활성화
  • Slow query log 확인
-- PostgreSQL
EXPLAIN ANALYZE SELECT * FROM orders WHERE date > '2026-01-01';

-- 결과에서 확인:
-- Planning Time: 0.5ms
-- Execution Time: 2680.3ms  ← 실제 실행 시간
-- Seq Scan vs Index Scan 여부

Phase 4: 병목 판단

비율 분석

모든 구간의 측정값을 수집한 뒤, 전체 대비 비율을 계산한다.

전체 ????ms 중:

  구간 이름        시간      비율
  ─────────────  ────────  ──────
  DB쿼리(orders)  2680ms   62%   ← 1순위 병목
  렌더링          1390ms   32%   ← 2순위 병목
  DB쿼리(users)    124ms    3%
  기타              138ms    3%

판단 프레임워크

[규칙 1] 80/20 법칙
  → 전체 시간의 80%를 차지하는 상위 구간에 집중

[규칙 2] 절대값 기준 병행
  → 비율이 높아도 절대값이 작으면 개선 효과 미미
  → 예: 전체 200ms 중 50%인 100ms → 최적화 가치 낮음

[규칙 3] 개선 가능성
  → 물리적 한계(네트워크 지연)보다 쿼리/로직 최적화가 개선폭 큼
  → 쉽게 큰 개선이 가능한 구간을 우선 공략

[규칙 4] 의존관계
  → 직렬 체인의 시작점(root cause)부터 해결
  → 예: API가 느리면 → 프론트 렌더도 늦게 시작 → API부터 해결

개선 유형별 가이드

병목 위치 일반적 원인 개선 방향
DB 쿼리 인덱스 부재, N+1, 풀스캔 인덱스 추가, 쿼리 최적화, 페이지네이션
API 서버 로직 동기 처리, 불필요한 연산 비동기화, 캐싱, 로직 간소화
네트워크 전송 응답 크기 과다 gzip, 필드 선택, 페이지네이션
JS 번들 번들 크기 과다 코드 스플리팅, tree-shaking, lazy load
렌더링 과다 리렌더, 대량 DOM 가상화, 메모이제이션, 리렌더 방지
외부 API 응답 지연 캐싱, 타임아웃 설정, 병렬 호출

Output: 병목 보고서

측정 완료 후 아래 형식으로 결과를 보고한다.

┌─────────────────────────────────────────────────────┐
│ 성능 병목 분석 보고서                                  │
├─────────────────────────────────────────────────────┤
│ 시나리오: [측정 대상]                                  │
│ 날짜: [측정 일시]                                     │
│ 환경: [브라우저, 디바이스, 네트워크, 데이터 규모]         │
│ 반복 횟수: [N회 평균]                                  │
├─────────────────────────────────────────────────────┤
│ 구간               │ 평균     │ 비율   │ 비고        │
│ ──────────────────│─────────│───────│────────────│
│ [구간 A]           │ ????ms  │ ??%   │            │
│ [구간 B]           │ ????ms  │ ??%   │ ← 1순위    │
│ [구간 C]           │ ????ms  │ ??%   │            │
│ ...                │         │       │            │
│────────────────────│─────────│───────│────────────│
│ 합계               │ ????ms  │ 100%  │            │
├─────────────────────────────────────────────────────┤
│ 병목 우선순위                                         │
│ 1. [구간명] — [원인 추정] — [개선 방향]                 │
│ 2. [구간명] — [원인 추정] — [개선 방향]                 │
└─────────────────────────────────────────────────────┘

재측정 (변경 후)

performance-improve-process 에서 변경을 적용한 뒤 재측정을 요청하면, 동일한 시나리오와 환경에서 측정을 반복하여 이전 baseline과 비교한다.

┌──────────────────────────────────────────────────────┐
│ 변경 전후 비교                                         │
├──────────────────────────────────────────────────────┤
│ 구간               │ Before   │ After    │ 변화      │
│ ──────────────────│─────────│─────────│──────────│
│ DB쿼리(orders)     │ 2680ms  │  340ms  │ -2340ms  │
│ 렌더링             │ 1390ms  │ 1390ms  │ 변화없음   │
│────────────────────│─────────│─────────│──────────│
│ 합계               │ 4332ms  │ 1992ms  │ -2340ms  │
└──────────────────────────────────────────────────────┘
Weekly Installs
1
First Seen
12 days ago
Installed on
amp1
cline1
openclaw1
opencode1
cursor1
kimi-cli1