Skip to content

Split View: 웹 성능 최적화 완전 가이드 2025: Core Web Vitals, 로딩 전략, 렌더링 패턴 총정리

✨ Learn with Quiz
|

웹 성능 최적화 완전 가이드 2025: Core Web Vitals, 로딩 전략, 렌더링 패턴 총정리

들어가며

웹 성능은 사용자 경험과 비즈니스 성과에 직접적인 영향을 미칩니다. Google의 연구에 따르면, 페이지 로딩 시간이 1초에서 3초로 늘어나면 이탈률이 32% 증가하고, 5초가 되면 90% 증가합니다. Amazon은 페이지 로드 시간이 100ms 증가할 때마다 매출이 1% 감소한다고 보고했습니다.

2025년, Google은 Core Web Vitals를 검색 순위 요소로 강화했으며, INP(Interaction to Next Paint)가 FID를 대체하면서 상호작용 성능의 중요성이 더욱 커졌습니다. 이 글에서는 Core Web Vitals 최적화부터 이미지, JavaScript, CSS, 폰트, 렌더링 패턴, 캐싱, 네트워크, 모니터링, 그리고 Next.js 특화 최적화까지 웹 성능의 모든 것을 다룹니다.


1. 왜 성능이 중요한가

1.1 비즈니스 임팩트

성능과 비즈니스 지표:
├── 로딩 3초 초과 → 53% 이탈 (Google)
├── 100ms 지연 → 매출 1% 감소 (Amazon)
├── 500ms 지연 → 트래픽 20% 감소 (Google)
├── 1초 개선 → 전환율 7% 향상 (Walmart)
└── 2초 개선 → 바운스율 50% 감소 (COOK)

1.2 SEO와 Core Web Vitals

2025년 Google은 Core Web Vitals를 검색 순위의 핵심 요소로 확정했습니다.

지표좋음 (Good)개선 필요 (Needs Improvement)나쁨 (Poor)
LCP2.5초 이내2.5~4.0초4.0초 초과
INP200ms 이내200~500ms500ms 초과
CLS0.1 이하0.1~0.250.25 초과

1.3 성능 예산 (Performance Budget)

성능 예산 예시:
├── 초기 로드 JS: 200KB 이하 (gzip)
├── 초기 로드 CSS: 50KB 이하 (gzip)
├── 전체 페이지 크기: 1.5MB 이하
├── LCP: 2.5초 이내
├── INP: 200ms 이내
├── CLS: 0.1 이하
├── Time to First Byte: 600ms 이내
└── 요청 수: 50개 이하

2. Core Web Vitals 심층 분석

2.1 LCP (Largest Contentful Paint)

LCP는 뷰포트 내에서 가장 큰 콘텐츠 요소가 렌더링되는 시간입니다.

LCP 대상 요소:
├── img 요소
├── video 요소 (포스터 이미지)
├── CSS background-image가 있는 요소
├── 텍스트 노드를 포함하는 블록 레벨 요소
└── svg 내의 image 요소

LCP 최적화 전략:

<!-- 1. 히어로 이미지 프리로드 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />

<!-- 2. LCP 이미지에 fetchpriority 설정 -->
<img src="/hero.webp" alt="Hero" fetchpriority="high" loading="eager" />

<!-- 3. 서버 응답 시간 최적화 -->
<!-- TTFB 목표: 200ms 이내 -->
/* 4. 폰트 로딩 최적화 */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap;
}

2.2 INP (Interaction to Next Paint)

INP는 사용자 상호작용(클릭, 탭, 키보드)에서 다음 페인트까지의 지연 시간을 측정합니다.

INP 최적화 전략:
1. 긴 태스크 분할
   - 50ms 이상의 태스크를 더 작은 단위로 분할
   - requestIdleCallback, scheduler.yield() 활용

2. 메인 스레드 해방
   - Web Worker로 무거운 계산 이동
   - 불필요한 동기 JS 제거

3. 이벤트 핸들러 최적화
   - debounce / throttle 적용
   - passive 이벤트 리스너 사용
// 긴 태스크 분할 예시
async function processLargeList(items: Item[]) {
  const CHUNK_SIZE = 50;

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    processChunk(chunk);

    // 브라우저에게 렌더링 기회 제공
    await scheduler.yield();
  }
}

// scheduler.yield 폴리필
if (!('scheduler' in globalThis)) {
  (globalThis as any).scheduler = {
    yield: () => new Promise(resolve => setTimeout(resolve, 0))
  };
}

2.3 CLS (Cumulative Layout Shift)

CLS는 페이지 로드 중 예기치 않은 레이아웃 이동의 총합입니다.

<!-- CLS 방지: 이미지에 크기 명시 -->
<img src="/photo.webp" width="800" height="600" alt="Photo" />

<!-- CLS 방지: 동적 콘텐츠에 min-height 설정 -->
<div style="min-height: 250px;">
  <!-- 광고 또는 동적 콘텐츠 -->
</div>
/* CLS 방지: 폰트 로딩 시 레이아웃 시프트 최소화 */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: optional; /* 또는 swap */
  /* size-adjust로 대체 폰트와 크기 맞춤 */
  size-adjust: 100.5%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

CLS 주요 원인과 해결:

원인해결 방법
크기 미지정 이미지width/height 또는 aspect-ratio 속성 사용
동적 삽입 콘텐츠min-height로 공간 예약
웹 폰트 FOUT/FOITfont-display: optional + preload
동적 광고고정 크기 컨테이너 사용
늦게 로드되는 CSSCritical CSS 인라인

3. 이미지 최적화

3.1 차세대 포맷

이미지 포맷 비교 (같은 품질 기준):
├── JPEG: 100KB (기준)
├── WebP: 70KB (-30%)
├── AVIF: 50KB (-50%)
└── JXL (JPEG XL): 55KB (-45%)

브라우저 지원 (2025):
├── WebP: 97%+ (IE 미지원)
├── AVIF: 92%+ (Safari 16.4+)
└── JXL: Chrome에서 제거, Safari/Firefox 지원
<!-- picture 요소로 포맷 폴백 -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>

3.2 반응형 이미지

<!-- srcset과 sizes로 반응형 이미지 -->
<img
  srcset="
    /hero-400w.webp 400w,
    /hero-800w.webp 800w,
    /hero-1200w.webp 1200w,
    /hero-1600w.webp 1600w
  "
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  src="/hero-800w.webp"
  alt="Hero image"
  width="1200"
  height="600"
  loading="lazy"
  decoding="async"
/>

3.3 지연 로딩과 블러 플레이스홀더

<!-- 네이티브 lazy loading -->
<img src="/photo.webp" loading="lazy" decoding="async" alt="Photo" />

<!-- Intersection Observer로 커스텀 lazy loading -->
// 블러 플레이스홀더 구현
function BlurImage({ src, alt, width, height, blurDataURL }: ImageProps) {
  return (
    <div style={{ position: 'relative', width, height }}>
      {/* 블러 플레이스홀더 */}
      <img
        src={blurDataURL}
        alt=""
        style={{
          position: 'absolute',
          inset: 0,
          filter: 'blur(20px)',
          transform: 'scale(1.1)',
        }}
      />
      {/* 실제 이미지 */}
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
        onLoad={(e) => {
          // 로드 완료 시 블러 제거
          e.currentTarget.style.opacity = '1';
        }}
        style={{ position: 'relative', opacity: 0, transition: 'opacity 0.3s' }}
      />
    </div>
  );
}

4. JavaScript 최적화

4.1 Code Splitting

// React lazy + Suspense (Route-based splitting)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// 조건부 동적 import
async function handleExport() {
  // xlsx 라이브러리를 필요할 때만 로드
  const XLSX = await import('xlsx');
  const workbook = XLSX.utils.book_new();
  // ...
}

4.2 Tree Shaking

// 나쁜 예: 전체 라이브러리 import
import _ from 'lodash';
_.debounce(fn, 300);

// 좋은 예: 필요한 함수만 import
import debounce from 'lodash/debounce';
debounce(fn, 300);

// 더 좋은 예: 네이티브 또는 경량 대안 사용
// lodash.debounce: 1.4KB vs lodash: 72KB
// webpack-bundle-analyzer로 번들 분석
// npm install --save-dev webpack-bundle-analyzer

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Next.js config
});

4.3 번들 최적화

// webpack splitChunks 설정
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
        common: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

4.4 스크립트 로딩 전략

<!-- 일반: 파싱 차단 -->
<script src="app.js"></script>

<!-- async: 비동기 다운로드, 즉시 실행 (파싱 차단 가능) -->
<script src="analytics.js" async></script>

<!-- defer: 비동기 다운로드, DOM 파싱 후 실행 (순서 보장) -->
<script src="app.js" defer></script>

<!-- module: defer와 동일한 동작 -->
<script type="module" src="app.js"></script>
스크립트 로딩 타임라인:
일반:    [HTML 파싱...] [다운로드] [실행] [HTML 파싱...]
async:   [HTML 파싱......다운로드......] [실행] [HTML 파싱...]
defer:   [HTML 파싱......다운로드.............] [실행]

5. CSS 최적화

5.1 Critical CSS

<!-- Critical CSS 인라인 -->
<head>
  <style>
    /* 첫 화면(Above the fold)에 필요한 최소 CSS만 인라인 */
    body { margin: 0; font-family: system-ui; }
    .header { height: 60px; background: #fff; }
    .hero { height: 400px; display: flex; align-items: center; }
  </style>

  <!-- 나머지 CSS는 비동기 로드 -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>

5.2 CSS Containment

/* contain으로 렌더링 범위 제한 */
.card {
  contain: layout style paint;
  /* 또는 content-visibility로 오프스크린 요소 최적화 */
  content-visibility: auto;
  contain-intrinsic-size: 0 300px;
}

/* will-change로 GPU 가속 힌트 */
.animated-element {
  will-change: transform;
  /* 주의: 남용 시 메모리 낭비 */
}

5.3 불필요한 CSS 제거

// PurgeCSS 설정 (postcss.config.js)
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: [
        './src/**/*.{js,jsx,ts,tsx}',
        './public/index.html'
      ],
      defaultExtractor: content =>
        content.match(/[\w-/:]+(?<!:)/g) || [],
      safelist: ['html', 'body', /^data-/]
    })
  ]
};

6. 폰트 최적화

6.1 font-display 전략

/* swap: 대체 폰트 → 웹폰트 (FOUT 발생, 텍스트 즉시 표시) */
@font-face {
  font-family: 'MyFont';
  font-display: swap;
  src: url('/fonts/myfont.woff2') format('woff2');
}

/* optional: 빠르면 웹폰트, 느리면 대체 폰트 유지 (CLS 최소) */
@font-face {
  font-family: 'MyFont';
  font-display: optional;
  src: url('/fonts/myfont.woff2') format('woff2');
}

6.2 폰트 프리로드

<!-- 핵심 폰트 프리로드 -->
<link
  rel="preload"
  href="/fonts/inter-var.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

6.3 Variable Fonts

/* 기존: 각 굵기별 별도 파일 (400, 500, 600, 700 = 4파일) */
/* Variable Font: 1파일로 모든 굵기 */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

/* 사용 */
.light { font-weight: 300; }
.regular { font-weight: 400; }
.bold { font-weight: 700; }

6.4 폰트 서브셋

# pyftsubset으로 한글 폰트 서브셋 생성
# pip install fonttools brotli
pyftsubset NotoSansKR-Regular.otf \
  --text-file=korean-chars.txt \
  --output-file=NotoSansKR-subset.woff2 \
  --flavor=woff2

# 결과: 4.5MB → 300KB (한글 2,350자 기준)

7. 렌더링 패턴

7.1 패턴 비교표

패턴TTFBFCPLCPTTISEO사용 사례
CSR빠름느림느림느림나쁨SPA, 대시보드
SSR느림빠름빠름느림좋음동적 콘텐츠, SEO 필요
SSG매우 빠름매우 빠름매우 빠름빠름매우 좋음블로그, 문서, 마케팅
ISR매우 빠름매우 빠름매우 빠름빠름매우 좋음이커머스, 뉴스
Streaming SSR빠름매우 빠름빠름빠름좋음복잡한 동적 페이지

7.2 CSR (Client-Side Rendering)

CSR 플로우:
Browser        Server
  │─ HTML 요청 ──→│
  │←─ 빈 HTML ────│
  │─ JS 요청 ────→│
  │←─ JS 번들 ────│
[JS 파싱/실행]  │─ API 요청 ───→│
  │←─ 데이터 ─────│
[렌더링]  ▼ 화면 표시      ▼

7.3 SSR (Server-Side Rendering)

SSR 플로우:
Browser        Server
  │─ HTML 요청 ──→│
  │               │ [데이터 패칭]
  │               │ [HTML 렌더링]
  │←─ 완성 HTML ──│
[화면 표시]  │─ JS 요청 ────→│
  │←─ JS 번들 ────│
[Hydration]  ▼ 인터랙티브     ▼

7.4 Streaming SSR

// Next.js App Router Streaming SSR
import { Suspense } from 'react';

async function SlowComponent() {
  const data = await fetchSlowData(); // 3초 소요
  return <div>{/* data 렌더링 */}</div>;
}

export default function Page() {
  return (
    <div>
      {/* 즉시 렌더링 */}
      <Header />
      <Hero />

      {/* 스트리밍: 준비되면 점진적 전송 */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>

      {/* 즉시 렌더링 */}
      <Footer />
    </div>
  );
}

7.5 ISR (Incremental Static Regeneration)

// Next.js ISR
// app/products/[id]/page.tsx
export const revalidate = 3600; // 1시간마다 재생성

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

export default ProductPage;

// 정적 경로 생성
export async function generateStaticParams() {
  const products = await getTopProducts(100);
  return products.map(p => ({ id: p.id }));
}

8. 캐싱 전략

8.1 HTTP 캐시

HTTP 캐시 전략:
├── 정적 자산 (JS/CSS/이미지)
Cache-Control: public, max-age=31536000, immutable
   (파일명에 해시 포함: app.abc123.js)
├── HTML
Cache-Control: public, max-age=0, must-revalidate
   (항상 서버 확인)
├── API 응답
Cache-Control: private, max-age=60, stale-while-revalidate=300
   (60초 캐시, 5분간 stale 허용)
└── 사용자별 데이터
    Cache-Control: private, no-cache
    (매번 서버 검증)

8.2 stale-while-revalidate

stale-while-revalidate 동작:
요청 1: [캐시 미스] → 서버 → 응답 + 캐시 저장
요청 2: [캐시 히트, fresh] → 즉시 응답
요청 3: [캐시 히트, stale] → 즉시 응답(stale) + 백그라운드 서버 갱신
요청 4: [캐시 히트, fresh] → 즉시 응답(갱신된 데이터)
// SWR 라이브러리 (React)
import useSWR from 'swr';

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher, {
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
    refreshInterval: 30000, // 30초마다 갱신
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error />;
  return <UserCard user={data} />;
}

8.3 Service Worker 캐시

// Service Worker 캐싱 전략
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles.css',
  '/app.js',
  '/offline.html'
];

// 설치: 정적 자산 사전 캐싱
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll(STATIC_ASSETS)
    )
  );
});

// 요청: Cache First + Network Fallback
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached;

      return fetch(event.request).then(response => {
        // 성공 응답을 캐시에 저장
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache =>
          cache.put(event.request, clone)
        );
        return response;
      }).catch(() => {
        // 오프라인 폴백
        return caches.match('/offline.html');
      });
    })
  );
});

8.4 CDN 캐시

CDN 캐시 계층:
사용자 → [브라우저 캐시][CDN Edge][CDN Origin Shield][서버]

CDN 헤더 예시:
정적 자산:
  Cache-Control: public, max-age=31536000, immutable
  CDN-Cache-Control: public, max-age=31536000

동적 콘텐츠:
  Cache-Control: public, max-age=0, must-revalidate
  CDN-Cache-Control: public, max-age=60, stale-while-revalidate=300
  Surrogate-Control: max-age=3600

9. 네트워크 최적화

9.1 리소스 힌트

<!-- DNS Prefetch: 외부 도메인 DNS 미리 해석 -->
<link rel="dns-prefetch" href="//api.example.com" />
<link rel="dns-prefetch" href="//cdn.example.com" />

<!-- Preconnect: DNS + TCP + TLS 미리 연결 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Prefetch: 다음 페이지 리소스 미리 로드 -->
<link rel="prefetch" href="/next-page.js" />

<!-- Preload: 현재 페이지 핵심 리소스 우선 로드 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/hero.webp" as="image" />

9.2 Priority Hints

<!-- fetchpriority로 리소스 우선순위 지정 -->
<!-- 히어로 이미지: 높은 우선순위 -->
<img src="/hero.webp" fetchpriority="high" />

<!-- 오프스크린 이미지: 낮은 우선순위 -->
<img src="/below-fold.webp" fetchpriority="low" loading="lazy" />

<!-- 핵심 스크립트: 높은 우선순위 -->
<script src="/critical.js" fetchpriority="high"></script>

9.3 HTTP/2와 HTTP/3

HTTP/1.1 vs HTTP/2 vs HTTP/3:
┌─────────────┬──────────────┬──────────────┬──────────────┐
│  기능        │  HTTP/1.1HTTP/2HTTP/3├─────────────┼──────────────┼──────────────┼──────────────┤
│ 멀티플렉싱   │ 불가         │ 지원          │ 지원          │
│ 헤더 압축    │ 없음         │ HPACKQPACK│ 서버 푸시    │ 없음         │ 지원          │ 제거됨        │
HOL 블로킹   │ TCP 레벨     │ TCP 레벨      │ 해결(QUIC)│ 전송 프로토콜│ TCPTCPQUIC(UDP)│ 연결 설정    │ TCP+TLSTCP+TLS0-RTT 가능   │
└─────────────┴──────────────┴──────────────┴──────────────┘

10. 성능 모니터링

10.1 Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: './lighthouserc.json'
          uploadArtifacts: true
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.9 }],
        "categories:best-practices": ["warn", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "interactive": ["error", { "maxNumericValue": 5000 }]
      }
    }
  }
}

10.2 Real User Monitoring (RUM)

// Web Vitals 측정 및 보고
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric: any) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });

  // Beacon API로 비동기 전송
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

10.3 성능 대시보드

성능 모니터링 도구:
├── 합성 모니터링 (Lab Data)
│   ├── Lighthouse CI (자동화)
│   ├── WebPageTest (상세 분석)
│   └── PageSpeed Insights (Google)
├── 실사용자 모니터링 (Field Data)
│   ├── Chrome UX Report (CrUX)
│   ├── web-vitals 라이브러리
│   └── 상용: Datadog RUM, New Relic
└── 번들 분석
    ├── webpack-bundle-analyzer
    ├── source-map-explorer
    └── bundlephobia.com

11. Next.js 특화 최적화

11.1 App Router와 Server Components

// Server Component (기본값 - JS 번들에 포함되지 않음)
// app/products/page.tsx
async function ProductsPage() {
  // 서버에서 직접 데이터 패칭 (API 라우트 불필요)
  const products = await db.product.findMany();

  return (
    <div>
      <h1>Products</h1>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

// Client Component (인터랙션이 필요한 부분만)
// components/AddToCart.tsx
'use client';

import { useState } from 'react';

export function AddToCart({ productId }: { productId: string }) {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Add to Cart ({count})
    </button>
  );
}

11.2 Next.js Image 최적화

import Image from 'next/image';

// 자동 WebP/AVIF 변환, 반응형, lazy loading
function HeroSection() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority  // LCP 이미지는 priority 설정
      sizes="(max-width: 768px) 100vw, 1200px"
      quality={85}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

// 원격 이미지
function Avatar({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl}
      alt={user.name}
      width={48}
      height={48}
      loading="lazy"
    />
  );
}

11.3 Next.js Font 최적화

// next/font로 자동 최적화 (자체 호스팅, CLS 제거)
import { Inter, Noto_Sans_KR } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

const notoSansKR = Noto_Sans_KR({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  display: 'swap',
  variable: '--font-noto-sans-kr',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={`${inter.variable} ${notoSansKR.variable}`}>
      <body>{children}</body>
    </html>
  );
}

11.4 Route Segment Config

// app/blog/[slug]/page.tsx

// 정적 생성 + ISR (60초)
export const revalidate = 60;

// 또는 완전 정적
export const dynamic = 'force-static';

// 또는 항상 동적
export const dynamic = 'force-dynamic';

// 런타임 선택
export const runtime = 'edge'; // Edge Runtime (빠른 TTFB)
// export const runtime = 'nodejs'; // Node.js Runtime (기본)

12. 면접 질문 모음

기본 개념

Q1. Core Web Vitals 3가지 지표를 설명하세요.
  1. LCP (Largest Contentful Paint): 뷰포트에서 가장 큰 콘텐츠 요소가 렌더링되는 시간. 목표: 2.5초 이내. 로딩 성능을 측정.

  2. INP (Interaction to Next Paint): 사용자 상호작용(클릭, 키보드 등)에서 다음 페인트까지의 지연. 목표: 200ms 이내. 응답성을 측정.

  3. CLS (Cumulative Layout Shift): 페이지 로드 중 예기치 않은 레이아웃 이동의 총합. 목표: 0.1 이하. 시각적 안정성을 측정.

Q2. CSR, SSR, SSG, ISR의 차이점을 설명하세요.

CSR (Client-Side Rendering): 브라우저에서 JS로 렌더링. 빈 HTML 전송 후 클라이언트에서 렌더링.

  • 장점: 서버 부하 낮음, SPA 적합
  • 단점: 느린 초기 로딩, SEO 불리

SSR (Server-Side Rendering): 서버에서 HTML을 매 요청마다 생성.

  • 장점: 빠른 FCP, SEO 유리
  • 단점: 서버 부하, 느린 TTFB

SSG (Static Site Generation): 빌드 시 HTML 미리 생성.

  • 장점: 최고 성능, CDN 캐싱
  • 단점: 빌드 시간, 동적 콘텐츠 제한

ISR (Incremental Static Regeneration): SSG + 백그라운드 재생성.

  • 장점: SSG 성능 + 데이터 갱신
  • 단점: 구현 복잡성
Q3. Code Splitting과 Tree Shaking의 차이를 설명하세요.

Code Splitting: JS 번들을 여러 청크로 분할하여 필요한 것만 로드하는 기법. 라우트 기반(dynamic import) 또는 컴포넌트 기반으로 분할. 초기 로딩 시간 단축.

Tree Shaking: 빌드 시 사용되지 않는(dead) 코드를 제거하는 최적화. ES Module의 정적 구조를 분석하여 import되지 않은 export를 제거. 최종 번들 크기 감소.

차이: Code Splitting은 "언제 로드할지" (When), Tree Shaking은 "무엇을 제거할지" (What).

Q4. LCP를 개선하는 방법을 설명하세요.
  1. 서버 응답 최적화: TTFB 200ms 이내, CDN 활용, 캐싱
  2. 리소스 프리로드: LCP 이미지에 preload + fetchpriority="high"
  3. 이미지 최적화: WebP/AVIF, 적절한 크기, 압축
  4. 렌더링 차단 제거: Critical CSS 인라인, JS defer
  5. 폰트 최적화: font-display: swap, preload
  6. SSR/SSG 활용: 서버에서 완성된 HTML 전송
  7. Third-party 최적화: 외부 스크립트 지연 로드
Q5. CLS의 주요 원인과 해결 방법을 설명하세요.

주요 원인:

  1. 크기 미지정 이미지/비디오
  2. 동적 삽입 콘텐츠(광고, 배너)
  3. 웹 폰트 로딩 시 레이아웃 시프트
  4. 늦게 로드되는 CSS
  5. DOM을 조작하는 JavaScript

해결 방법:

  1. img/video에 width, height 속성 또는 aspect-ratio CSS 명시
  2. 동적 콘텐츠 영역에 min-height로 공간 예약
  3. font-display: optional, 폰트 프리로드
  4. Critical CSS 인라인
  5. transform 애니메이션 사용(top/left 대신)

심화 질문

Q6. Service Worker의 캐싱 전략을 설명하세요.
  1. Cache First: 캐시 확인 후 없으면 네트워크. 정적 자산에 적합.
  2. Network First: 네트워크 시도 후 실패 시 캐시. API 응답에 적합.
  3. Stale While Revalidate: 캐시된 응답 즉시 반환 + 백그라운드 갱신. 빈번히 변경되는 자산에 적합.
  4. Cache Only: 캐시만 사용. 오프라인 자산에 적합.
  5. Network Only: 네트워크만 사용. 실시간 데이터에 적합.

선택 기준: 데이터의 신선도 요구사항과 오프라인 지원 필요성에 따라 결정.

Q7. HTTP 캐시 헤더를 설명하세요.
  • Cache-Control: 캐시 정책의 핵심 헤더

    • max-age: 캐시 유효 시간(초)
    • no-cache: 매번 서버 검증 필요
    • no-store: 절대 캐시하지 않음
    • public/private: CDN 캐시 가능 여부
    • immutable: 변하지 않는 리소스
    • stale-while-revalidate: stale 응답 허용 시간
  • ETag: 리소스 버전 식별자. 조건부 요청(If-None-Match)에 사용.

  • Last-Modified: 마지막 수정 시간. If-Modified-Since와 함께 사용.

추천 전략:

  • 정적 자산(해시 포함): max-age=31536000, immutable
  • HTML: no-cache 또는 max-age=0, must-revalidate
Q8. 이미지 최적화 전략을 종합적으로 설명하세요.
  1. 포맷: WebP/AVIF 사용, picture 요소로 폴백
  2. 크기: 반응형 이미지(srcset + sizes), 실제 표시 크기에 맞게
  3. 압축: 품질 75~85, 용도에 따라 조정
  4. 로딩: LCP 이미지는 eager + fetchpriority="high", 나머지는 lazy
  5. 프레임워크: Next.js Image 등 자동 최적화 도구 활용
  6. CDN: 이미지 CDN(Cloudinary, imgix) 활용
  7. 플레이스홀더: 블러 또는 LQIP로 사용자 경험 향상
Q9. INP(Interaction to Next Paint)를 최적화하는 방법을 설명하세요.
  1. 긴 태스크 분할: 50ms 이상의 작업을 scheduler.yield()나 requestIdleCallback으로 분할
  2. 메인 스레드 해방: Web Worker로 무거운 계산 이동
  3. 이벤트 핸들러 최적화: debounce/throttle, passive 이벤트 리스너
  4. JS 번들 최소화: 코드 스플리팅, 트리 셰이킹
  5. 가상화: 긴 리스트는 react-virtuoso 등으로 가상화
  6. startTransition: 긴급하지 않은 업데이트를 전환으로 표시
  7. React 최적화: useMemo, useCallback, React.memo 적절히 사용
Q10. CDN의 동작 원리와 성능 이점을 설명하세요.

동작 원리:

  1. 전 세계에 분산된 Edge 서버(PoP)에 콘텐츠 캐시
  2. 사용자의 DNS 요청을 가장 가까운 Edge로 라우팅
  3. Edge에 캐시가 있으면 즉시 응답, 없으면 Origin에서 가져와 캐시

성능 이점:

  • 물리적 거리 단축: RTT(Round Trip Time) 감소
  • Origin 서버 부하 분산
  • DDoS 방어
  • TLS 최적화 (Edge에서 TLS 종료)
  • 자동 압축 (Brotli, gzip)
  • HTTP/2, HTTP/3 지원
Q11. webpack의 코드 스플리팅 설정을 설명하세요.

webpack의 splitChunks 플러그인으로 코드 분할:

  1. Entry Points: 여러 진입점으로 수동 분할
  2. Dynamic Imports: import()로 동적 분할
  3. splitChunks: 자동 분할 규칙 설정

주요 설정:

  • chunks: 'all'로 동기/비동기 모두 분할
  • cacheGroups: vendor(node_modules)와 common(공유 모듈) 분리
  • minSize: 최소 청크 크기 (기본 20KB)
  • maxSize: 최대 청크 크기 (자동 분할)

효과: 초기 로딩 시간 단축, 캐시 효율 향상, 필요한 코드만 로드

Q12. Streaming SSR의 장점과 구현 방법을 설명하세요.

장점:

  • TTFB 단축: HTML을 점진적으로 전송
  • FCP 향상: 준비된 부분부터 표시
  • 느린 데이터 소스 격리: Suspense로 감싸서 독립적 로딩
  • 사용자 경험 향상: 스켈레톤/로딩 상태 즉시 표시

구현 (Next.js App Router):

  • Server Component를 기본으로 사용
  • 느린 컴포넌트를 Suspense로 감싸기
  • fallback에 스켈레톤 UI 제공
  • 데이터가 준비되면 자동으로 스트리밍

핵심: 전체 페이지가 준비될 때까지 기다리지 않고, 준비된 부분부터 점진적으로 전송.

Q13. Next.js Server Components의 성능 이점을 설명하세요.
  1. 제로 JS 번들: Server Component는 클라이언트 JS 번들에 포함되지 않음
  2. 직접 데이터 접근: 서버에서 직접 DB/API 접근 (API 라우트 불필요)
  3. 자동 코드 분할: Client Component만 번들에 포함
  4. 스트리밍: Suspense와 결합하여 점진적 렌더링
  5. 캐싱: 서버 측 캐싱으로 반복 요청 최적화
  6. 보안: 민감한 로직/키가 클라이언트에 노출되지 않음

사용 원칙: 인터랙션이 없는 컴포넌트는 Server, useState/useEffect가 필요한 부분만 Client로.

Q14. 웹 성능 예산(Performance Budget)의 설정과 적용 방법은?

설정:

  1. 경쟁사 분석: 주요 경쟁사의 성능 지표 측정
  2. 사용자 기기 분석: 대상 사용자의 평균 기기/네트워크 파악
  3. 비즈니스 목표 반영: 전환율, 이탈률 목표에 맞게 설정
  4. 구체적 수치: JS 200KB, CSS 50KB, LCP 2.5s, INP 200ms 등

적용:

  1. Lighthouse CI로 PR마다 자동 검사
  2. 예산 초과 시 빌드 실패 또는 경고
  3. bundlesize 또는 size-limit으로 번들 크기 제한
  4. 팀 대시보드에서 트렌드 모니터링
  5. 정기적으로 예산 검토 및 조정
Q15. 웹 폰트 최적화의 모범 사례를 설명하세요.
  1. Variable Font: 하나의 파일로 모든 굵기/스타일 (파일 수 감소)
  2. 서브셋: 필요한 글자만 포함 (한글: 4MB → 300KB)
  3. WOFF2 포맷: 최고 압축률의 웹 폰트 포맷
  4. font-display: swap(텍스트 즉시 표시) 또는 optional(CLS 최소)
  5. preload: 핵심 폰트를 link preload로 조기 다운로드
  6. 자체 호스팅: Google Fonts 대신 직접 호스팅 (DNS/연결 비용 절감)
  7. size-adjust: 대체 폰트와 웹 폰트의 크기를 맞추어 CLS 방지

13. 퀴즈

Q1. LCP의 "Good" 기준은?

정답: 2.5초 이내

LCP(Largest Contentful Paint)의 기준:

  • Good: 2.5초 이내
  • Needs Improvement: 2.5~4.0초
  • Poor: 4.0초 초과

LCP는 뷰포트에서 가장 큰 콘텐츠 요소가 렌더링되는 시간을 측정합니다.

Q2. FID를 대체한 Core Web Vitals 지표는?

정답: INP (Interaction to Next Paint)

2024년 3월부터 FID(First Input Delay)가 INP로 대체되었습니다. FID는 첫 번째 상호작용만 측정했지만, INP는 페이지 전체 수명 동안의 모든 상호작용을 측정하여 더 포괄적인 응답성 지표를 제공합니다.

Q3. HTTP 캐시에서 immutable의 의미는?

정답: 리소스가 절대 변경되지 않음을 나타내어 재검증 요청을 방지

Cache-Control: immutable은 리소스가 변경되지 않을 것임을 브라우저에게 알려줍니다. 이로 인해 브라우저가 max-age 내에서 재검증(304) 요청을 보내지 않습니다. 파일명에 해시가 포함된 정적 자산(app.abc123.js)에 적합합니다.

Q4. Tree Shaking이 동작하기 위한 전제 조건은?

정답: ES Module (import/export) 사용

Tree Shaking은 ES Module의 정적 구조를 분석하여 사용되지 않는 export를 제거합니다. CommonJS(require/module.exports)는 동적이므로 Tree Shaking이 불가능합니다. package.json의 "sideEffects": false 설정도 중요합니다.

Q5. Next.js Server Components가 클라이언트 번들에 포함되나요?

정답: 아니요, Server Components는 클라이언트 JS 번들에 포함되지 않습니다

Server Components는 서버에서만 실행되고 결과 HTML만 클라이언트에 전송됩니다. 따라서 JS 번들 크기를 크게 줄일 수 있습니다. 'use client' 디렉티브가 선언된 Client Components만 번들에 포함됩니다.


14. 참고 자료

공식 문서

측정 도구

이미지 최적화

성능 참고

프레임워크

Web Performance Optimization Complete Guide 2025: Core Web Vitals, Loading Strategies, and Rendering Patterns

Introduction

Web performance has a direct impact on user experience and business outcomes. According to Google research, when page load time increases from 1 to 3 seconds, bounce rate goes up by 32%; at 5 seconds, it increases by 90%. Amazon reported that every 100ms increase in page load time results in a 1% decrease in sales.

In 2025, Google has strengthened Core Web Vitals as a search ranking factor, and with INP (Interaction to Next Paint) replacing FID, the importance of interaction performance has grown. This article covers everything about web performance: Core Web Vitals optimization, images, JavaScript, CSS, fonts, rendering patterns, caching, networking, monitoring, and Next.js-specific optimizations.


1. Why Performance Matters

1.1 Business Impact

Performance and business metrics:
├── 3s+ load time → 53% bounce (Google)
├── 100ms delay → 1% revenue loss (Amazon)
├── 500ms delay → 20% traffic drop (Google)
├── 1s improvement → 7% conversion boost (Walmart)
└── 2s improvement → 50% bounce rate reduction (COOK)

1.2 SEO and Core Web Vitals

In 2025, Google has confirmed Core Web Vitals as a key search ranking factor.

MetricGoodNeeds ImprovementPoor
LCPUnder 2.5s2.5~4.0sOver 4.0s
INPUnder 200ms200~500msOver 500ms
CLSUnder 0.10.1~0.25Over 0.25

1.3 Performance Budget

Performance budget example:
├── Initial JS load: under 200KB (gzip)
├── Initial CSS load: under 50KB (gzip)
├── Total page weight: under 1.5MB
├── LCP: under 2.5s
├── INP: under 200ms
├── CLS: under 0.1
├── Time to First Byte: under 600ms
└── Request count: under 50

2. Core Web Vitals Deep Dive

2.1 LCP (Largest Contentful Paint)

LCP measures the time it takes for the largest content element in the viewport to render.

LCP target elements:
├── img elements
├── video elements (poster image)
├── Elements with CSS background-image
├── Block-level elements containing text nodes
└── image elements within svg

LCP Optimization Strategies:

<!-- 1. Preload hero image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />

<!-- 2. Set fetchpriority on LCP image -->
<img src="/hero.webp" alt="Hero" fetchpriority="high" loading="eager" />

<!-- 3. Optimize server response time -->
<!-- TTFB target: under 200ms -->
/* 4. Font loading optimization */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap;
}

2.2 INP (Interaction to Next Paint)

INP measures the delay from user interactions (clicks, taps, keyboard) to the next paint.

INP optimization strategies:
1. Break up long tasks
   - Split tasks over 50ms into smaller units
   - Use requestIdleCallback, scheduler.yield()

2. Free the main thread
   - Move heavy computations to Web Workers
   - Remove unnecessary synchronous JS

3. Optimize event handlers
   - Apply debounce / throttle
   - Use passive event listeners
// Long task splitting example
async function processLargeList(items: Item[]) {
  const CHUNK_SIZE = 50;

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    processChunk(chunk);

    // Give the browser a chance to render
    await scheduler.yield();
  }
}

// scheduler.yield polyfill
if (!('scheduler' in globalThis)) {
  (globalThis as any).scheduler = {
    yield: () => new Promise(resolve => setTimeout(resolve, 0))
  };
}

2.3 CLS (Cumulative Layout Shift)

CLS measures the total of all unexpected layout shifts during page load.

<!-- Prevent CLS: specify image dimensions -->
<img src="/photo.webp" width="800" height="600" alt="Photo" />

<!-- Prevent CLS: set min-height for dynamic content -->
<div style="min-height: 250px;">
  <!-- Ad or dynamic content -->
</div>
/* Prevent CLS: minimize layout shift during font loading */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: optional; /* or swap */
  size-adjust: 100.5%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

CLS Common Causes and Solutions:

CauseSolution
Images without dimensionsUse width/height or aspect-ratio
Dynamically injected contentReserve space with min-height
Web font FOUT/FOITfont-display: optional + preload
Dynamic adsUse fixed-size containers
Late-loading CSSInline critical CSS

3. Image Optimization

3.1 Next-Gen Formats

Image format comparison (same quality):
├── JPEG: 100KB (baseline)
├── WebP: 70KB (-30%)
├── AVIF: 50KB (-50%)
└── JXL (JPEG XL): 55KB (-45%)

Browser support (2025):
├── WebP: 97%+ (no IE)
├── AVIF: 92%+ (Safari 16.4+)
└── JXL: Removed from Chrome, supported in Safari/Firefox
<!-- picture element for format fallback -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>

3.2 Responsive Images

<!-- Responsive images with srcset and sizes -->
<img
  srcset="
    /hero-400w.webp 400w,
    /hero-800w.webp 800w,
    /hero-1200w.webp 1200w,
    /hero-1600w.webp 1600w
  "
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  src="/hero-800w.webp"
  alt="Hero image"
  width="1200"
  height="600"
  loading="lazy"
  decoding="async"
/>

3.3 Lazy Loading and Blur Placeholders

<!-- Native lazy loading -->
<img src="/photo.webp" loading="lazy" decoding="async" alt="Photo" />
// Blur placeholder implementation
function BlurImage({ src, alt, width, height, blurDataURL }: ImageProps) {
  return (
    <div style={{ position: 'relative', width, height }}>
      {/* Blur placeholder */}
      <img
        src={blurDataURL}
        alt=""
        style={{
          position: 'absolute',
          inset: 0,
          filter: 'blur(20px)',
          transform: 'scale(1.1)',
        }}
      />
      {/* Actual image */}
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
        onLoad={(e) => {
          e.currentTarget.style.opacity = '1';
        }}
        style={{ position: 'relative', opacity: 0, transition: 'opacity 0.3s' }}
      />
    </div>
  );
}

4. JavaScript Optimization

4.1 Code Splitting

// React lazy + Suspense (Route-based splitting)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// Conditional dynamic import
async function handleExport() {
  // Load xlsx library only when needed
  const XLSX = await import('xlsx');
  const workbook = XLSX.utils.book_new();
  // ...
}

4.2 Tree Shaking

// Bad: import entire library
import _ from 'lodash';
_.debounce(fn, 300);

// Good: import only what you need
import debounce from 'lodash/debounce';
debounce(fn, 300);

// Better: use native or lightweight alternatives
// lodash.debounce: 1.4KB vs lodash: 72KB
// Bundle analysis with webpack-bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Next.js config
});

4.3 Bundle Optimization

// webpack splitChunks configuration
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
        common: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

4.4 Script Loading Strategies

<!-- Normal: blocks parsing -->
<script src="app.js"></script>

<!-- async: download async, execute immediately (may block parsing) -->
<script src="analytics.js" async></script>

<!-- defer: download async, execute after DOM parsing (order preserved) -->
<script src="app.js" defer></script>

<!-- module: same behavior as defer -->
<script type="module" src="app.js"></script>
Script loading timeline:
normal:  [HTML parsing...] [download] [execute] [HTML parsing...]
async:   [HTML parsing......download......] [execute] [HTML parsing...]
defer:   [HTML parsing......download.............] [execute]

5. CSS Optimization

5.1 Critical CSS

<!-- Inline critical CSS -->
<head>
  <style>
    /* Only minimum CSS needed for above-the-fold content */
    body { margin: 0; font-family: system-ui; }
    .header { height: 60px; background: #fff; }
    .hero { height: 400px; display: flex; align-items: center; }
  </style>

  <!-- Load remaining CSS asynchronously -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>

5.2 CSS Containment

/* Limit rendering scope with contain */
.card {
  contain: layout style paint;
  /* Or optimize offscreen elements with content-visibility */
  content-visibility: auto;
  contain-intrinsic-size: 0 300px;
}

/* GPU acceleration hints with will-change */
.animated-element {
  will-change: transform;
  /* Warning: overuse wastes memory */
}

5.3 Remove Unused CSS

// PurgeCSS configuration (postcss.config.js)
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: [
        './src/**/*.{js,jsx,ts,tsx}',
        './public/index.html'
      ],
      defaultExtractor: content =>
        content.match(/[\w-/:]+(?<!:)/g) || [],
      safelist: ['html', 'body', /^data-/]
    })
  ]
};

6. Font Optimization

6.1 font-display Strategy

/* swap: fallback font → web font (FOUT occurs, text shown immediately) */
@font-face {
  font-family: 'MyFont';
  font-display: swap;
  src: url('/fonts/myfont.woff2') format('woff2');
}

/* optional: use web font if fast, keep fallback if slow (minimal CLS) */
@font-face {
  font-family: 'MyFont';
  font-display: optional;
  src: url('/fonts/myfont.woff2') format('woff2');
}

6.2 Font Preloading

<!-- Preload critical fonts -->
<link
  rel="preload"
  href="/fonts/inter-var.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

6.3 Variable Fonts

/* Traditional: separate file per weight (400, 500, 600, 700 = 4 files) */
/* Variable Font: single file for all weights */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

/* Usage */
.light { font-weight: 300; }
.regular { font-weight: 400; }
.bold { font-weight: 700; }

6.4 Font Subsetting

# Generate CJK font subset with pyftsubset
# pip install fonttools brotli
pyftsubset NotoSansKR-Regular.otf \
  --text-file=korean-chars.txt \
  --output-file=NotoSansKR-subset.woff2 \
  --flavor=woff2

# Result: 4.5MB → 300KB (with 2,350 Korean characters)

7. Rendering Patterns

7.1 Pattern Comparison Table

PatternTTFBFCPLCPTTISEOUse Cases
CSRFastSlowSlowSlowPoorSPA, Dashboards
SSRSlowFastFastSlowGoodDynamic content, SEO needed
SSGVery fastVery fastVery fastFastExcellentBlogs, docs, marketing
ISRVery fastVery fastVery fastFastExcellentE-commerce, news
Streaming SSRFastVery fastFastFastGoodComplex dynamic pages

7.2 CSR (Client-Side Rendering)

CSR flow:
Browser        Server
  │─ HTML req ────→│
  │←─ Empty HTML ──│
  │─ JS req ──────→│
  │←─ JS bundle ───│
[Parse/Execute]  │─ API req ─────→│
  │←─ Data ────────│
[Render]Display

7.3 SSR (Server-Side Rendering)

SSR flow:
Browser        Server
  │─ HTML req ────→│
  │                │ [Data fetching]
  │                │ [HTML rendering]
  │←─ Full HTML ───│
[Display]  │─ JS req ──────→│
  │←─ JS bundle ───│
[Hydration]Interactive

7.4 Streaming SSR

// Next.js App Router Streaming SSR
import { Suspense } from 'react';

async function SlowComponent() {
  const data = await fetchSlowData(); // Takes 3 seconds
  return <div>{/* render data */}</div>;
}

export default function Page() {
  return (
    <div>
      {/* Rendered immediately */}
      <Header />
      <Hero />

      {/* Streaming: sent progressively when ready */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>

      {/* Rendered immediately */}
      <Footer />
    </div>
  );
}

7.5 ISR (Incremental Static Regeneration)

// Next.js ISR
// app/products/[id]/page.tsx
export const revalidate = 3600; // Regenerate every hour

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

export default ProductPage;

// Generate static paths
export async function generateStaticParams() {
  const products = await getTopProducts(100);
  return products.map(p => ({ id: p.id }));
}

8. Caching Strategies

8.1 HTTP Cache

HTTP cache strategies:
├── Static assets (JS/CSS/images)
Cache-Control: public, max-age=31536000, immutable
   (filename includes hash: app.abc123.js)
├── HTML
Cache-Control: public, max-age=0, must-revalidate
   (always verify with server)
├── API responses
Cache-Control: private, max-age=60, stale-while-revalidate=300
   (60s cache, 5min stale allowed)
└── User-specific data
    Cache-Control: private, no-cache
    (verify with server every time)

8.2 stale-while-revalidate

stale-while-revalidate behavior:
Request 1: [Cache miss]ServerResponse + cache stored
Request 2: [Cache hit, fresh]Immediate response
Request 3: [Cache hit, stale]Immediate response (stale) + background refresh
Request 4: [Cache hit, fresh]Immediate response (updated data)
// SWR library (React)
import useSWR from 'swr';

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher, {
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
    refreshInterval: 30000, // Refresh every 30s
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error />;
  return <UserCard user={data} />;
}

8.3 Service Worker Cache

// Service Worker caching strategies
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles.css',
  '/app.js',
  '/offline.html'
];

// Install: pre-cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll(STATIC_ASSETS)
    )
  );
});

// Fetch: Cache First + Network Fallback
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached;

      return fetch(event.request).then(response => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache =>
          cache.put(event.request, clone)
        );
        return response;
      }).catch(() => {
        return caches.match('/offline.html');
      });
    })
  );
});

8.4 CDN Cache

CDN cache hierarchy:
User[Browser Cache][CDN Edge][CDN Origin Shield][Server]

CDN header examples:
Static assets:
  Cache-Control: public, max-age=31536000, immutable
  CDN-Cache-Control: public, max-age=31536000

Dynamic content:
  Cache-Control: public, max-age=0, must-revalidate
  CDN-Cache-Control: public, max-age=60, stale-while-revalidate=300
  Surrogate-Control: max-age=3600

9. Network Optimization

9.1 Resource Hints

<!-- DNS Prefetch: resolve external domain DNS early -->
<link rel="dns-prefetch" href="//api.example.com" />
<link rel="dns-prefetch" href="//cdn.example.com" />

<!-- Preconnect: DNS + TCP + TLS early connection -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Prefetch: preload next-page resources -->
<link rel="prefetch" href="/next-page.js" />

<!-- Preload: prioritize current-page critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/hero.webp" as="image" />

9.2 Priority Hints

<!-- fetchpriority for resource prioritization -->
<!-- Hero image: high priority -->
<img src="/hero.webp" fetchpriority="high" />

<!-- Offscreen image: low priority -->
<img src="/below-fold.webp" fetchpriority="low" loading="lazy" />

<!-- Critical script: high priority -->
<script src="/critical.js" fetchpriority="high"></script>

9.3 HTTP/2 and HTTP/3

HTTP/1.1 vs HTTP/2 vs HTTP/3:
┌──────────────┬──────────────┬──────────────┬──────────────┐
FeatureHTTP/1.1HTTP/2HTTP/3├──────────────┼──────────────┼──────────────┼──────────────┤
MultiplexingNoSupportedSupportedHeader CompNoneHPACKQPACKServer PushNoneSupportedRemovedHOL BlockingTCP level    │ TCP level    │ Solved(QUIC)TransportTCPTCPQUIC(UDP)ConnectionTCP+TLSTCP+TLS0-RTT able   │
└──────────────┴──────────────┴──────────────┴──────────────┘

10. Performance Monitoring

10.1 Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: './lighthouserc.json'
          uploadArtifacts: true
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.9 }],
        "categories:best-practices": ["warn", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "interactive": ["error", { "maxNumericValue": 5000 }]
      }
    }
  }
}

10.2 Real User Monitoring (RUM)

// Measure and report Web Vitals
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric: any) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });

  // Send asynchronously with Beacon API
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

10.3 Performance Dashboard

Performance monitoring tools:
├── Synthetic Monitoring (Lab Data)
│   ├── Lighthouse CI (automated)
│   ├── WebPageTest (detailed analysis)
│   └── PageSpeed Insights (Google)
├── Real User Monitoring (Field Data)
│   ├── Chrome UX Report (CrUX)
│   ├── web-vitals library
│   └── Commercial: Datadog RUM, New Relic
└── Bundle Analysis
    ├── webpack-bundle-analyzer
    ├── source-map-explorer
    └── bundlephobia.com

11. Next.js-Specific Optimizations

11.1 App Router and Server Components

// Server Component (default - NOT included in JS bundle)
// app/products/page.tsx
async function ProductsPage() {
  // Fetch data directly on server (no API route needed)
  const products = await db.product.findMany();

  return (
    <div>
      <h1>Products</h1>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

// Client Component (only for parts needing interaction)
// components/AddToCart.tsx
'use client';

import { useState } from 'react';

export function AddToCart({ productId }: { productId: string }) {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Add to Cart ({count})
    </button>
  );
}

11.2 Next.js Image Optimization

import Image from 'next/image';

// Auto WebP/AVIF conversion, responsive, lazy loading
function HeroSection() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority  // Set priority for LCP image
      sizes="(max-width: 768px) 100vw, 1200px"
      quality={85}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

// Remote images
function Avatar({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl}
      alt={user.name}
      width={48}
      height={48}
      loading="lazy"
    />
  );
}

11.3 Next.js Font Optimization

// Auto-optimization with next/font (self-hosted, zero CLS)
import { Inter, Noto_Sans_KR } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

const notoSansKR = Noto_Sans_KR({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  display: 'swap',
  variable: '--font-noto-sans-kr',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={`${inter.variable} ${notoSansKR.variable}`}>
      <body>{children}</body>
    </html>
  );
}

11.4 Route Segment Config

// app/blog/[slug]/page.tsx

// Static generation + ISR (60 seconds)
export const revalidate = 60;

// Or fully static
export const dynamic = 'force-static';

// Or always dynamic
export const dynamic = 'force-dynamic';

// Runtime selection
export const runtime = 'edge'; // Edge Runtime (fast TTFB)
// export const runtime = 'nodejs'; // Node.js Runtime (default)

12. Interview Questions

Basic Concepts

Q1. Explain the three Core Web Vitals metrics.
  1. LCP (Largest Contentful Paint): Time for the largest content element in the viewport to render. Target: under 2.5s. Measures loading performance.

  2. INP (Interaction to Next Paint): Delay from user interaction (click, keyboard, etc.) to the next paint. Target: under 200ms. Measures responsiveness.

  3. CLS (Cumulative Layout Shift): Total of all unexpected layout shifts during page load. Target: under 0.1. Measures visual stability.

Q2. Explain the differences between CSR, SSR, SSG, and ISR.

CSR (Client-Side Rendering): Browser renders with JS. Empty HTML sent, client renders everything.

  • Pros: Low server load, ideal for SPAs
  • Cons: Slow initial load, poor SEO

SSR (Server-Side Rendering): Server generates HTML per request.

  • Pros: Fast FCP, good SEO
  • Cons: Server load, slower TTFB

SSG (Static Site Generation): HTML pre-generated at build time.

  • Pros: Best performance, CDN caching
  • Cons: Build time, limited dynamic content

ISR (Incremental Static Regeneration): SSG + background regeneration.

  • Pros: SSG performance + data freshness
  • Cons: Implementation complexity
Q3. Explain the difference between Code Splitting and Tree Shaking.

Code Splitting: Dividing JS bundles into multiple chunks so only what is needed gets loaded. Split by route (dynamic import) or by component. Reduces initial load time.

Tree Shaking: Build-time optimization that removes unused (dead) code. Analyzes the static structure of ES Modules to remove un-imported exports. Reduces final bundle size.

Difference: Code Splitting is about "when to load" (When). Tree Shaking is about "what to remove" (What).

Q4. How do you improve LCP?
  1. Optimize server response: TTFB under 200ms, use CDN, caching
  2. Preload resources: Preload LCP image + fetchpriority="high"
  3. Image optimization: WebP/AVIF, proper sizing, compression
  4. Remove render blocking: Inline critical CSS, defer JS
  5. Font optimization: font-display: swap, preload
  6. Use SSR/SSG: Send complete HTML from server
  7. Third-party optimization: Lazy load external scripts
Q5. Explain CLS causes and solutions.

Main causes:

  1. Images/videos without dimensions
  2. Dynamically injected content (ads, banners)
  3. Web font loading layout shifts
  4. Late-loading CSS
  5. DOM-manipulating JavaScript

Solutions:

  1. Specify width/height or aspect-ratio CSS on img/video
  2. Reserve space for dynamic content with min-height
  3. font-display: optional, font preloading
  4. Inline critical CSS
  5. Use transform animations instead of top/left

Advanced Questions

Q6. Explain Service Worker caching strategies.
  1. Cache First: Check cache, fallback to network. Ideal for static assets.
  2. Network First: Try network, fallback to cache on failure. Ideal for API responses.
  3. Stale While Revalidate: Return cached response immediately + refresh in background. Ideal for frequently updated assets.
  4. Cache Only: Use cache exclusively. For offline assets.
  5. Network Only: Use network exclusively. For real-time data.

Criteria: Choose based on data freshness requirements and offline support needs.

Q7. Explain HTTP cache headers.
  • Cache-Control: Core caching policy header

    • max-age: Cache validity period (seconds)
    • no-cache: Server validation required each time
    • no-store: Never cache
    • public/private: CDN cacheability
    • immutable: Resource never changes
    • stale-while-revalidate: Allow stale response period
  • ETag: Resource version identifier. Used with conditional requests (If-None-Match).

  • Last-Modified: Last modification time. Used with If-Modified-Since.

Recommended strategy:

  • Static assets (with hash): max-age=31536000, immutable
  • HTML: no-cache or max-age=0, must-revalidate
Q8. Describe a comprehensive image optimization strategy.
  1. Format: Use WebP/AVIF, picture element for fallback
  2. Size: Responsive images (srcset + sizes), match actual display size
  3. Compression: Quality 75-85, adjust by use case
  4. Loading: LCP image uses eager + fetchpriority="high", rest use lazy
  5. Framework: Leverage auto-optimization tools like Next.js Image
  6. CDN: Use image CDNs (Cloudinary, imgix)
  7. Placeholder: Blur or LQIP for improved UX
Q9. How do you optimize INP (Interaction to Next Paint)?
  1. Break long tasks: Split 50ms+ tasks with scheduler.yield() or requestIdleCallback
  2. Free main thread: Move heavy computations to Web Workers
  3. Optimize event handlers: debounce/throttle, passive event listeners
  4. Minimize JS bundle: Code splitting, tree shaking
  5. Virtualization: Virtualize long lists with react-virtuoso
  6. startTransition: Mark non-urgent updates as transitions
  7. React optimization: Use useMemo, useCallback, React.memo appropriately
Q10. Explain how CDNs work and their performance benefits.

How they work:

  1. Cache content on Edge servers (PoPs) distributed globally
  2. Route user DNS requests to the nearest Edge
  3. If Edge has cache, respond immediately; otherwise, fetch from Origin and cache

Performance benefits:

  • Reduced physical distance: Lower RTT (Round Trip Time)
  • Origin server load distribution
  • DDoS protection
  • TLS optimization (TLS termination at Edge)
  • Automatic compression (Brotli, gzip)
  • HTTP/2, HTTP/3 support
Q11. Explain webpack code splitting configuration.

Code splitting with webpack's splitChunks plugin:

  1. Entry Points: Manual splitting via multiple entry points
  2. Dynamic Imports: Dynamic splitting via import()
  3. splitChunks: Automatic splitting rules

Key settings:

  • chunks: 'all' splits both sync and async
  • cacheGroups: Separate vendor (node_modules) and common (shared modules)
  • minSize: Minimum chunk size (default 20KB)
  • maxSize: Maximum chunk size (auto-split)

Benefits: Reduced initial load time, improved cache efficiency, load only needed code.

Q12. Explain the benefits and implementation of Streaming SSR.

Benefits:

  • Reduced TTFB: HTML sent progressively
  • Improved FCP: Display ready parts first
  • Isolate slow data sources: Wrap in Suspense for independent loading
  • Better UX: Show skeleton/loading states immediately

Implementation (Next.js App Router):

  • Use Server Components by default
  • Wrap slow components with Suspense
  • Provide skeleton UI as fallback
  • Automatically streams when data is ready

Key: Do not wait for the entire page to be ready; progressively send parts as they become available.

Q13. Explain the performance benefits of Next.js Server Components.
  1. Zero JS bundle: Server Components are not included in client JS bundle
  2. Direct data access: Access DB/API directly on server (no API routes needed)
  3. Automatic code splitting: Only Client Components are bundled
  4. Streaming: Progressive rendering combined with Suspense
  5. Caching: Server-side caching optimizes repeated requests
  6. Security: Sensitive logic/keys are not exposed to client

Principle: Components without interaction should be Server; only parts needing useState/useEffect should be Client.

Q14. How do you set and enforce a performance budget?

Setting:

  1. Competitor analysis: Measure key competitor performance metrics
  2. User device analysis: Understand average target user device/network
  3. Reflect business goals: Align with conversion rate and bounce rate targets
  4. Specific numbers: JS 200KB, CSS 50KB, LCP 2.5s, INP 200ms, etc.

Enforcement:

  1. Lighthouse CI for automated checks on every PR
  2. Fail build or warn on budget violations
  3. bundlesize or size-limit for bundle size limits
  4. Team dashboard for trend monitoring
  5. Regular budget review and adjustment
Q15. Describe web font optimization best practices.
  1. Variable Fonts: Single file for all weights/styles (fewer files)
  2. Subsetting: Include only needed characters (CJK: 4MB to 300KB)
  3. WOFF2 format: Best compression for web fonts
  4. font-display: swap (show text immediately) or optional (minimal CLS)
  5. preload: Early download critical fonts via link preload
  6. Self-hosting: Host fonts directly instead of Google Fonts (save DNS/connection cost)
  7. size-adjust: Match fallback and web font sizes to prevent CLS

13. Quiz

Q1. What is the "Good" threshold for LCP?

Answer: Under 2.5 seconds

LCP (Largest Contentful Paint) thresholds:

  • Good: Under 2.5s
  • Needs Improvement: 2.5-4.0s
  • Poor: Over 4.0s

LCP measures the time for the largest content element in the viewport to render.

Q2. Which Core Web Vitals metric replaced FID?

Answer: INP (Interaction to Next Paint)

Starting March 2024, FID (First Input Delay) was replaced by INP. While FID only measured the first interaction, INP measures all interactions throughout the entire page lifecycle, providing a more comprehensive responsiveness metric.

Q3. What does "immutable" mean in HTTP cache?

Answer: Indicates the resource will never change, preventing revalidation requests

Cache-Control: immutable tells the browser the resource will not change. This prevents the browser from sending revalidation (304) requests within the max-age period. Ideal for static assets with hashes in filenames (app.abc123.js).

Q4. What is the prerequisite for Tree Shaking to work?

Answer: ES Modules (import/export) must be used

Tree Shaking analyzes the static structure of ES Modules to remove unused exports. CommonJS (require/module.exports) is dynamic and cannot be tree-shaken. The "sideEffects": false setting in package.json is also important.

Q5. Are Next.js Server Components included in the client bundle?

Answer: No, Server Components are NOT included in the client JS bundle

Server Components run only on the server, and only the resulting HTML is sent to the client. This can significantly reduce JS bundle size. Only Client Components with the 'use client' directive are included in the bundle.


14. References

Official Documentation

Measurement Tools

Image Optimization

Performance Resources

Frameworks