avatar
Published on

주요 렌더링 경로 - 브라우저의 원리를 이해하고 최적화 하기

Author
  • avatar
    Name
    yceffort

Critical Rendering Path를 요약했습니다. 이글을 보는게 더 나아요 사실

Table of Contents

객체 모델 생성

브랑루저가 페이지를 렌더링하기 위해서는 DOM과 CSSOM 트리를 생성해야 한다. 따라서 최대한 빠르게 이를 생성해서 넘겨야 한다.

먼저 바이트 코드로 넘어온 HTML과 CSS를 어떻게 처리하는지 알아야 한다.

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/full-process.png?hl=ko

  1. 바이트
  2. 문자
  3. 토큰
  4. 노드
  5. 객체모델

HTML은 DOM(Document Object Model)로, CSS는 CSSOM(CSS Object Model)로 변환된다. 이 둘은 독립적인 데이터 구조다.

이 DOM을 기준으로 브라우저는 이후 모든 페이지 처리에 이 DOM을 사용한다.

렌더링 트리 생성과 레이아웃 프린트

  • DOM과 CSSDOM 이 만들어졌다면, 이 두 트리를 결합하여 렌더링 트리를 생성한다.
  • 이 렌더링 트리에는 페이지를 렌더링하는데 필요한 노드만 포함된다. 예를 들어서 display:none으로 처리된 것은 렌더링 처리에서 누락된다.
  • 레이아웃 단계에서는, 각 객체의 정확한 위치와 크기를 계산한다.
  • 마지막으로 페인트 단계를 거치는데, 픽셀을 화면에 렌더링한다.

브라우저의 렌더링 단계 요약

  1. HTML 을 처리해서 DOM 트리를 만든다
  2. CSS 를 처리해서 CSSOM 트리를 만든다.
  3. DOM과 CSSOM을 결합하여 렌더링 트리를 만든다
  4. 렌더링 트리에서 레이아웃 처리를 통하여 각 객체의 위치와 크기를 계산한다
  5. 마지막으로 개별노드를 페인트 한다.

주요 렌더링 경로를 최적화 하는 작업은 위 다섯가지의 단계를 최소화 하는 프로세스다.

HTML과 CSS는 기본적으로 렌더링을 차단한다.

CSSOM이 생성되기 전까지, 브라우저는 처리되는 모든 컨텐츠를 렌더링하지 않는다. 따라서 가능한 간단하고 빠르게 제공해야 렌더링 차단을 최소화 할 수 있다. HTML도 마찬가지로, 렌더링을 차단하는 리소스다.

하지만 미디어 유형과 미디어 쿼리를 통해서 일부 리소스를 렌더링을 차단하지 않는 리소스로 선언할 수 있다.

<!-- 기본적으로 렌더링을 차단한다. -->
<link href="style.css" rel="stylesheet" />
<!-- 위의 선언과 같다. -->
<link href="style.css" rel="stylesheet" media="all" />
<!-- 컨텐츠가 인쇄될 때만 적용된다. 따라서 렌더링이 차단되지 않는다.-->
<link href="print.css" rel="stylesheet" media="print" />
<!-- 브라우저가 해당 조건을 만족하면 차단된다. -->
<link href="other.css" rel="stylesheet" media="(min-width: 40em)" />

한 가지 중요한 것은, 위 미디어 쿼리가 있다고 하더라도 해당 리소스에 대해 초기 렌더링을 보류해야 하는지만 나타낸다. 어떤 경우든지, 브라우저는 CSS를 모두 다운받으며, 단지 초기 렌더링을 보류 해야하는지만을 나타낸다.

자바스크립트로 상호작용 추가

자바스크립트를 활용하면 페이지 대부분의 측면을 수정할 수 있다. DOM 요소를 추가하고 제거하거나, CSSOM 속성을 수정하는 등 거의 모든 측면에 관여할 수 있다.

이러한 점 때문에, 인라인 스크립트를 실행하면 DOM 생성이 차단되고, 이로 인해 초기 렌더링에도 지연이 발생하게 된다. 자바스크립트로는 DOM, CSSDOM 등에 관여할 수 있기 때문에 렌더링하는데 있어 지연이 발생할 수 있다.

따라서

  • 문서의 스크립트 위치는 중요하다. 위치에 따라서 실행이 달라진다.
  • 브라우저가 스크립트 태그를 만나면 스크립트가 종료될때까지 DOM 생성이 중단된다.
  • 자바스크립트는 CSSOM이 준비될 때까지 일시 중단된다.

따라서 기본적으로, 자바스크립트의 실행은 파서를 차단한다. 그리고 만약 인라인 자바스크립트가 아닌, 스크립트 태그를 통해 포함된 자바스크립트가 있다면, 브라우저가 일시 중단하고, 디스크 / 캐시 / 원격 서버등에서 스크립트를 가져올 때 까지 기다리므로, 추가적인 지연이 더 발생하게 된다.

이를 방지 하기 위해서는 async 키워드를 추가하면 된다.

<script src="app.js" async></script>

이를 추가하면, 스크립트가 사용가능 해질 때까지 기다리는 동안에도 DOM 생성을 계속해서 하게된다. 이경우 성능이 향상된다.

https://kimlog.me/static/7b56046cd820d53017f5fa7124ba2255/44a54/script_load.png

일반적인 script는 다운로드하고 실행될 때까지 HTML 파싱을 멈추지만, async는 다운로드와 파싱을 동시에 진행한다. defer의 경우에는, 마찬가지로 다운로드를 동시에 진행하지만, 스크립트 실행이 맨뒤로 밀리게 된다.

주요 렌더링 경로 측정

해당 렌더링 경로를 측정하는 좋은 방법은 LightHouse를 이용하는 것이다.

높은 우선순위를 가지고 로딩되는 리소스를 보여준다.

또는 Navigation Timing API를 활용하여 측정할 수도 있다.

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
    <script>
      function measureCRP() {
        var t = window.performance.timing,
          interactive = t.domInteractive - t.domLoading,
          dcl = t.domContentLoadedEventStart - t.domLoading,
          complete = t.domComplete - t.domLoading
        var stats = document.createElement('p')
        stats.textContent =
          'interactive: ' +
          interactive +
          'ms, ' +
          'dcl: ' +
          dcl +
          'ms, complete: ' +
          complete +
          'ms'
        document.body.appendChild(stats)
      }
    </script>
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>
interactive: 229ms, dcl: 230ms, complete: 956ms

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/dom-navtiming.png?hl=ko

  • domLoading: 전체 프로세스의 시작 타임스탬프. 브라우저가 처음 수신한 HTML 문서 바이트의 파싱 시작.
  • domInteractive: 브라우저가 파싱을 완료한 시점을 표시. 모든 HTML 및 DOM 생성 작업이 완료됨을 의미.
  • domContentLoaded: DOM이 준비되고 자바스크립트 실행을 차단하는 스타일시트가 없는 시점을 표시. 즉, 이제 (잠재적으로) 렌더링 트리를 생성할 수 있음. 많은 자바스크립트 프레임워크가 자체 로직을 실행하기 전에 이 이벤트를 기린다. 따라서 브라우저는 EventStart 및 EventEnd 타임스탬프를 캡처하여, 이를 통해 이 실행이 얼마나 오래 걸렸는지 추적할 수 있다.
  • domComplete: 이름이 의미하는 바와 같이, 모든 처리가 완료되고 페이지의 모든 리소스(이미지 등) 다운로드가 완료되었음을 의미.
  • loadEvent: 각 페이지 로드의 최종 단계로, 브라우저가 추가 애플리케이션 로직을 트리거할 수 있는 onload 이벤트를 트리거함.

주요 렌더링 경로 성능 분석

예제 페이지를 통해 분석을 시작해보자.

example1.png

DOMContentLoaded가 호출되는데 약 300ms 정도가 소요되었다. 그리고 이는 파란색 수직선으로 체크 되었다. 이미지 로딩은 이에 영향을 받지 않았다. 주요 렌더링 경로에는 HTML, CSS, 자바스크립트만 포함된다.

하지만 load이벤트는 이미지에서 차단되었다. onload는 따라서 모든 리소스가 다운로드 된 후에 호출된다는 것을 알 수 있다.

이제 여기에 자바스크립트와 CSS를 추가해보자.

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Script</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="timing.js"></script>
  </body>
</html>

example2.png

DOMContentLoaded가 거의 onload와 동시에 호출된 것을 알 수 있다. 일반적인 HTML과는 다르게, CSSOM을 생성하기 위해 CSS 파일도 가져와야 한다. 또한 파서 차단 자바스크립트가 포함되어 있어 CSS 파일이 다운로드 될때까지 차단되어 있는 것을 볼 수 있다.

이번엔 async 키워드를 추가해보자.

example3.png

이번에는 HTML이 파싱된 이후에 domContentLoaded가 실행된 것을 볼 수 있다. 브라우저가 자바스크립트를 차단하지 않는다는 것을 알게 되었고, 다른 파서 차단 스크립트가 없으므로 CSSDOM 생성 또한 동시에 처리 가능하다.

만약 이 모든 코드를 인라인으로 때려박게 되면, 위의 예제와 비슷한 성능을 느낄 수 있다. HTML 페이지는 더 커지지만, 페이지 안에 모든 요소가 있으므로 외부 리소스가 올때까지 기다릴 필요가 없기 때문이다.

성능 분석

순수 HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/analysis-dom.png?hl=ko

CSS 파일 추가

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/analysis-dom-css.png?hl=ko

CSS를 가져오기 위해 한번의 왕복이 더 추가되었고, 그만큼 렌더링도 밀려난 것을 볼 수 있다.

CSS + 자바스크립트 추가

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js"></script>
  </body>
</html>

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/analysis-dom-css-js.png?hl=ko

외부 자바스크립트 asset은 파서 차단 리소스 인 것을 명심하자. 자바스크립트 실행을 위해 작업을 차단하고 CSSDOM 이 생성되기 전까지 기다려야 한다.

한가지 눈여겨 봐야 할 점은, CSS와 자바스크립트 리소스 요청이 거의 동시에 이루어진다는 것이다. 두 리소스를 검색한 후 두 요청을 모두 동시에 실행 한다. 이는 CSS와 자바스크립트 전송을 동시에 할 수 있다는 것을 말한다.

CSS + Async javascript

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/analysis-dom-css-js-async.png?hl=ko

더 이상 스크립트가 파서를 차단하지 않고, 주요 렌더링 경로에도 포함되지 않았다. 또한 주요 스크립트가 없어서 CSS가 domContentLoaded이벤트를 차단하지도 않고, 따라서 애플리케이션 로직이 빠르게 실행되었다는 것을 알 수 있다.

주요 렌더링 경로 최적화

최초에 렌더링을 빠르게 하려면 다음의 경우를 고려해야 한다.

  • 주요 리소스 수: 페이지의 초기 렌더링을 차단하는 리소스의 수. 최대한 적어야 한다.
  • 주요 경로 길이 (얼마나 많은 리소스가 엮여 있는가): 주요 리소스와 해당 바이트 크기간의 그래프를 나타낸다. 일부 리소스는 이전 리소스가 다운된 후에만 시작할 수 있으며, 리소스가 클수록 다운로드 하는데 걸리는 왕복 수가 많아진다.
  • 주요 바이트 수 (리소스의 크기): 당연히 작아야 한다.

페이지 스피드 규칙 및 권장 사항

  • 렌더링 차단 스크립트 및 CSS 를 최소화
  • 자바스크립트 사용최적화
    • async 속성을 적극적으로 활용
    • 중요하지 않은 비필수 자바스크립트 실행 지연
    • 길게 실행되는 자바스크립트 회피
  • CSS 사용 최적화
    • CSS는 문서 헤드에 넣기
    • @import의 사용을 피하기. 이는 경로에 대한 왕복이 추가된다.
    • 렌더링 차단 CSS를 인라인으로 처리