전체 목록
브라우저Medium#043

브라우저의 렌더링 과정을 Critical Rendering Path 관점에서 설명해주세요.

#브라우저#렌더링#CRP#성능

답변 포인트

HTML 파싱 → CSSOM → DOM → Render Tree → Layout → Paint → Composite 순서를 떠올려보세요.

정답 및 해설

빠른 요약

1) HTML 파싱 → DOM 트리 생성 2) CSS 파싱 → CSSOM 트리 생성 3) DOM + CSSOM → Render Tree (보이는 요소만) 4) Layout (Reflow): 각 요소의 크기/위치 계산 5) Paint: 픽셀 그리기 6) Composite: 레이어 합성 최적화: - JS는 파싱 차단 → async/defer 사용 - CSS는 렌더 차단 → Critical CSS 인라인화 - Reflow 최소화 → transform/opacity로 애니메이션

Critical Rendering Path(CRP)는 브라우저가 HTML, CSS, JavaScript를 화면에 픽셀로 변환하는 일련의 단계를 의미합니다. 이 과정을 이해하면 페이지 로딩 성능을 최적화하고 First Contentful Paint(FCP), Largest Contentful Paint(LCP) 같은 Web Vitals 지표를 개선할 수 있습니다. CRP를 최적화하면 사용자가 콘텐츠를 더 빠르게 볼 수 있게 됩니다.

렌더링 과정 전체 흐름

브라우저
HTML 
   
HTML   DOM  
    (CSS   )
CSS   CSSOM  
   
DOM + CSSOM  Render Tree 
   
Layout (Reflow): / 
   
Paint:  
   
Compositing:  
   
 

1단계: HTML 파싱과 DOM 트리 생성

브라우저는 HTML 바이트를 문자로 변환하고, 토큰화 과정을 거쳐 DOM(Document Object Model) 트리를 생성합니다.

브라우저
Bytes  Characters  Tokens  Nodes  DOM Tree

:
<html>
  <head><title></title></head>
  <body>
    <h1 class="title"></h1>
    <p> </p>
  </body>
</html>

DOM :
Document
 html
     head
        title
            "페이지"
     body
         h1 (class="title")
            "안녕하세요"
         p
             "본문 내용"

HTML 파싱 중 JS/CSS 처리

HTML
<!-- CSS    (CSSOM    ) -->
<link rel="stylesheet" href="style.css" />

<!-- JS DOM   () -->
<script src="app.js"></script>

<!-- defer: DOM    () -->
<script src="app.js" defer></script>

<!-- async:     (DOM   ) -->
<script src="analytics.js" async></script>

<!--  : HTML    -->
<script>
  document.getElementById('app') // 이 시점에 아직 없을 수 있음
</script>

2단계: CSS 파싱과 CSSOM 트리 생성

CSS를 파싱하여 CSSOM(CSS Object Model) 트리를 생성합니다. CSSOM이 완성되기 전까지 렌더링이 차단됩니다.

브라우저
Bytes  Characters  Tokens  Nodes  CSSOM Tree

 CSS:
body { font-size: 16px; }
h1 { color: blue; font-size: 2em; }
h1.title { color: red; }
p { color: gray; }

CSSOM  (   ):
body
 font-size: 16px
 h1 (font-size: 32px, color: blue)
     .title (color: red )
     ( h1)

CSS가 렌더링을 차단하는 이유

JavaScript
// 브라우저가 렌더링을 차단하는 이유:
// CSS는 캐스케이딩 규칙에 의해 나중 스타일이 앞 스타일을 덮어쓸 수 있음
// CSSOM이 완전히 구성되어야 최종 스타일을 알 수 있음
// 불완전한 CSSOM으로 렌더링하면 화면이 깜빡이는 FOUC(Flash of Unstyled Content) 발생

// FOUC 방지 - CSS가 로드될 때까지 렌더링 차단
// <link rel="stylesheet"> 는 항상 렌더링 차단

3단계: Render Tree 생성

DOM과 CSSOM을 결합하여 실제로 화면에 표시될 요소들만 포함하는 Render Tree를 만듭니다.

브라우저
DOM Tree + CSSOM Tree  Render Tree

:
- display: none   (  )
- visibility: hidden   (   )
- <head>, <script>, <meta>    
- ::before, ::after    

:
DOM:                    CSSOM:              Render Tree:
html                    h1 { color: red }   html
 head ()          p { display:none }   body
 body                                         h1 (color:red)
     h1                                          "제목"
        "제목"                               (p display:none )
     p
         "본문"

4단계: Layout (Reflow)

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

JavaScript
// Layout을 유발하는 CSS 속성들 (비용이 큰 작업)
// width, height, margin, padding, border
// top, left, right, bottom (position)
// font-size, line-height
// display, float

// JavaScript에서 Layout을 강제 발생시키는 작업 (피해야 함)
const element = document.getElementById('box')

// 이런 속성 읽기는 최신 Layout 결과를 요구함
const width = element.offsetWidth
const height = element.offsetHeight
const rect = element.getBoundingClientRect()

// ❌ Layout Thrashing (성능 저하)
for (let i = 0; i < 100; i++) {
  element.style.width = (element.offsetWidth + 1) + 'px' // 매 반복마다 Layout
}

// ✅ Layout Thrashing 방지 - 읽기와 쓰기를 분리
const currentWidth = element.offsetWidth // 한 번에 읽기
for (let i = 0; i < 100; i++) {
  element.style.width = (currentWidth + i) + 'px' // 쓰기만
}

5단계: Paint

Layout에서 계산된 위치/크기를 기반으로 실제 픽셀을 채웁니다. 텍스트, 색상, 이미지, 테두리, 그림자 등을 그립니다.

JavaScript
// Paint를 유발하는 CSS 속성들 (Layout보다는 저렴)
// color, background-color, background-image
// border-color, border-radius
// box-shadow, text-shadow
// outline

// GPU 가속을 활용하는 속성들 (Paint 없이 Compositing만 발생 - 가장 빠름)
// transform: translate(), scale(), rotate()
// opacity
// will-change: transform (레이어 분리 힌트)

// ✅ 애니메이션 최적화 - transform 사용 (Paint 없이 Compositing만)
.animated-element {
  transform: translateX(0);
  transition: transform 0.3s ease;
}
.animated-element:hover {
  transform: translateX(100px); /* Reflow, Repaint 없이 Compositing만 */
}

/* ❌ 느린 애니메이션 - left 사용 (Reflow + Repaint 발생) */
.slow-element {
  left: 0;
  transition: left 0.3s ease;
}
.slow-element:hover {
  left: 100px; /* Layout + Paint 재실행 */
}

6단계: Compositing (컴포지팅)

여러 레이어를 합쳐 최종 화면을 구성합니다. 특정 CSS 속성은 별도 레이어를 생성하여 GPU에서 처리됩니다.

CSS
/* 레이어 생성을 유발하는 속성들 */
.new-layer {
  /* 아래 속성들은 브라우저가 새 합성 레이어를 생성 */
  transform: translateZ(0); /* 또는 translate3d(0, 0, 0) */
  will-change: transform, opacity;
  position: fixed; /* fixed 요소는 별도 레이어 */
}

/* 레이어를 미리 생성하여 애니메이션 준비 */
.hero-image {
  will-change: transform; /* 브라우저에게 "이 요소는 변환될 것임" 힌트 */
}

렌더링 차단 리소스 최적화

CSS 최적화

HTML
<!--     CSS   -->
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="print.css" media="print" /> <!--    -->
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)" />

<!--  Critical CSS  -->
<style>
  /* Above-the-fold에 필요한 최소한의 CSS만 인라인 */
  body { margin: 0; font-family: sans-serif; }
  .hero { background: #000; color: #fff; height: 100vh; }
</style>
<!--  CSS   -->
<link rel="preload" href="main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

JavaScript 최적화

HTML
<!--     -->
<head>
  <script src="large-library.js"></script> <!-- DOM  ! -->
</head>

<!--  defer: DOM   , DOMContentLoaded   -->
<script src="app.js" defer></script>

<!--  async:     (  ,   ) -->
<script src="analytics.js" async></script>

<!--    defer -->
<script type="module" src="app.mjs"></script>

<!--   import   -->
<script>
  // 필요할 때만 로드
  button.addEventListener('click', async () => {
    const { heavyFeature } = await import('./heavy-feature.js')
    heavyFeature()
  })
</script>

리소스 힌트

HTML
<head>
  <!-- preconnect: DNS  + TCP  + TLS    -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />

  <!-- dns-prefetch: DNS    (preconnect ) -->
  <link rel="dns-prefetch" href="https://cdn.example.com" />

  <!-- preload:        -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
  <link rel="preload" href="/hero.jpg" as="image" />

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

Reflow와 Repaint 최소화

JavaScript
// ❌ 여러 번 DOM 조작 (매번 Reflow 유발)
const el = document.getElementById('box')
el.style.width = '100px'   // Reflow
el.style.height = '100px'  // Reflow
el.style.margin = '10px'   // Reflow

// ✅ 한 번에 스타일 적용
el.style.cssText = 'width: 100px; height: 100px; margin: 10px;'

// ✅ 또는 클래스 변경으로 한 번에 처리
el.classList.add('sized-box')

// ✅ DocumentFragment 사용 - DOM 조작을 메모리에서 처리
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li')
  li.textContent = `아이템 ${i}`
  fragment.appendChild(li) // 메모리에서만 조작 (Reflow 없음)
}
document.getElementById('list').appendChild(fragment) // 한 번만 DOM에 추가

// ✅ 요소를 숨기고 조작한 후 다시 표시
const el = document.getElementById('complex-element')
el.style.display = 'none'  // Reflow (한 번)
// ... 많은 DOM 조작 ...
el.style.display = 'block' // Reflow (한 번)
// 중간 조작들은 화면에 반영 안 됨

브라우저 DevTools로 CRP 분석

브라우저
Chrome DevTools Performance  :
1. F12  Performance 
2. Record     
3.  :
   - Parsing HTML (): DOM/CSSOM 
   - Layout (): Reflow 
   - Paint (): Repaint 
   - Composite Layers: 

Lighthouse CRP :
- Render-blocking resources  
- Critical Request Chains 
- Unused CSS/JS 

정리 표

단계작업렌더링 차단 여부최적화 방법
HTML 파싱DOM 트리 생성JS에 의해 차단defer, async 사용
CSS 파싱CSSOM 트리 생성렌더링 차단Critical CSS 인라인화, 미디어 쿼리
Render TreeDOM + CSSOM 결합-불필요한 요소 display:none
Layout위치/크기 계산느린 단계transform 사용, Layout Thrashing 방지
Paint픽셀 채우기중간 비용will-change, GPU 가속 활용
Compositing레이어 합성빠른 단계transform, opacity로 직접 처리

관련 질문

같은 카테고리/태그 기준