---
id: web-performance-core-vitals
title: Core Web Vitals — LCP / INP / CLS
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [web, performance, core-web-vitals, vibe-coding]
tech_stack: { language: "TS / Web", applicable_to: ["Frontend"] }
applied_in: []
aliases: [LCP, INP, CLS, FCP, TTFB, web vitals, lighthouse, Core Web Vitals]
---

# Core Web Vitals

> Google ranking 영향. **LCP (로드) / INP (반응) / CLS (안정)** 3개. 좋음: LCP < 2.5s, INP < 200ms, CLS < 0.1. **PageSpeed / web-vitals JS / RUM**.

## 📖 핵심 개념
- LCP (Largest Contentful Paint): 가장 큰 콘텐츠 그려지는 시간.
- INP (Interaction to Next Paint): 사용자 입력 → 다음 paint (FID 후속, 2024 표준).
- CLS (Cumulative Layout Shift): 화면 흔들림.
- TTFB / FCP: 보조 지표.

## 💻 코드 패턴

### 측정 (web-vitals)
```ts
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function send(metric: { name: string; value: number; id: string; navigationType: string }) {
  navigator.sendBeacon('/api/vitals', JSON.stringify(metric));
}

onLCP(send);
onINP(send);
onCLS(send);
onFCP(send);
onTTFB(send);
```

### LCP 최적화
```html
<!-- 1. Preload hero image -->
<link rel="preload" as="image" href="/hero.avif" 
      imagesrcset="/hero-800.avif 800w, /hero-1600.avif 1600w" 
      imagesizes="100vw" type="image/avif">

<!-- 2. priority -->
<img src="/hero.avif" fetchpriority="high" alt="hero">

<!-- 3. inline critical CSS -->
<style>/* above-the-fold only */</style>

<!-- 4. defer JS -->
<script src="app.js" defer></script>
```

```tsx
// Next.js
<Image src="/hero.jpg" priority alt="hero" />
```

### INP 최적화
```ts
// 무거운 작업 → background / yield
async function heavyWork() {
  const items = await fetchBigData();
  for (let i = 0; i < items.length; i++) {
    process(items[i]);
    if (i % 100 === 0) await scheduler.yield(); // 또는 setTimeout(0)
  }
}

// 또는 isInputPending
async function processChunk() {
  while (more && !navigator.scheduling?.isInputPending()) {
    process(items.shift()!);
  }
  if (more) requestIdleCallback(processChunk);
}
```

```tsx
// React useDeferredValue / startTransition
import { useDeferredValue, useTransition } from 'react';

function Search({ query }) {
  const deferred = useDeferredValue(query);
  // 무거운 검색 결과 — input 안 막힘
  const results = useMemo(() => search(deferred), [deferred]);
}

function Tabs() {
  const [tab, setTab] = useState('home');
  const [, startTransition] = useTransition();
  const onTab = (t) => startTransition(() => setTab(t));
}
```

### CLS 최적화
```html
<!-- 이미지 dimensions 명시 -->
<img src="..." width="800" height="600" alt="...">

<!-- 광고 영역 reserve -->
<div style="min-height: 250px"><!-- ad slot --></div>

<!-- Font swap 없이 -->
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>
```

```css
/* font-display swap 보다 fallback similar metrics */
@font-face {
  font-family: Inter;
  src: url('/Inter.woff2') format('woff2');
  font-display: optional;
  size-adjust: 100%;
  ascent-override: 90%;
}
```

### Lazy load offscreen
```html
<img loading="lazy" decoding="async" src="..." alt="...">
<iframe loading="lazy" src="..."></iframe>
```

### Code split
```ts
// React lazy
const Heavy = lazy(() => import('./Heavy'));

// Route-based
const router = createBrowserRouter([
  { path: '/admin', lazy: () => import('./AdminRoute') },
]);
```

### Resource hints
```html
<link rel="dns-prefetch" href="//api.example.com">
<link rel="preconnect" href="//fonts.gstatic.com" crossorigin>
<link rel="prefetch" href="/next-page.js">
<link rel="modulepreload" href="/critical-module.js">
```

### Server timing (TTFB)
```ts
// Express
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    res.setHeader('Server-Timing', `total;dur=${Date.now() - start}`);
  });
  next();
});
```

→ Browser DevTools Network → Server Timing.

### Lighthouse CI
```yaml
- uses: treosh/lighthouse-ci-action@v11
  with:
    urls: |
      https://example.com/
      https://example.com/products
    budgetPath: ./budget.json
    uploadArtifacts: true
```

```json
// budget.json
[{
  "path": "/*",
  "timings": [
    { "metric": "interactive", "budget": 3000 },
    { "metric": "first-contentful-paint", "budget": 1500 }
  ],
  "resourceSizes": [
    { "resourceType": "script", "budget": 200 }
  ]
}]
```

### RUM (Real User Monitoring)
```ts
// 실제 사용자 측정 (lab 보다 진실)
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(({ name, value, rating }) => {
  analytics.track('webvital', { name, value, rating });
});
```

→ Sentry / Datadog / Vercel Speed Insights / Cloudflare RUM.

### React 최적화 패턴
```tsx
// React Compiler (자동 memo)
// 또는 수동
const Memo = memo(Heavy);
const ids = useMemo(() => items.map(i => i.id), [items]);

// 큰 list = virtualization
import { useVirtualizer } from '@tanstack/react-virtual';
```

## 🤔 의사결정 기준
| 지표 | 우선 액션 |
|---|---|
| LCP > 2.5s | Image preload + format + CDN |
| INP > 200ms | JS yield + React transition |
| CLS > 0.1 | Image dimensions + font preload + reserved space |
| TTFB > 600ms | CDN + edge function |
| Bundle 큼 | Code split + tree shake |

## ❌ 안티패턴
- **Lab metric 만 (Lighthouse)**: 사용자 hardware 다름. RUM 같이.
- **JS heavy 사용 + INP 무시**: 느림.
- **모든 image lazy**: LCP 깨짐.
- **Font @font-face fallback X**: FOIT (보이지 않음).
- **광고 / iframe 동적 삽입**: CLS.
- **`document.write()`**: parser block.
- **큰 third-party JS**: bundle 폭발.
- **HTTP/1.1 prod**: 6 connection 한계. HTTP/2 / HTTP/3.

## 🤖 LLM 활용 힌트
- web-vitals + analytics → 진짜 사용자 데이터.
- Image (preload + priority + WebP) + JS (yield + split) + Font (preload).
- Lighthouse CI 가 PR 회귀 차단.

## 🔗 관련 문서
- [[Frontend_Image_Optimization]]
- [[React_Code_Splitting]]
- [[Web_HTTP_Cache_Headers]]
