[Daily morning study] 브라우저 렌더링 과정 (Critical Rendering Path)
#daily morning study
브라우저 렌더링 과정 (Critical Rendering Path)
브라우저가 HTML 파일을 받아서 화면에 픽셀을 그리기까지의 과정을 Critical Rendering Path(CRP)라고 한다. 이 과정을 이해하면 왜 특정 리소스가 렌더링을 막는지, 어떻게 최적화해야 하는지 파악할 수 있다.
전체 흐름 요약
HTML 파싱 → DOM 생성
CSS 파싱 → CSSOM 생성
↓
Render Tree 생성
↓
Layout (Reflow)
↓
Paint
↓
Compositing
1. HTML 파싱 → DOM 생성
브라우저는 서버에서 받은 HTML 바이트를 문자열로 변환하고, 토크나이저가 토큰으로 분리한 뒤 트리 구조의 DOM(Document Object Model)을 만든다.
- 태그를 만나면 노드를 생성하고 부모-자식 관계를 설정한다.
<script>태그를 만나면 파싱을 즉시 중단하고 스크립트를 실행한다. (렌더 블로킹)<img>같은 외부 리소스는 다운로드 요청만 보내고 파싱은 계속한다.
<!-- 파서는 이 구조를 DOM 트리로 변환한다 -->
<html>
<head><title>Hello</title></head>
<body>
<h1>제목</h1>
<p>본문</p>
</body>
</html>
2. CSS 파싱 → CSSOM 생성
CSS는 DOM과 별개로 CSSOM(CSS Object Model)을 만든다. DOM처럼 트리 구조이며, 선택자 특이성(Specificity)과 상속 관계를 적용한 최종 스타일 값을 담는다.
- CSS는 렌더 블로킹 리소스다. CSSOM이 완성될 때까지 Render Tree 생성을 기다린다.
- CSS 규칙은 오른쪽에서 왼쪽으로 파싱된다.
div .title p같은 선택자는 먼저p를 찾고,.title의 자식인지 확인하는 방식이다.
body { font-size: 16px; }
h1 { font-size: 2em; color: #333; } /* 32px 계산됨 */
3. Render Tree 생성
DOM과 CSSOM을 결합해서 화면에 실제로 보이는 요소만 남긴 Render Tree를 만든다.
display: none요소는 Render Tree에 포함되지 않는다.visibility: hidden은 공간을 차지하므로 포함된다.<head>,<script>,<meta>등 화면에 안 보이는 태그는 제외된다.
각 노드에는 적용된 CSS 스타일이 포함된다(Computed Style).
4. Layout (Reflow)
Render Tree의 각 노드가 화면 어디에, 얼마나 큰 크기로 배치될지 계산한다.
- 뷰포트 크기를 기준으로
%,em,vh등 상대적 단위를 픽셀로 환산한다. - 박스 모델(margin, padding, border)을 반영한다.
- 이 단계를 Reflow라고도 부른다.
Reflow는 비용이 크다. 한 요소의 크기가 바뀌면 부모 및 자식 요소까지 재계산해야 하기 때문이다.
// 이렇게 하면 Reflow가 여러 번 발생할 수 있다
el.style.width = '100px';
el.style.height = '200px';
el.style.top = '50px';
// 한 번에 변경하면 Reflow 횟수를 줄일 수 있다
el.style.cssText = 'width: 100px; height: 200px; top: 50px;';
5. Paint (Rasterization)
Layout 결과를 바탕으로 각 요소를 픽셀로 변환한다. 색상, 배경, 테두리, 텍스트, 그림자 등 시각적 속성을 레이어(Layer)에 그린다.
opacity,transform등 일부 속성은 별도 레이어를 만든다.- 레이어가 많아지면 메모리 사용량이 증가하므로 무분별하게
will-change를 남발하면 안 된다.
6. Compositing
여러 레이어를 GPU가 최종적으로 합성해서 화면에 출력한다.
transform,opacity변화는 Layout과 Paint를 건너뛰고 이 단계만 실행된다. → 애니메이션에 유리하다.top,left변화는 Layout부터 다시 실행된다. → 애니메이션에 비효율적이다.
/* Layout + Paint + Composite 전부 트리거 */
.bad { position: relative; top: 10px; }
/* Composite 단계만 트리거 */
.good { transform: translateY(10px); }
렌더 블로킹 리소스
| 리소스 | 블로킹 여부 | 이유 |
|---|---|---|
CSS (<link>) | 렌더 블로킹 | CSSOM 없이 Render Tree 못 만듦 |
JS (<script>) | 파서 블로킹 | 스크립트가 DOM을 수정할 수 있음 |
JS (async) | 비블로킹 | 다운로드는 병렬, 실행만 파싱 중단 |
JS (defer) | 비블로킹 | DOM 완성 후 실행, DOMContentLoaded 전 |
<!-- 파서 블로킹 -->
<script src="app.js"></script>
<!-- 다운로드만 병렬, 실행 시점에 파싱 중단 -->
<script src="app.js" async></script>
<!-- DOM 완성 후 실행 (순서 보장) -->
<script src="app.js" defer></script>
Reflow vs Repaint 차이
| 구분 | 트리거 속성 예시 | 재실행 단계 |
|---|---|---|
| Reflow | width, height, font-size, margin, top | Layout → Paint → Composite |
| Repaint | color, background-color, visibility | Paint → Composite |
| Composite only | transform, opacity | Composite |
Reflow가 가장 비용이 크고 Composite만 발생하는 게 가장 빠르다.
주요 최적화 기법
Critical CSS 인라인 삽입
첫 화면(above-the-fold)에 필요한 CSS만 <style> 태그로 인라인으로 넣으면 외부 CSS 다운로드를 기다리지 않아도 된다.
<head>
<style>/* 핵심 스타일만 인라인 */</style>
<link rel="stylesheet" href="full.css" media="print" onload="this.media='all'">
</head>
preload, prefetch
<!-- 현재 페이지에 곧 필요한 리소스 미리 로드 -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<!-- 다음 페이지에 필요할 것 같은 리소스 선제 다운로드 -->
<link rel="prefetch" href="next-page.js">
배치 DOM 업데이트 (DocumentFragment 활용)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `item ${i}`;
fragment.appendChild(li);
}
// DOM에는 한 번만 붙인다 → Reflow 1회
document.getElementById('list').appendChild(fragment);
정리
- CRP는 DOM → CSSOM → Render Tree → Layout → Paint → Composite 순서로 진행된다.
- CSS는 렌더 블로킹, 일반
<script>는 파서 블로킹 리소스다. transform,opacity기반 애니메이션은 GPU Composite 단계만 타서 성능이 좋다.top/left대신transform,color대신opacity를 활용하면 Reflow를 피할 수 있다.- DOM 조작은 배치(batch)로 처리해서 Reflow 횟수를 최소화한다.