본문으로 건너뛰기

브라우저의 렌더링 과정:Critical Rendering Path

CRP: Critical Rendering Path

웹 페이지를 화면에 표시하기 위해 브라우저가 수행하는 일련의 단계를 CRP(Critical Rendering Path)라고 한다.

전체 렌더링 플로우 상세 분석

1. HTML 파싱 → DOM 생성

브라우저가 HTML 문서를 받으면 가장 먼저 HTML을 파싱하여 DOM 트리를 구축한다. DOM(Document Object Model)은 웹 페이지의 객체 기반 표현이며, JavaScript로 접근하고 조작할 수 있는 인터페이스를 제공한다.

<html>
<head>
<title>렌더링 예제</title>
</head>
<body>
<div id="app">
<h1>안녕하세요</h1>
<p>브라우저 렌더링 프로세스</p>
</div>
</body>
</html>

위 HTML은 다음과 같은 DOM 트리로 변환된다.

html
├── head
│ └── title
│ └── "렌더링 예제"
└── body
└── div#app
├── h1
│ └── "안녕하세요"
└── p
└── "브라우저 렌더링 프로세스"

HTML 파싱 과정은 점진적(incremental)으로 이루어지며, DOM 구축이 완료되기 전에도 화면 일부가 렌더링될 수 있다. 이것이 사용자가 웹 페이지가 로드되는 동안 콘텐츠가 점진적으로 나타나는 것을 볼 수 있는 이유이다.

2. CSS 파싱 → CSSOM 생성

브라우저는 HTML문서에서 참조하는 모든 CSS를 다운로드하고 파싱하여 CSSOM(CSS Object Model)을 생성한다. CSSOM은 DOM과 유사한 트리 구조를 가지지만, 페이지 요소의 스타일 정보를 포함한다.

body {
font-family: Arial, sans-serif;
}
#app {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #333;
}
p {
color: #666;
}

CSS 특성 중 하나는 '계단식(cascading)'이라는 점이다. 상위 요소의 스타일이 하위 요소에 상속되며, 명시도(specificity)와 소스 순서에 따라 적용되는 규칙이 결정된다. 때문에 CSSOM 구축 과정은 DOM보다 복잡할 수 있다.

CSSOM 구축은 렌더 차단(render-blocking) 프로세스로, CSS가 완전히 파싱되기 전까지 브라우저는 렌더링을 진행하지 않는다. 이는 CSS가 웹 페이지의 시각적 표현에 필수적이기 때문이다.

3. DOM + CSSOM → Render Tree

DOM과 CSSOM이 구축되면, 브라우저는 이를 결합하여 렌더 트리(Render Tree)를 생성한다. 렌더 트리는 화면에 표시될 내용만을 포함한다.

렌더 트리 구축 과정에서 주목할 점:

  • display: none;이 적용된 요소는 렌더 트리에 포함되지 않는다.

  • visibility: hidden;이 적용된 요소는 공간을 차지하지만 보이지 않으므로 렌더 트리에 포함된다.

  • head요소와 같이 시각적으로 표시되지 않는 요소는 렌더 트리에 포함되지 않는다.

  • 생성된 콘텐츠(pseudo-elements)는 DOM에는 없지만 렌더 트리에는 포함된다.

4. Layout(Reflow)

렌더 트리가 구축되면, 브라우저는 각 요소의 정확한 위치와 크기를 계산하는 레이아웃(Layout) 또는 리플로우(Reflow) 프로세스를 시작한다. 이 과정에서는 뷰포트 크기, 요소의 박스 모델, 상대적 위치와 같은 요소들이 고려된다.

레이아웃은 컴퓨팅 리소스를 많이 소모하는 작업으로, 특히 복잡한 레이아웃과 대규모 DOM에서 더욱 그렇다. 이것이 웹 성능 최적화에서 불필요한 리플로우를 최소화하는 것이 중요한 이유이다.

레이아웃이 시작되는 주요 트리거:

  • 초기 페이지 렌더링
  • 뷰포트 크기 변경 (화면 크기 조정)
  • DOM 요소 추가, 제거, 크기 또는 위치 변경
  • 글꼴 변경, 이미지 크기 변경 등

5. Paint

레이아웃 단계가 완료되면, 브라우저는 각 요소를 화면에 그리는 페인트(Paint) 단계를 진행한다. 이 과정에는 텍스트, 색상, 이미지, 경계선, 그림자 등 요소의 시각적 부분을 그리는 작업이 포함된다.

복잡한 요소나 효과가 많은 페이지는 이 단계에서 더 많은 처리 시간이 필요하다. 페인트 과정은 일반적으로 여러 레이어로 나뉘어 처리된다.

6. Composite → GPU

최신 브라우저는 페이지의 각 부분을 별도의 레이어로 페인트한 다음, 이를 합성(Composite)하여 최종 화면을 생성한다. 이 과정은 GPU를 활용하여 가속화될 수 있다.

레이어 합성을 통해 브라우저는 일부 애니메이션이나 변환을 더 효율적으로 처리할 수 있다. 예를 들어, transformopacity 속성 변경은 리플로우나 리페인트 없이 합성 단계에서만 처리될 수 있다.

/* GPU 가속을 활용하는 CSS 예시 */
.animated-element {
transform: translateZ(0); /* 요소를 새 레이어로 승격 */
will-change: transform; /* 브라우저에게 변환이 발생할 것임을 알림 */
transition: transform 0.3s ease; /* 부드러운 애니메이션 */
}

렌더링 플로우 시각화

CRP 최적화를 위한 주요 포인트

Render-blocking 리소스 최소화

CSS는 렌더 차단 리소스로, HTML 파싱 중에 외부 스타일시트가 발견되면 브라우저는 해당 리소스를 다운로드하고 처리할 때까지 렌더링을 중단한다. 이를 최적화하기 위한 방법은 아래와 같다.

1. CSS 최적화

  • 필요하지 않은 CSS 제거
  • CSS파일 최소화 및 압축
  • 중요 CSS를 인라인으로 포함하고 나머지는 비동기적으로 로드
<!-- 중요 CSS를 인라인으로 포함 -->
<style>
/* 첫 화면 렌더링에 필요한 최소한의 스타일 */
body {
font-family: Arial, sans-serif;
}
.hero {
/* ... */
}
</style>

<!-- 비동기적으로 나머지 CSS 로드 -->
<link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="main.css" /></noscript>

2. JavaScript 최적화

  • <script> 태그에 async 또는 defer 속성 사용
  • 코드 분할(Code Splitting) 및 지연 로딩(Lazy Loading) 적용
  • 불필요한 JavaScript 제거
<!-- 비동기적으로 JavaScript 로드 -->
<script src="analytics.js" async></script>
<!-- 페이지 파싱 후 실행할 스크립트 -->
<script src="app.js" defer></script>

Lazy Loading(이미지, 폰트)

모든 리소스를 즉시 로드하는 대신, 사용자에게 필요할 때 로드하는 전략이다.

1. 이미지 지연 로딩

<!-- 네이티브 지연 로딩 -->
<img src="image.jpg" loading="lazy" alt="지연 로딩된 이미지" />

<!-- 자바스크립트 지연 로딩 (Intersection Observer 사용) -->
<img data-src="image.jpg" class="lazy" alt="지연 로딩된 이미지" />
// Intersection Observer를 사용한 이미지 지연 로딩
document.addEventListener("DOMContentLoaded", function () {
const lazyImages = document.querySelectorAll("img.lazy");

const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove("lazy");
observer.unobserve(img);
}
});
});

lazyImages.forEach((img) => imageObserver.observe(img));
});

2. 폰트 최적화

/* 폰트 디스플레이 옵션 */
@font-face {
font-family: "MyFont";
src: url("myfont.woff2") format("woff2");
font-display: swap; /* 폰트 로딩 중 시스템 폰트 사용 */
}

preload, prefetch, dns-prefetch 태그

브라우저에게 리소스 로딩의 우선순위를 알려주는 리소스 힌트를 제공할 수 있다.

<!-- 현재 페이지에 중요한 리소스 미리 로드 -->
<link
rel="preload"
href="critical-font.woff2"
as="font"
type="font/woff2"
crossorigin
/>

<!-- 다음 페이지에 필요할 것 같은 리소스 미리 가져오기 -->
<link rel="prefetch" href="next-page.js" />

<!-- 외부 도메인 DNS 조회 미리 수행 -->
<link rel="dns-prefetch" href="https://api.example.com" />

<!-- 외부 도메인에 대한 연결 미리 수립 -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

각 리소스 힌트의 특성

  • preload: 현재 페이지에 필요한 중요 리소스를 높은 우선순위로 로드
  • prefetch: 미래에 필요할 수 있는 리소스를 낮은 우선순위로 로드
  • dns-prefetch: 외부 도메인의 DNS 조회를 미리 수행
  • preconnect: DNS 조회, TCP 핸드셰이크, TLS 협상 등을 미리 수행

Frame Budget 개념

웹 애플리케이션의 성능에서 프레임 속도는 사용자 경험에 직접적인 영향을 미친다. 60FPS(Frame Per Second)를 목표로 할 때, 각 프레임은 약 16.66ms 이내에 완료되어야 한다.

60FPS 기준: 16.66ms 안에 모든 연산이 끝나야 함

프레임 버짓은 한 프레임을 렌더링하는 데 사용할 수 있는 시간을 의미한다. 부드러운 애니메이션과 사용자 인터렉션을 위해서느 16.66ms 내에 모든 JavaScript 실행, 스타일 계산, 레이아웃, 페인트, 합성 작업이 완료되어야 한다.

브라우저의 메인 스레드는 대부분의 렌더링 작업을 처리하며, 이 스레드가 차단되면 프레임이 지연되거나 건너뛸 수 있다. 이것이 바로 프레임 드롭(Frame Drop)이 발생하는 이유이다.

프레임 버짓의 일반적인 분배

  • JavaScript 실행: ~ 3.5ms
  • 스타일 계산 및 레이아웃: ~ 3.5ms
  • 페인트 및 래스터화: ~ 3.5ms
  • 브라우저 오버헤드: ~ 6ms

Long Task, Dropped Frame 원인 분석

Long Task(긴 작업)

브라우저의 메인 스레드를 50ms 이상 차단하는 작업을 Long Task라고 한다. 이는 사용자 인터렉션 지연과 프레임 드롭의 주요 원인이 된다.

Long Task의 일반적인 원인

  • 대용량 JavaScript 번들
  • 복잡한 계산 또는 동기 작업
  • 대규모 DOM 조작
  • 복잡한 CSS 선택자
  • 비효율적인 레이아웃 강제 동기화(Forced Synchronous Layout)

Long Task를 감지하고 분석하기 위한 도구

  • Performance API의 Long Tasks API
  • Chrome DevTools의 Performance 패널
  • Lighthouse 성능 감사
// Long Task 감지 예시
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Long Task detected: ${entry.duration}ms`);
}
});

observer.observe({ entryTypes: ["longtask"] });

Dropped Frame(프레임 드롭)

프레임 드롭은 브라우저가 설정된 시간 내에 프레임을 렌더링하지 못할 때 발생한다. 60FPS를 목표로 할 때, 프레임 드롭이 발생하면 사용자는 끊김이나 버벅거림을 경험한다.

프레임 드롭의 일반적인 원인

  • 과도한 리플로우(reflow)
  • 복잡한 페인트 작업
  • 비효율적인 이벤트 핸들러
  • 제한된 하드웨어 리소스

프레임 드롭을 최소화하기 위한 최적화 기법

  • requestAnimationFrame을 사용하여 시각적 변경 일정 조정
  • 비용이 많이 드는 작업을 Web Worker로 이동
  • GPU 가속을 활용한 합성 최적화
  • 복잡한 애니메이션에는 CSS 트랜지션 및 변환 사용
  • 큰 DOM 변경은 일괄 처리(batching) 하기
// requestAnimationFrame을 사용한 효율적인 애니메이션
function animate() {
// 시각적 변경 수행
element.style.transform = `translateX(${position}px)`;

// 다음 프레임 예약
requestAnimationFrame(animate);
}

// 애니메이션 시작
requestAnimationFrame(animate);

결론

브라우저의 렌더링 과정(CRP)을 이해하는 것은 웹 성능 최적화의 기본이다. DOM 생성, CSSOM 생성, 렌더 트리 구축, 레이아웃, 페인트, 합성 단계를 거쳐 웹 페이지가 화면에 표시된다.

성능 최적화를 위해서는,

  • 렌더 차단 리소스를 최소화하고 최적화
  • 불필요한 리플로우와 리페인트 방지
  • 효율적인 리소스 로딩 전략(지연 로딩, 리소스 힌트) 사용
  • 60FPS를 목표로 하는 프레임 버짓 관리
  • 필요에 따라 Virtual DOM과 같은 추상화 활용

이러한 원칙을 적용하면 더 빠르고 부드러운 웹 애플리케이션을 구축할 수 있다.

참고