Critical Rendering Path
Critical Rendering Path(CRP)는 브라우저가 HTML, CSS, JavaScript를 파싱해 화면에 픽셀을 그리기까지의 단계별 렌더링 파이프라인입니다. HTML Parse → DOM → CSSOM → Render Tree → Layout → Paint → Composite 각 단계를 이해하면 첫 화면이 왜 느린지, 어디를 최적화해야 하는지 파악할 수 있습니다.
▶아키텍처 다이어그램
🔄 프로세스 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
브라우저가 HTML 파일을 받은 뒤 화면에 첫 픽셀이 나타나기까지 걸리는 시간이 왜 긴지 직관적으로 파악하기 어렵습니다. CSS 파일 하나가 스타일시트 위치에 있다는 이유만으로 그 아래 모든 HTML 렌더링을 막고 있거나, `<head>`에 넣은 `<script>` 하나가 파싱을 멈추게 한다는 사실은 소스를 봐서는 바로 보이지 않습니다. 어디서 시간이 빠져나가는지 모르면 성능 측정 수치를 봐도 어디를 손봐야 하는지 알기 어렵습니다.
초기 웹 브라우저는 HTML을 위에서 아래로 읽으며 만나는 대로 화면을 그려나갔습니다. 스타일과 스크립트가 인라인이거나 없을 때는 이 선형 처리가 단순하고 직관적이었습니다. 외부 CSS 파일과 JavaScript가 표준화되면서 브라우저는 스타일이 확정되지 않은 상태로 화면을 그리면 깜빡임이 생긴다는 문제를 마주했고, CSSOM이 완성될 때까지 Render Tree 생성을 기다리는 방식을 선택했습니다. JavaScript가 DOM을 동적으로 조작할 수 있게 되면서 파싱 도중 스크립트를 만나면 파서를 멈추는 규칙도 생겼습니다. 이 결정들이 쌓여 지금의 Critical Rendering Path가 됐습니다.
브라우저는 HTML 바이트를 받으면 토큰으로 변환하고 DOM 트리를 만듭니다. 동시에 `<link rel="stylesheet">`를 만나면 CSS 파일을 가져와 CSSOM 트리를 별도로 만듭니다. CSS는 render-blocking 리소스입니다. CSSOM이 완성되지 않으면 Render Tree를 만들 수 없어서, CSS 파일이 늦게 오거나 크면 그만큼 첫 화면이 늦어집니다. `<script>` 태그는 파서 차단 리소스이기도 합니다. 브라우저는 스크립트가 DOM을 바꿀 수 있다고 가정하므로, `defer`나 `async` 속성이 없으면 HTML 파싱을 멈추고 스크립트가 실행될 때까지 기다립니다. DOM과 CSSOM이 준비되면 둘을 합쳐 Render Tree를 만듭니다. `display: none`인 요소는 Render Tree에서 제외됩니다. Layout(또는 Reflow) 단계에서 각 노드의 크기와 위치를 계산하고, Paint에서 색상과 경계를 픽셀로 채웁니다. 마지막으로 Composite 단계에서 GPU가 레이어를 합성해 화면에 출력합니다. JavaScript로 DOM을 읽고 쓰기를 반복하면 Layout이 여러 번 재실행되는 Layout Thrashing이 발생합니다. 읽기 작업을 모아서 먼저 하고 쓰기 작업을 나중에 하면 이 낭비를 줄일 수 있습니다.
CRP와 DOM은 밀접하게 연결되어 있지만 다루는 범위가 다릅니다. DOM은 HTML 문서를 메모리에서 다루는 구조 자체이고, CRP는 DOM이 생성되는 과정부터 화면 출력까지 전체 파이프라인입니다. DOM을 이해하면 CRP에서 어떤 조작이 Layout이나 Paint를 유발하는지, 왜 그 비용이 발생하는지 인과를 따라갈 수 있습니다. SSR(Server-Side Rendering)과의 차이도 여기서 드러납니다. SSR은 서버에서 HTML을 미리 완성해 보내므로 브라우저가 빈 HTML로 시작하는 SPA보다 초기 DOM 구성이 빠릅니다. 하지만 CSS와 JavaScript 로딩으로 인한 render-blocking은 SSR에서도 동일하게 발생합니다. CRP 최적화는 렌더링 방식과 무관하게 적용됩니다. `transform`이나 `opacity` 같은 CSS 속성은 Composite 단계만 트리거하므로 Layout이나 Paint를 건드리지 않아 애니메이션에 유리합니다. 반면 `width`, `height`, `margin`, `top` 같은 속성은 Layout을 다시 계산하게 만들어 성능 비용이 큽니다.
CRP 이해가 실제로 필요해지는 상황은 Lighthouse에서 Render-Blocking Resources 경고가 뜨거나, FCP나 LCP가 목표보다 높게 잡힐 때입니다. 원인을 파악하려면 어떤 CSS와 JS 파일이 첫 화면 렌더링 전에 반드시 완료되어야 하는지 추려야 합니다. `<link rel="stylesheet">`는 기본이 render-blocking이므로, 첫 화면에 필요 없는 스타일을 별도 파일로 분리하고 `media` 속성이나 동적 삽입으로 non-blocking 처리를 고려할 수 있습니다. `<script>` 태그는 `defer`나 `async`를 붙이는 것만으로도 HTML 파싱 차단을 피할 수 있습니다. JavaScript로 레이아웃 관련 속성을 읽는 작업과 변경하는 작업이 섞여 있으면 브라우저가 강제로 Reflow를 중간중간 실행합니다. DevTools의 Performance 탭에서 Layout이 비정상적으로 자주 나타나면 이 패턴을 의심할 수 있습니다. 모든 단계를 최적화할 필요는 없습니다. 첫 화면 진입 지표가 목표 범위 안에 있다면, Composite만 발생하는 애니메이션과 defer 처리된 JS 정도로도 대부분의 요구사항을 충족합니다.