전체 목록
브라우저Medium#97

브라우저의 Critical Rendering Path(CRP)를 설명해주세요.

#브라우저#CRP#렌더링#성능최적화
힌트

HTML 파싱부터 화면에 픽셀이 그려지기까지의 단계를 생각해보세요.

정답 및 해설

브라우저의 Critical Rendering Path(CRP)를 설명해주세요.

Critical Rendering Path(CRP, 핵심 렌더링 경로)는 브라우저가 HTML, CSS, JavaScript를 받아서 화면에 픽셀을 그리기까지의 과정입니다. 이 과정을 이해하면 웹 페이지의 초기 로딩 성능을 최적화하는 데 핵심적인 인사이트를 얻을 수 있습니다. CRP 최적화는 사용자가 페이지를 처음 볼 때까지 걸리는 시간(FCP, LCP)을 단축하는 데 직결됩니다.

CRP 전체 흐름

네트워크에서 HTML 수신
        ↓
1. HTML 파싱 → DOM 생성
        ↓ (동시에)
2. CSS 파싱 → CSSOM 생성
        ↓
3. DOM + CSSOM → Render Tree 생성
        ↓
4. Layout (Reflow) - 위치/크기 계산
        ↓
5. Paint - 픽셀 채우기
        ↓
6. Composite - 레이어 합성
        ↓
화면 출력

1단계: HTML 파싱 → DOM 생성

<!-- 브라우저가 HTML을 파싱하여 DOM(Document Object Model) 트리 생성 -->
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css">  <!-- CSS 파싱 시작 트리거 -->
    <script src="app.js"></script>            <!-- JS: DOM 파싱 차단! -->
  </head>
  <body>
    <h1>안녕하세요</h1>
    <p>본문입니다</p>
  </body>
</html>
DOM 트리:
Document
└── html
    ├── head
    │   ├── link (rel="stylesheet")
    │   └── script (src="app.js")
    └── body
        ├── h1 ("안녕하세요")
        └── p ("본문입니다")

JavaScript는 DOM 파싱을 차단한다

<!-- ❌ 기본 script 태그: DOM 파싱을 중단하고 JS 실행 후 재개 -->
<head>
  <script src="heavy-lib.js"></script>  <!-- 이 스크립트 다운로드+실행 동안 파싱 중단 -->
</head>
<body>
  <!-- heavy-lib.js 실행 완료 후 파싱 재개 -->
  <h1>여기까지 파싱이 지연됨</h1>
</body>

<!-- ✅ defer: DOM 파싱 완료 후 실행, DOMContentLoaded 전 실행 -->
<script src="app.js" defer></script>

<!-- ✅ async: 다운로드는 병렬, 다운로드 완료 즉시 실행 (파싱 차단 가능) -->
<!-- 실행 순서 보장 안 됨 - 독립적인 스크립트에 적합 -->
<script src="analytics.js" async></script>

<!-- ✅ 모듈 스크립트: 기본적으로 defer 동작 -->
<script type="module" src="app.js"></script>

2단계: CSS 파싱 → CSSOM 생성

/* CSS 파싱 → CSSOM(CSS Object Model) 생성 */
body {
  font-size: 16px;
  color: #333;
}

h1 {
  font-size: 2em;  /* 상속: body의 font-size 기반 → 32px */
}

p.highlight {
  color: blue;
}
CSSOM 트리:
body (font-size: 16px, color: #333)
├── h1 (font-size: 32px, color: #333 [상속])
└── p.highlight (color: blue)

CSS는 렌더링을 차단한다

<!-- CSS는 렌더링 차단 리소스 (Render-blocking resource) -->
<!-- CSSOM이 완성될 때까지 Render Tree 생성 불가 → 화면 출력 지연 -->

<!-- ❌ 느린 CSS 로드 -->
<link rel="stylesheet" href="large-styles.css">

<!-- ✅ 미디어 쿼리로 차단 조건 제한 -->
<!-- 프린트용 CSS는 화면 렌더링을 차단하지 않음 -->
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)">

<!-- ✅ Critical CSS 인라인화 -->
<head>
  <style>
    /* 위에 보이는 영역(above the fold)의 핵심 CSS만 인라인으로 포함 */
    body { margin: 0; font-family: sans-serif; }
    header { background: #fff; }
    .hero { display: flex; ... }
  </style>
  <!-- 나머지 CSS는 비동기로 로드 -->
  <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>

3단계: Render Tree 생성

DOM과 CSSOM을 결합하여 실제로 화면에 그려질 요소만 포함하는 Render Tree를 생성합니다.

DOM 트리 → Render Tree 변환 규칙:
- display: none  → Render Tree에서 제외 (공간 차지 없음)
- visibility: hidden → Render Tree에 포함 (공간은 차지, 보이지 않음)
- <head>, <script>, <style> → 화면에 표시 안 되므로 제외
- ::before, ::after 가상 요소 → 포함

DOM                    Render Tree
document               body
├── head (제외)         ├── h1 "안녕하세요" [font-size:32px, color:#333]
├── body                └── p "본문" [color:#333]
│   ├── h1
│   ├── p
│   └── div [display:none] (제외!)

4단계: Layout (Reflow) - 위치와 크기 계산

Render Tree의 각 노드가 화면에서 차지할 정확한 위치와 크기를 계산합니다.

// Reflow를 유발하는 CSS 속성들 (비용이 큼)
// 변경 시 전체 또는 일부 레이아웃 재계산 필요
const reflowProperties = [
  'width', 'height', 'margin', 'padding',
  'border', 'position', 'top', 'left', 'right', 'bottom',
  'display', 'float', 'overflow',
  'font-size', 'font-weight', 'line-height'
];

// ❌ Reflow를 강제로 발생시키는 코드 (레이아웃 스래싱)
function badAnimation() {
  const el = document.getElementById('box');
  for (let i = 0; i < 100; i++) {
    el.style.left = el.offsetLeft + 1 + 'px';  // 읽기(offsetLeft) + 쓰기(style.left) 반복
    // → 매번 Reflow 강제 발생!
  }
}

// ✅ Reflow 최소화
function goodAnimation() {
  const el = document.getElementById('box');
  let left = el.offsetLeft;  // 한 번만 읽기
  for (let i = 0; i < 100; i++) {
    left += 1;
  }
  el.style.left = left + 'px';  // 한 번만 쓰기
}

// ✅ CSS transform 사용 (Layout 단계 건너뜀)
el.style.transform = 'translateX(100px)';  // Composite 단계만 실행

5단계: Paint - 픽셀 채우기

Layout 단계에서 계산된 위치에 실제 픽셀을 그립니다. 색상, 배경, 그림자, 텍스트 등을 그립니다.

// Repaint를 유발하는 CSS 속성 (Layout은 변경 안 하고 그리기만 다시)
const repaintProperties = [
  'color', 'background-color', 'visibility',
  'outline', 'border-radius', 'box-shadow',
  'text-decoration'
];

// Repaint는 Reflow보다 비용이 적지만, 여전히 비쌈
// 가능하면 Composite 단계만 실행되도록 최적화

6단계: Composite - 레이어 합성

여러 레이어를 최종적으로 합성하여 화면에 출력합니다.

// GPU에서 처리 가능한 CSS 속성 (Composite 단계만 실행 → 가장 효율적)
const compositeOnlyProperties = [
  'transform',    // translateX, translateY, scale, rotate 등
  'opacity',      // 투명도
  'filter',       // blur 등 (일부)
];

// ✅ 애니메이션 최적화: transform과 opacity만 사용
.animated-element {
  /* ❌ 느림: width 변경은 Layout → Paint → Composite 순서로 실행 */
  transition: width 0.3s ease;

  /* ✅ 빠름: transform은 Composite 단계만 실행 (GPU 가속) */
  transition: transform 0.3s ease;
}

// will-change로 레이어 사전 생성 (과도한 사용 주의)
.panel {
  will-change: transform;  /* 이 요소가 변환될 것임을 브라우저에 힌트 */
}

렌더링 단계 트리거 비교

변경 속성        실행되는 단계
─────────────────────────────────────────
width, height   Layout → Paint → Composite  (가장 비쌈)
color           Paint → Composite
transform       Composite만                 (가장 저렴)
opacity         Composite만                 (GPU 가속)

CRP 최적화 전략

1. 렌더 차단 리소스 최소화

<!-- Critical CSS 인라인화 -->
<head>
  <style>
    /* Above the fold 영역의 핵심 스타일만 */
    body { margin: 0; }
    .header { height: 60px; background: white; }
    .hero { min-height: 400px; }
  </style>

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

<!-- JS defer/async 사용 -->
<script src="analytics.js" async></script>
<script src="app.js" defer></script>

2. 리소스 우선순위 힌트

<!-- 중요 리소스 미리 로드 -->
<link rel="preload" href="hero-image.jpg" as="image">
<link rel="preload" href="main-font.woff2" as="font" crossorigin>

<!-- 다음 페이지 리소스 미리 가져오기 (낮은 우선순위) -->
<link rel="prefetch" href="/next-page.html">

<!-- DNS 미리 조회 -->
<link rel="dns-prefetch" href="//api.example.com">

<!-- 연결 미리 맺기 -->
<link rel="preconnect" href="https://fonts.googleapis.com">

3. Reflow 최소화

// ✅ 여러 스타일 변경은 한 번에
const el = document.getElementById('box');

// ❌ 각각 Reflow 발생
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';

// ✅ 클래스로 한 번에 변경
el.classList.add('resized');

// ✅ DocumentFragment로 DOM 조작 최소화
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `항목 ${i}`;
  fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);  // DOM에 한 번만 추가

4. requestAnimationFrame 활용

// ✅ 애니메이션은 rAF로 처리 (브라우저 렌더링 주기에 맞춤)
function animate() {
  element.style.transform = `translateX(${position}px)`;
  position += 1;
  if (position < 300) {
    requestAnimationFrame(animate);  // 다음 프레임에 실행
  }
}
requestAnimationFrame(animate);

성능 측정 지표

지표의미목표
FCP (First Contentful Paint)첫 번째 콘텐츠 표시 시간1.8초 이하
LCP (Largest Contentful Paint)가장 큰 콘텐츠 표시 시간2.5초 이하
CLS (Cumulative Layout Shift)레이아웃 이동 누적 점수0.1 이하
TTI (Time to Interactive)완전히 인터랙션 가능한 시간3.8초 이하

정리

단계역할차단 요소최적화 방법
HTML 파싱DOM 트리 생성<script> 태그defer / async 속성
CSS 파싱CSSOM 트리 생성CSS 파일Critical CSS 인라인화
Render Tree표시할 요소 결정CSSOM 완성 전까지 대기불필요한 노드 최소화
Layout위치/크기 계산width, height 변경transform 사용, 배치 계산 최소화
Paint픽셀 채우기color, background 변경레이어 분리
Composite레이어 합성-GPU 가속 속성 (transform, opacity) 활용

핵심: CRP 최적화의 핵심은 "렌더 차단 리소스를 줄이고, Layout과 Paint 단계를 최소화하며, Composite 단계만 실행되는 CSS 속성(transform, opacity)으로 애니메이션을 구현하는 것"입니다.