필사 모드: 현대 웹 성능의 과학 심화 가이드 — Core Web Vitals, INP, LCP, CLS, RUM, Lighthouse, Critical Rendering Path, Speculation Rules까지 (2025)
한국어> **TL;DR** — 2024년은 웹 성능의 지각변동이었다. **INP(Interaction to Next Paint)**가 FID를 대체하며 Core Web Vitals 3대 지표가 **LCP · CLS · INP**로 재편됐고, **Speculation Rules API**(Chrome 121), **Partial Prerendering**(Next.js 14), **HTTP/3 QUIC**(전체 트래픽의 30% 돌파), **Early Hints**(103 Status)가 같은 해에 표준화·일반화됐다. 이 글은 '왜 내 사이트가 느린가'에 답하기 위해 **Critical Rendering Path** 원리부터 **Long Task 쪼개기**, **RUM vs Lab Data** 차이, **Lighthouse가 자주 틀리는 이유**, **Islands · Resumability**(Qwik), **Image/Font 최적화**(AVIF, font-display: optional), **2025년 성능 도구 스택**(Vercel Analytics, SpeedCurve, PerfEtto)까지 — 현대 웹 성능의 전 지형도를 정리한다.
왜 웹 성능이 다시 '뜨거운 주제'가 됐는가
웹 성능은 2010년대 초 **YSlow**(Yahoo)와 **PageSpeed Insights**(Google)가 대중화한 뒤 한동안 '인프라 팀의 체크리스트' 수준에 머물렀다. 그런데 2020년 Google이 **Core Web Vitals**를 검색 랭킹 신호로 공식 발표하고, 2024년 **INP**가 FID를 대체하며 — 성능은 **검색 순위 · 광고 전환율 · 사용자 이탈률**을 직접 좌우하는 비즈니스 지표가 됐다.
숫자로 보면 이렇다:
- **Amazon**: 페이지 로드 100ms 지연당 매출 1% 감소 (2006년 기준, 2024년엔 더 민감)
- **Walmart**: LCP 1초 단축 → 전환율 2% 향상
- **BBC**: 1초 지연당 10% 이탈 증가
- **Vodafone**: LCP 31% 개선 → 판매 전환 8% 증가 (2021 케이스스터디)
2024년 **Chrome UX Report**(CrUX, 실제 사용자 데이터)에 따르면, 전 세계 상위 100만 사이트 중 Core Web Vitals 3개를 모두 통과한 사이트는 **42%**뿐이다. 반면 React/Vue/Angular 같은 SPA 프레임워크를 쓰는 사이트의 통과율은 **28%**로 정적 사이트(65%)보다 크게 낮다. 성능은 '프레임워크 선택의 대가'이기도 하다.
이 글의 목적은 **Core Web Vitals 3개 지표의 정의와 측정법**, 그리고 **왜 같은 코드가 Lab에선 빠르고 Field에선 느린지**, **어떻게 Long Task와 Layout Shift를 추적·수정하는지**를 원리부터 실무까지 한 호흡에 정리하는 것이다. 성능은 '기법 하나'가 아니라 '**렌더링 파이프라인 전체의 이해**'에서 나온다.
브라우저 렌더링 파이프라인 — 픽셀까지 도달하는 여정
웹 성능을 논하려면 브라우저가 HTML을 받아 픽셀을 그리기까지 거치는 **Critical Rendering Path**를 알아야 한다. 이 경로에서 시간이 새는 모든 지점이 '성능 문제'의 원인이다.
1. Navigation — URL 입력 / 링크 클릭
↓
2. DNS Lookup — example.com → 93.184.216.34
↓
3. TCP + TLS Handshake — 3-way + SSL (HTTP/1.1: ~300ms, HTTP/3: ~100ms)
↓
4. HTTP Request — GET /
↓
5. TTFB — Time To First Byte (서버 응답)
↓
6. HTML Parsing — DOM 트리 구축 (파싱 중 외부 리소스 요청)
↓
7. CSSOM Construction — CSS 파싱, CSSOM 트리
↓
8. Render Tree — DOM + CSSOM → 화면에 그릴 노드만
↓
9. Layout (Reflow) — 각 노드의 위치/크기 계산
↓
10. Paint — 픽셀 정보 생성 (레이어 단위)
↓
11. Composite — GPU에서 레이어 합성
↓
12. 화면 표시 — 사용자가 보는 최초 픽셀 (FCP)
각 단계에서 일어나는 핵심 병목:
1. **DNS + TCP + TLS** — 첫 바이트를 받기까지 RTT(Round Trip Time) 3-4회. 이걸 **HTTP/3 QUIC**(0-RTT resumption)이 공격한다.
2. **TTFB** — 서버 응답 시간. SSR이면 DB + 렌더링, 정적 파일이면 CDN 캐시 히트 여부가 결정.
3. **HTML Parsing Blocking** — `<script>` 태그는 기본적으로 파싱을 멈춘다. `async`/`defer`로 해소.
4. **CSSOM Blocking** — CSS는 **렌더링 블로킹 리소스**. CSS 로딩이 끝날 때까지 Render Tree 안 만들어짐.
5. **Layout** — `offsetHeight` 읽기 같은 **동기 강제 레이아웃**(Forced Reflow)이 성능 킬러.
6. **Paint / Composite** — `will-change: transform`, `contain: layout`으로 GPU 레이어 분리 유도.
React/Vue 같은 JS 프레임워크를 쓰면 여기에 **JS 다운로드 · 파싱 · 실행 · Hydration**이 추가된다. 이 추가 단계가 SPA가 Core Web Vitals에서 불리한 근본 이유다.
Core Web Vitals 3대 지표 (2024-2025)
Google은 2020년 **Core Web Vitals**를 사용자 경험의 3대 축으로 정의했다:
- **로딩 (Loading)** — LCP
- **상호작용성 (Interactivity)** — FID → **INP** (2024.03 교체)
- **시각적 안정성 (Visual Stability)** — CLS
LCP — Largest Contentful Paint (로딩)
**정의**: 뷰포트에 보이는 가장 큰 콘텐츠 요소(이미지, 비디오 포스터, 블록 텍스트)가 화면에 그려지는 시점.
**기준**: 2.5초 이하 = Good, 2.5-4.0초 = Needs Improvement, 4.0초 초과 = Poor.
**측정**: `LargestContentfulPaint` PerformanceObserver API.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP element:', entry.element)
console.log('LCP time:', entry.startTime)
console.log('LCP render time:', entry.renderTime)
console.log('LCP size:', entry.size)
}
}).observe({ type: 'largest-contentful-paint', buffered: true })
**LCP가 느려지는 주범 4가지**:
1. **느린 TTFB** — 서버 응답이 1초 걸리면 LCP 2.5초 통과는 불가능.
2. **렌더링 블로킹 리소스** — `<link rel="stylesheet">` 지연이 LCP를 그대로 지연시킴.
3. **리소스 로드 시간** — LCP 이미지가 lazy-load되면 안 됨. `fetchpriority="high"` 사용.
4. **클라이언트 사이드 렌더링** — React가 JS 다운받아 Hydration 끝나야 LCP 요소가 DOM에 올라옴.
**LCP 최적화 체크리스트**:
- LCP 이미지에 `fetchpriority="high"` + `loading="eager"` + `decoding="async"`
- `<link rel="preload" as="image" href="/hero.webp" imagesrcset="..." fetchpriority="high">`
- Above-the-fold 콘텐츠는 인라인 CSS로
- 폰트는 `font-display: optional` 또는 `swap`
- CDN + HTTP/3 + Brotli 압축
- SSR 또는 SSG (CSR 피하기)
CLS — Cumulative Layout Shift (시각적 안정성)
**정의**: 페이지 로드 중 예기치 않게 레이아웃이 이동한 정도. 이동한 요소의 '영향 비율' × '이동 거리'를 누적.
**기준**: 0.1 이하 = Good, 0.1-0.25 = Needs Improvement, 0.25 초과 = Poor.
**CLS 공식**: `impact fraction × distance fraction`
- Impact Fraction: 뷰포트 대비 이동한 요소가 차지하는 비율
- Distance Fraction: 이동한 거리 / 뷰포트 크기
**CLS 주범 5가지**:
1. **크기 지정 없는 이미지** — `<img>`에 width/height 누락 → 이미지 로드 후 레이아웃 밀림.
2. **크기 지정 없는 광고/임베드** — AdSense, YouTube embed, iframe.
3. **FOIT/FOUT** — 폰트 로드 전후 글꼴 변경으로 텍스트 높이 변화.
4. **동적 콘텐츠 주입** — 상단에 배너/알림 삽입.
5. **Web Font 로드 지연** — `font-display: swap`이 CLS를 일으킴 (아이러니).
**CLS 최적화**:
<!-- 이미지 크기 명시 -->
<!-- CSS aspect-ratio로 공간 예약 -->
.embed { aspect-ratio: 16 / 9; }
<!-- Font fallback 크기 조정 -->
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: optional; /* CLS 방지, 폰트 로드 안 되면 fallback 유지 */
size-adjust: 107%; /* fallback과 크기 일치시키기 */
}
<!-- Skeleton / Placeholder -->
INP — Interaction to Next Paint (2024.03 공식화)
**정의**: 사용자가 클릭/탭/키보드 입력을 했을 때, 다음 프레임이 그려지기까지의 시간 — **페이지 전체 수명 동안 최악(가장 느린) 상호작용** 기준(98 percentile 가까움).
**기준**: 200ms 이하 = Good, 200-500ms = Needs Improvement, 500ms 초과 = Poor.
**INP가 FID를 왜 대체했나**:
- **FID(First Input Delay)**는 첫 입력의 '시작 지연'만 측정 (입력 → 핸들러 시작).
- 하지만 실제 UX 문제는 **입력 후 → 화면 반영까지** 전체 시간. 긴 JS 작업, 리렌더링, Layout, Paint 모두 포함해야 함.
- FID는 대부분 사이트가 100ms 이내 통과 → 변별력 없음. INP는 훨씬 엄격.
**INP 공식** (간소화):
INP = max(interactions) where interaction_time =
(input delay) + (processing time) + (presentation delay)
**INP 측정**:
onINP((metric) => {
console.log('INP:', metric.value, 'ms')
console.log('Attribution:', metric.attribution)
// attribution: { interactionType, eventTarget, loafTime, ... }
})
**INP가 나빠지는 주범 7가지**:
1. **Long Task** — 50ms 초과 메인 스레드 블로킹 JS.
2. **큰 React 컴포넌트 리렌더** — state 업데이트 → 전체 subtree 재렌더.
3. **동기 네트워크 요청** — fetch를 event handler에서 await.
4. **대규모 DOM** — 수천 개 노드의 Layout 재계산.
5. **heavy CSS selector** — `:has()`, 복잡한 nth-child.
6. **동기 third-party 스크립트** — 광고, 분석 도구.
7. **ResizeObserver/MutationObserver 폭주** — 콜백이 동기 Layout 유발.
**INP 최적화 — Long Task 쪼개기**:
// 나쁨 — 1000개 아이템을 한 번에 처리 (800ms Long Task)
function processItems(items) {
items.forEach(item => expensiveWork(item))
}
// 좋음 — scheduler.yield() (2024 표준화)
async function processItems(items) {
for (const item of items) {
expensiveWork(item)
await scheduler.yield() // 메인 스레드 양보
}
}
// 대체 — setTimeout으로 양보 (구형 브라우저)
function processItemsYield(items, i = 0) {
const deadline = performance.now() + 10
while (i < items.length && performance.now() < deadline) {
expensiveWork(items[i++])
}
if (i < items.length) {
setTimeout(() => processItemsYield(items, i), 0)
}
}
**React INP 특화 — useTransition**:
function SearchBox() {
const [isPending, startTransition] = useTransition()
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
function handleChange(e) {
setQuery(e.target.value) // 긴급 업데이트 (input value)
startTransition(() => {
setResults(expensiveSearch(e.target.value)) // 낮은 우선순위
})
}
// ...
}
RUM vs Lab Data — 왜 Lighthouse는 자주 틀리는가
웹 성능 데이터에는 크게 두 종류가 있다:
Lab Data (합성 모니터링, Synthetic)
- **Lighthouse**, **WebPageTest**, **PageSpeed Insights**(Lab 탭)
- 통제된 환경에서 1회 측정
- 장점: 재현 가능, 회귀 탐지 쉬움, CI/CD에 통합 가능
- 단점: 실제 사용자 네트워크/디바이스/상호작용과 다름
Field Data / RUM (Real User Monitoring)
- **Chrome UX Report**(CrUX), **Vercel Analytics**, **Sentry Performance**, **New Relic Browser**, **SpeedCurve**
- 실제 사용자 브라우저에서 Performance API로 수집
- 장점: 실제 UX 반영, 디바이스/네트워크 분포 반영
- 단점: 노이즈 많음, 디버깅 어려움 (어떤 상호작용이 INP 80ms인지 추적 필요)
**왜 Lighthouse 점수와 실제 점수가 다른가**:
1. Lighthouse는 **Moto G Power + 4G 시뮬레이션** 고정. 실제 유저는 iPhone 15 + 5G일 수도.
2. Lighthouse는 페이지 로드만 측정 → **INP는 세션 전체 상호작용 기반** → Lab에선 측정 안 됨.
3. Lighthouse는 **1회 측정** → 실제는 분포. Google은 CrUX의 **75 percentile**을 기준 삼음.
4. Lighthouse는 **쿠키/로그인 없음** → 실제는 인증된 사용자 화면이 다름.
5. Lighthouse는 **viewport 고정 360×640** → 실제 디바이스 폭 분포와 다름.
**권장 전략**:
- **Lab Data (Lighthouse)**: PR마다 회귀 테스트 (Lighthouse CI), 상한선 설정
- **RUM**: 프로덕션 모니터링, p75 / p95 지표 추적, 국가별/디바이스별 드릴다운
- 두 데이터가 일치하지 않을 때 **RUM을 믿어라**
2025년 RUM 스택
| 제공자 | 특징 | 가격(참고) |
|---|---|---|
| **Vercel Speed Insights** | Next.js 통합, Core Web Vitals + Custom Events | 10,000 events 무료 |
| **Google CrUX** | 월별 공개 데이터, BigQuery | 무료 |
| **Sentry Performance** | 에러 + 성능 통합 | $26/mo부터 |
| **SpeedCurve** | 경쟁사 비교, 커스텀 대시보드 | $149/mo부터 |
| **New Relic Browser** | APM 통합 | Free tier |
| **Cloudflare Web Analytics** | 서버리스, 프라이버시 우선 | 무료 |
| **Pingdom RUM** | 지리적 분포 강점 | $14.95/mo부터 |
리소스 우선순위와 로딩 전략
브라우저가 100개 리소스를 동시에 받는 건 아니다. **Fetch Priority**, **Preload Scanner**, **HTTP/2 Priority**, **HTTP/3 Priority**가 복잡하게 얽혀 결정한다.
Resource Hints — 브라우저에 미리 알리기
<!-- DNS 미리 해석 -->
<!-- 연결(DNS + TCP + TLS) 미리 열기 -->
<!-- 리소스 미리 다운로드 (현재 페이지용) -->
<!-- 다음 페이지 미리 가져오기 (low priority) -->
<!-- 전체 페이지 미리 렌더링 (Speculation Rules로 대체됨) -->
Fetch Priority API (Chrome 101+, Safari 17+, Firefox 132+)
<!-- LCP 이미지 -->
<!-- 아래 스크롤 영역 이미지 -->
<!-- fetch() API -->
fetch('/critical.json', { priority: 'high' })
fetch('/analytics.json', { priority: 'low' })
Speculation Rules API — 2024 표준화
기존 `<link rel="prefetch">`와 `prerender`의 한계를 넘어, **CSS selector 기반**으로 사용자가 **방문할 가능성이 높은 링크**를 미리 prerender.
{
"prerender": [{
"urls": ["/product/1", "/product/2"],
"eagerness": "moderate"
}],
"prefetch": [{
"where": { "href_matches": "/product/*" },
"eagerness": "conservative"
}]
}
**eagerness 레벨**:
- `immediate` — 즉시 (aggressive)
- `eager` — 힌트 발견 즉시
- `moderate` — 링크에 hover/touch 등
- `conservative` — 링크 클릭 직전
Chrome 121+, **실질적 LCP 0ms 달성**이 가능. (prerender된 페이지 전환 시 즉시 표시)
Early Hints (HTTP 103)
서버가 최종 응답(200) 전에 **103 Early Hints** 상태코드로 `Link: </main.css>; rel=preload` 힌트를 먼저 보내는 기술.
HTTP/1.1 103 Early Hints
Link: </main.css>; rel=preload; as=style
Link: </hero.webp>; rel=preload; as=image
HTTP/1.1 200 OK
Content-Type: text/html
...
Cloudflare, Fastly, Next.js(14.1+)가 지원. TTFB를 기다리지 않고 핵심 리소스를 미리 받음 → LCP 200-400ms 단축 가능.
이미지 최적화 — 모든 웹사이트 대역폭의 50%
Chrome UX Report에 따르면 평균 웹페이지의 **이미지는 전체 바이트의 48%**. 이미지 최적화 하나로 LCP를 1초 이상 단축할 수 있다.
포맷 선택
| 포맷 | 지원 | 특징 | 압축률 |
|---|---|---|---|
| **JPEG** | 100% | 사진, 손실 압축 | 기준 |
| **PNG** | 100% | 투명도, 무손실 | 크기 큼 |
| **WebP** | 97% (IE 제외) | Google, 25-35% 작음 | JPEG 대비 25-35% 작음 |
| **AVIF** | 93% | AV1 코덱 기반, 50% 작음 | JPEG 대비 50% 작음 |
| **JPEG XL** | Safari only (실험적) | 미래 표준 후보 | AVIF와 비슷 |
**2025년 권장**: `<picture>`로 AVIF → WebP → JPEG fallback.
Responsive Images — srcset + sizes
src="/hero-800.jpg"
srcset="/hero-400.jpg 400w,
/hero-800.jpg 800w,
/hero-1600.jpg 1600w,
/hero-2400.jpg 2400w"
sizes="(max-width: 768px) 100vw,
(max-width: 1200px) 50vw,
33vw"
width="800" height="600"
alt="Hero"
/>
현대 CDN — 자동 포맷 변환
- **Cloudinary** — URL 기반 변환 (`w_800,f_auto,q_auto`)
- **imgix** — 동적 파라미터
- **Cloudflare Images** — $5/월 100k 이미지
- **Next.js Image** — `<Image />` 컴포넌트 (AVIF/WebP 자동)
- **Vercel Image Optimization** — 빌드 타임 + 온디맨드
Lazy Loading
<!-- 기본 lazy loading (Chrome 77+) -->
<!-- IntersectionObserver 기반 커스텀 -->
const images = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
}, { rootMargin: '200px' }) // 200px 전에 미리 로드
images.forEach(img => observer.observe(img))
**주의**: LCP 이미지는 **절대 lazy loading 하지 말 것**. `loading="eager"` + `fetchpriority="high"`.
폰트 최적화 — FOIT/FOUT와 CLS의 주범
웹 폰트는 다운로드 전까지 텍스트가 안 보이거나(FOIT), fallback으로 보이다 갑자기 바뀐다(FOUT) — 모두 사용자 경험을 해친다.
font-display 전략
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: swap; /* FOUT: fallback 보여주다 폰트 로드되면 교체 — CLS 발생 */
font-display: optional; /* 100ms 안에 안 오면 fallback 유지 — CLS 0 */
font-display: block; /* 3초까지 기다림 — FOIT */
font-display: fallback; /* 100ms + 3초 */
}
**권장**: LCP 텍스트는 `font-display: optional` + **size-adjust**로 fallback 크기 맞추기.
Font Fallback 크기 맞추기 (size-adjust)
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: optional;
}
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107.4%; /* Arial을 Inter 크기로 조정 */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter-fallback', sans-serif;
}
**도구**: [Font Style Matcher](https://meowni.ca/font-style-matcher/), **Fontaine**(Vite plugin).
Preload + Subset
<!-- 필수 폰트 preload -->
**Subset**: 한국어 폰트(Noto Sans KR, Pretendard)는 전체 글리프가 3-10MB. **unicode-range**로 한국어 영역만 로드.
@font-face {
font-family: 'Pretendard';
src: url('Pretendard-KR.woff2') format('woff2');
unicode-range: U+AC00-D7A3, U+1100-11FF, U+3130-318F; /* 한글 영역만 */
}
JavaScript 로딩 전략
JS는 웹 성능의 가장 큰 적이자 친구다. 현대 SPA는 평균 **400KB gzipped JS**를 로드 — 모바일에서 파싱/컴파일만 800ms+.
async vs defer vs module
<!-- Blocking (절대 쓰지 말 것) -->
<!-- Async: 다운로드 완료 즉시 실행, 순서 보장 X -->
<!-- Defer: 다운로드 병렬, HTML 파싱 끝나고 실행, 순서 보장 -->
<!-- Module: 기본 defer, 순서 보장 -->
Code Splitting
Webpack/Rollup/esbuild 모두 지원. 라우트/컴포넌트 단위로 JS를 쪼개 초기 로드량 감소.
// React.lazy
const Heavy = React.lazy(() => import('./HeavyComponent'))
function App() {
return (
)
}
// Next.js dynamic
const Chart = dynamic(() => import('./Chart'), { ssr: false })
Tree Shaking
ESM import 구문을 분석해 사용 안 한 코드를 제거. **side-effect free** 선언 필수.
// package.json
{
"sideEffects": false,
"exports": {
".": "./dist/index.js"
}
}
Third-Party Scripts — 가장 큰 적
Google Analytics, Facebook Pixel, Intercom 등 3rd party 스크립트 하나가 INP 500ms를 만든다.
**Partytown** (Builder.io) — 3rd party 스크립트를 **Web Worker**에서 실행.
**Next.js Script 컴포넌트**:
Long Task와 메인 스레드 예산
**Long Task**: 50ms 초과로 메인 스레드를 블로킹하는 JS 작업. INP를 망치는 최대 원인.
Long Task 감지
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long Task:', entry.duration, 'ms', entry.attribution)
}
}).observe({ type: 'longtask', buffered: true })
Long Animation Frames (LoAF) — 2024 신규
Long Task의 한계를 보완한 새 API. 프레임 단위로 **렌더링 + 스크립트 + Layout + Paint** 시간 분석.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LoAF:', {
duration: entry.duration,
scripts: entry.scripts, // 어떤 스크립트가 얼마나 걸렸나
blockingDuration: entry.blockingDuration,
})
}
}).observe({ type: 'long-animation-frame', buffered: true })
scheduler.yield() — 메인 스레드 양보
async function bulkWork(items) {
for (const item of items) {
process(item)
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield() // 입력 대기 중이면 즉시 양보
}
}
}
Web Worker — CPU 오프로딩
// main.js
const worker = new Worker('/worker.js')
worker.postMessage({ cmd: 'hash', data: largeString })
worker.onmessage = (e) => console.log('hashed:', e.data)
// worker.js
self.onmessage = async (e) => {
const buf = new TextEncoder().encode(e.data.data)
const hash = await crypto.subtle.digest('SHA-256', buf)
self.postMessage(Array.from(new Uint8Array(hash)))
}
Hydration 문제 — SPA의 근본 비용
React/Vue/Angular 같은 SPA는 **Hydration** — 서버에서 렌더된 HTML에 JS를 붙여 상호작용 가능하게 만드는 과정 — 이 INP를 망친다.
Hydration의 6단계 비용 (Addy Osmani)
1. JS 다운로드 — 200-500KB gzipped
2. JS 파싱 + 컴파일
3. React Tree 재구성 (서버 HTML 무관)
4. 이벤트 리스너 부착
5. useState/useEffect 실행
6. Commit
모바일에서 이 전체가 **1-3초** 걸린다. 그동안 사용자 클릭은 무시.
해결책 1: Partial Hydration (Islands)
**Astro**, **Marko**, **Fresh**(Deno)의 접근. 페이지 대부분은 정적 HTML, **상호작용이 필요한 부분만** 섬(island)으로 Hydrate.
// Astro 파일
해결책 2: Resumability (Qwik)
Qwik의 혁신: **Hydration 자체를 없애고**, HTML에 serialize된 상태를 사용자 상호작용 시점에 재개(resume).
// Qwik 컴포넌트
export default component$(() => {
const count = useSignal(0)
return (
{count.value}
)
})
HTML에 핸들러 URL을 임베드:
**JS 0KB로 시작**, 클릭 시점에만 해당 핸들러 JS를 lazy load. **TTI = LCP**를 실현.
해결책 3: React Server Components + Streaming
React 18 + Next.js 14. 서버 컴포넌트는 JS 번들에 포함 안 됨 → 클라이언트 번들 감소. Streaming으로 `<Suspense>` 경계까지 먼저 렌더 → LCP 단축.
해결책 4: Selective Hydration
React 18 기본 기능. `<Suspense>` 경계를 우선순위 기반으로 Hydrate. 사용자가 클릭한 영역 우선 처리.
HTTP/3, QUIC, 그리고 네트워크 계층
2024년 HTTP/3가 전체 트래픽의 **30%**를 넘어섰다(W3Techs). TCP 위에서 동작하는 HTTP/2와 달리, HTTP/3는 **UDP 기반 QUIC 프로토콜** 위에서 동작한다.
HTTP/1.1 → HTTP/2 → HTTP/3
| 버전 | 기반 | 멀티플렉싱 | Head-of-Line Blocking | 0-RTT |
|---|---|---|---|---|
| HTTP/1.1 | TCP | X (6 connections/origin) | Yes | No |
| HTTP/2 | TCP | Yes | **TCP 수준** Yes | No |
| HTTP/3 | UDP (QUIC) | Yes | No | **Yes** |
**HTTP/3 핵심 이득**:
1. **0-RTT Resumption** — 이전 연결 키 재사용, 첫 요청부터 데이터 전송
2. **Connection Migration** — WiFi → 셀룰러 전환해도 연결 유지 (Connection ID)
3. **HOL Blocking 해소** — 한 스트림 패킷 손실이 다른 스트림 차단 안 함
4. **암호화 필수** — TLS 1.3 내장, 평문 불가
실제 측정 (Cloudflare 2024 자료):
- Google Search: HTTP/3로 중앙값 응답 시간 3% 단축, 상위 10% 구간 10% 단축
- Facebook: 동영상 rebuffering 20% 감소
- Akamai: TTFB 모바일 12% 개선
CDN + Edge 컴퓨팅
**Cloudflare**, **Fastly**, **AWS CloudFront**, **Vercel Edge Network**, **Bunny.net**. 콘텐츠를 사용자 가까이 캐시해 지연 최소화.
**2025년 트렌드**:
- **Edge Workers** — Cloudflare Workers, Deno Deploy, Vercel Edge Functions. V8 Isolate 기반 수 ms 콜드 스타트.
- **Regional Edge Cache** — 기존 Origin → Edge 2단계를 3단계로 (Origin → Regional → Edge).
- **Smart Placement** (Cloudflare) — 사용자 기반이 아닌 **Origin 가까이** 배치해 DB 지연 최소화.
2025년 성능 도구 스택
측정 도구
- **Chrome DevTools Performance** — 가장 기본. 2024년 **Performance Insights** 패널 추가로 Core Web Vitals 실시간 분석.
- **Lighthouse** — Chrome 내장, CI용 `lighthouse-ci`, Vercel/Netlify 통합.
- **WebPageTest** — 상세 분석, Filmstrip, 연결 상세. 무료 + 유료 플랜.
- **PageSpeed Insights** — Lab(Lighthouse) + Field(CrUX) 통합 뷰.
- **Chrome UX Report** — 월간 공개, BigQuery로 경쟁사 비교.
프로파일러
- **SpeedScope** — https://www.speedscope.app, 불꽃 그래프 시각화, Chrome Performance 프로파일 import.
- **Perfetto** — Chrome DevTools와 Chromium 내부 추적, UI 공유.
- **React DevTools Profiler** — 컴포넌트 렌더 시간 분해.
- **Next.js Build Analyzer** — `@next/bundle-analyzer`, 번들 크기 시각화.
RUM
- **Vercel Speed Insights** + **Web Analytics** — Next.js 기본.
- **Sentry Performance** — 에러 + RUM 통합.
- **New Relic Browser** — APM 연동.
- **Cloudflare Web Analytics** — 무료, 프라이버시 우선.
- **SpeedCurve** — 경쟁사 비교에 강점.
최적화 도구
- **Next.js Image** + **Vercel Image Optimization** — AVIF/WebP 자동.
- **Sharp** (Node.js) — 서버 사이드 이미지 변환.
- **Partytown** — 3rd party 스크립트 Worker 격리.
- **Fontaine** (Vite plugin) — fallback 폰트 자동 생성.
- **Critical** (Addy Osmani) — Critical CSS 추출.
실전 최적화 체크리스트 (2025)
실제 웹사이트를 최적화할 때 순서:
1. **RUM 도입** — Vercel Speed Insights 또는 web-vitals 라이브러리로 실제 지표 측정
2. **CDN + HTTP/3 + Brotli** — 네트워크 계층
3. **서버 TTFB 200ms 이하** — DB 쿼리 최적화, SSR 캐싱, Edge Function
4. **LCP 이미지 최적화** — AVIF + `fetchpriority="high"` + `preload`
5. **Critical CSS 인라인 + 나머지 deferred** — `media="print"` 해킹 또는 Critical 라이브러리
6. **Font Loading** — `font-display: optional` + size-adjust fallback
7. **JS Code Splitting** — 라우트 단위 + React.lazy
8. **3rd Party Scripts 감사** — Partytown, `next/script` strategy="lazyOnload"
9. **CLS 제거** — 이미지/iframe width/height, Ad 슬롯 aspect-ratio, 폰트 fallback 매칭
10. **INP 최적화** — Long Task 쪼개기 (scheduler.yield), useTransition, Web Worker
11. **Speculation Rules** — 예측 가능한 다음 페이지 prerender
12. **회귀 방지** — Lighthouse CI, Performance Budget (webpack/rollup plugin)
10가지 흔한 안티패턴
1. **LCP 이미지에 `loading="lazy"` 적용** — LCP 영구 지연.
2. **Fonts 없이 커스텀 폰트 사용** — FOIT 3초, 빈 화면.
3. **React 전체 Hydration + 정적 사이트** — Astro/Next SSG를 안 쓰고 Next.js CSR.
4. **Third-party 스크립트 blocking 로드** — GA/GTM을 head에 그냥 넣기.
5. **클라이언트에서 Markdown 렌더링** — 서버에서 미리 HTML로 변환해야.
6. **Lighthouse 점수만 모니터링** — RUM 없이 실제 사용자 경험 맹목.
7. **Layout Thrashing** — `for` 루프 안에서 `offsetHeight` 반복 읽고 쓰기.
8. **거대 이미지 원본 로드** — 4K 이미지를 200px 썸네일에 사용.
9. **동기 `fetch`를 event handler에 await** — INP 크게 악화.
10. **Hydration 중 state 업데이트** — 무한 Hydration/Rerender 루프.
다음 글 예고 — 데이터베이스의 새 물결 — PostgreSQL, pgvector, pg_vector, HNSW, AI 시대의 DB 전략
웹 성능 최적화의 종착지는 보통 **데이터베이스**다. 아무리 CDN 잘 써도 DB 쿼리가 느리면 TTFB가 망가진다. 2023-2025년 데이터베이스 세계의 가장 큰 사건은 **PostgreSQL의 벡터 데이터베이스 정복**이었다. **pgvector** 확장이 Pinecone, Weaviate, Qdrant 같은 전용 벡터 DB를 위협하며 '만능 DB로서의 PostgreSQL' 시대를 열었다.
다음 글에서는:
- **PostgreSQL 왜 다시 1등인가** — StackOverflow 2024 개발자 설문 1위
- **pgvector와 HNSW 인덱스** — 벡터 검색의 수학과 실제
- **pgvector vs Pinecone vs Weaviate vs Qdrant** — 성능/기능/비용 비교
- **PostgreSQL 17의 비약** — Logical Replication, Incremental Backup
- **Supabase, Neon, PlanetScale, CockroachDB** — 클라우드 PostgreSQL 생태계
- **JSON, JSONB, GIN index** — NoSQL 기능 완벽 통합
- **MVCC 원리** — 낙관적 동시성의 우아함
- **Citus, TimescaleDB, PostGIS** — 확장 생태계
- **PostgreSQL + AI** — RAG 파이프라인 실전
...을 다룬다. '**하나의 DB로 모든 것**'이 현실이 된 시대, 그 배경과 실전 설계를 살펴본다. 웹 성능의 여정이 데이터 계층으로 이어지는 이유를 추적해보자.
현재 단락 (1/439)
웹 성능은 2010년대 초 **YSlow**(Yahoo)와 **PageSpeed Insights**(Google)가 대중화한 뒤 한동안 '인프라 팀의 체크리스트' 수준에 머물렀다....