docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
---
|
||||
id: wiki-2026-0508-웹-프론트엔드-성능-최적화
|
||||
title: 웹 프론트엔드 성능 최적화
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Frontend performance, Web perf, Core Web Vitals]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [performance, frontend, core-web-vitals, optimization]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Next.js / Vite
|
||||
---
|
||||
|
||||
# 웹 프론트엔드 성능 최적화
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 LCP/INP/CLS 의 3 metric 을 따라 image, JS, layout 을 deliberate optimize"**. 2026 매 INP (Interaction to Next Paint) 가 FID 대체 → 매 long task 의 적극 splitting. 매 RUM 으로 매 측정, lab data 의 보완.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Core Web Vitals (2026)
|
||||
- **LCP (Largest Contentful Paint)**: ≤ 2.5s — 매 hero image/text 의 render 시간.
|
||||
- **INP (Interaction to Next Paint)**: ≤ 200ms — 매 user interaction 의 response.
|
||||
- **CLS (Cumulative Layout Shift)**: ≤ 0.1 — 매 layout 안정성.
|
||||
|
||||
### 매 optimization layer
|
||||
1. **Network**: HTTP/3, CDN, preload, prefetch, brotli.
|
||||
2. **JS**: code splitting, tree shake, defer, async, lazy import.
|
||||
3. **CSS**: critical CSS inline, remove unused, contain.
|
||||
4. **Image**: AVIF/WebP, responsive srcset, lazy, dimension hints.
|
||||
5. **Render**: Server Components, streaming SSR, partial hydration.
|
||||
|
||||
### 매 응용
|
||||
1. Next.js App Router 매 RSC + streaming.
|
||||
2. Astro 매 island architecture.
|
||||
3. Image CDN (Cloudinary, Cloudflare Images) 매 auto format/size.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Image — 매 next/image
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero"
|
||||
width={1600}
|
||||
height={900}
|
||||
priority // 매 above-fold
|
||||
sizes="(max-width: 768px) 100vw, 80vw"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,..."
|
||||
/>
|
||||
```
|
||||
|
||||
### Code splitting — 매 dynamic import
|
||||
```typescript
|
||||
// 매 route-level: Next.js 자동
|
||||
// 매 component-level: dynamic
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const HeavyChart = dynamic(() => import('./HeavyChart'), {
|
||||
loading: () => <Skeleton />,
|
||||
ssr: false, // 매 client-only
|
||||
});
|
||||
```
|
||||
|
||||
### LCP — 매 preload critical
|
||||
```html
|
||||
<link rel="preload" as="image" href="/hero.avif"
|
||||
imagesrcset="/hero-mobile.avif 480w, /hero.avif 1600w"
|
||||
imagesizes="100vw" fetchpriority="high">
|
||||
|
||||
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://analytics.example.com">
|
||||
```
|
||||
|
||||
### INP — 매 long task splitting
|
||||
```typescript
|
||||
async function processLargeList(items: Item[]) {
|
||||
for (const [i, item] of items.entries()) {
|
||||
process(item);
|
||||
if (i % 50 === 0) {
|
||||
await new Promise((r) => setTimeout(r, 0)); // 매 yield
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 매 또는 scheduler API
|
||||
import { scheduler } from 'scheduler';
|
||||
await scheduler.yield();
|
||||
```
|
||||
|
||||
### CLS — 매 dimension reserve
|
||||
```css
|
||||
/* 매 aspect-ratio */
|
||||
.thumb {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 100%;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/* 매 font-display: optional → CLS 0 */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/inter.woff2') format('woff2');
|
||||
font-display: optional; /* 매 swap 매 layout shift, optional 매 안정 */
|
||||
}
|
||||
```
|
||||
|
||||
### Server Components — 매 zero-JS by default
|
||||
```tsx
|
||||
// app/products/page.tsx — Server Component
|
||||
async function ProductsPage() {
|
||||
const products = await db.products.findAll();
|
||||
return (
|
||||
<ul>
|
||||
{products.map((p) => <ProductCard key={p.id} {...p} />)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
// 매 client component 만 JS 의 hydrate
|
||||
```
|
||||
|
||||
### RUM — 매 web-vitals 수집
|
||||
```typescript
|
||||
import { onLCP, onINP, onCLS } from 'web-vitals';
|
||||
|
||||
function send(metric) {
|
||||
navigator.sendBeacon('/rum',
|
||||
JSON.stringify({ name: metric.name, value: metric.value, id: metric.id })
|
||||
);
|
||||
}
|
||||
|
||||
onLCP(send);
|
||||
onINP(send);
|
||||
onCLS(send);
|
||||
```
|
||||
|
||||
### Critical CSS — 매 inline
|
||||
```html
|
||||
<head>
|
||||
<style>
|
||||
/* 매 above-fold critical CSS — beasties/critters 등으로 추출 */
|
||||
body { margin: 0; font-family: system-ui; }
|
||||
.header { background: #fff; height: 64px; }
|
||||
</style>
|
||||
<link rel="preload" href="/main.css" as="style" onload="this.rel='stylesheet'">
|
||||
</head>
|
||||
```
|
||||
|
||||
### Service Worker — 매 stale-while-revalidate
|
||||
```javascript
|
||||
// sw.js
|
||||
self.addEventListener('fetch', (e) => {
|
||||
if (e.request.destination === 'image') {
|
||||
e.respondWith((async () => {
|
||||
const cache = await caches.open('img-v1');
|
||||
const cached = await cache.match(e.request);
|
||||
const networked = fetch(e.request).then((res) => {
|
||||
cache.put(e.request, res.clone()); return res;
|
||||
});
|
||||
return cached || networked;
|
||||
})());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 hero image | priority + preload + AVIF |
|
||||
| 매 below-fold widget | dynamic import + ssr:false |
|
||||
| 매 long table | virtualization (react-window) |
|
||||
| 매 large list iteration | scheduler.yield |
|
||||
| 매 third-party script | next/script with strategy |
|
||||
|
||||
**기본값**: RSC + next/image + dynamic + Web Vitals RUM.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Web Performance]] · [[Large_Frontend_Projects|Frontend Architecture]]
|
||||
- 변형: [[INP Optimization]] · [[CLS]]
|
||||
- 응용: [[Next.js]] · [[Astro]] · [[Remix]]
|
||||
- Adjacent: [[Lighthouse]] · [[Web Vitals]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 perf audit (Lighthouse JSON 분석), 매 bundle size review, 매 RUM data triage.
|
||||
**언제 X**: 매 actual measurement (lab/RUM 도구 가 더 정확).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 component client**: 매 RSC 무시 매 JS bundle 폭발.
|
||||
- **lazy load hero image**: 매 LCP 악화.
|
||||
- **fixed-pixel layout**: 매 mobile 매 CLS.
|
||||
- **synchronous third-party**: 매 main thread block.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev, Next.js docs, Chrome DevRel).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Core Web Vitals (LCP/INP/CLS), RSC, RUM 패턴 정리 |
|
||||
Reference in New Issue
Block a user