주요 렌더링 경로 성능 분석

Ilya Grigorik
Ilya Grigorik

주요 렌더링 경로 성능 병목 현상을 식별하고 해결하려면 일반적인 함정에 대한 충분한 지식이 필요합니다. 실습을 통해 페이지 최적화에 도움이 되는 일반적인 성능 패턴을 도출해 보겠습니다.

핵심 렌더링 경로를 최적화하면 브라우저에서 페이지를 최대한 빨리 그릴 수 있습니다. 페이지가 빨라지면 참여도, 페이지 조회수, 전환율이 개선됩니다. 방문자가 빈 화면을 보는 시간을 최소화하려면 로드되는 리소스와 순서를 최적화해야 합니다.

이 프로세스를 설명하는 데 도움이 되도록 가능한 가장 간단한 사례부터 시작하고 추가 리소스, 스타일 및 애플리케이션 로직을 포함하도록 페이지를 점진적으로 빌드해 보겠습니다. 이 과정에서 각 사례를 최적화하고 문제가 발생할 수 있는 부분도 살펴봅니다.

지금까지는 리소스 (CSS, JS 또는 HTML 파일)를 처리할 수 있게 되면 브라우저에서 어떤 일이 일어나는지에 대해서만 집중적으로 살펴보았습니다. 캐시나 네트워크에서 리소스를 가져오는 데 걸리는 시간을 무시했습니다. 다음과 같이 가정하겠습니다.

  • 서버에 대한 네트워크 왕복 시간 (전파 지연 시간)은 100ms입니다.
  • 서버 응답 시간은 HTML 문서의 경우 100밀리초, 기타 모든 파일의 경우 10밀리초입니다.

Hello World 환경

<!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>

사용해 보기

기본적인 HTML 마크업과 단일 이미지로 시작해 보겠습니다. CSS 또는 JavaScript는 포함하지 않습니다. Chrome DevTools에서 네트워크 타임라인을 열고 결과로 반환되는 리소스 워터폴(waterfall)을 검사합니다.

CRP

예상대로 HTML 파일을 다운로드하는 데 약 200ms가 소요되었습니다. 파란색 선의 투명한 부분은 브라우저가 응답 바이트를 수신하지 않고 네트워크에서 대기하는 시간을 나타내는 반면, 실선은 첫 번째 응답 바이트가 수신된 후 다운로드를 완료하는 데 걸리는 시간을 나타냅니다. HTML 다운로드 크기는 작기 때문에(4K 미만) 전체 파일을 가져오는 데 한 번의 왕복만 있으면 됩니다. 따라서 HTML 문서를 가져오는 데 약 200ms가 소요되며 시간의 절반은 네트워크에서 대기하고 나머지 절반은 서버 응답을 기다리는 데 사용됩니다.

HTML 콘텐츠를 사용할 수 있게 되면 브라우저가 바이트를 파싱하여 토큰으로 변환한 후 DOM 트리를 빌드합니다. DevTools는 개발자가 편하게 볼 수 있도록 DOMContentLoaded 이벤트 시간 (216ms)을 하단에 보고합니다. 이는 파란색 수직선에 해당합니다. HTML 다운로드 완료 시점과 파란색 수직선(DOMContentLoaded) 사이의 간격은 브라우저에서 DOM 트리를 빌드하는 데 걸리는 시간(이 경우 몇 밀리초)입니다.

'awesome photo'가 domContentLoaded 이벤트를 차단하지 않았다는 것을 알 수 있습니다. 이에 따라, 렌더링 트리를 생성하고 페이지의 각 애셋을 기다릴 필요 없이 페이지를 칠할 수도 있습니다. 빠르게 첫 페인트를 제공하는 데 모든 리소스가 중요한 것은 아닙니다. 사실, 우리가 주요 렌더링 경로에 대해 이야기할 때 일반적으로 HTML 마크업, CSS 및 JavaScript에 대해 이야기합니다. 이미지는 페이지의 초기 렌더링을 차단하지 않습니다. 물론 이미지를 가능한 한 빨리 그리도록 해야 합니다.

즉, load 이벤트 (onload라고도 함)는 이미지에서 차단됩니다. DevTools가 335ms에 onload 이벤트를 보고합니다. onload 이벤트는 페이지에 필요한 모든 리소스가 다운로드되고 처리된 지점을 표시합니다. 이 시점에서 로드 스피너가 브라우저에서 회전을 멈출 수 있습니다 (워터폴의 빨간색 수직선).

자바스크립트와 CSS를 함께 추가

'Hello World 체험' 페이지는 단순해 보이지만 사실 많은 내용이 포함되어 있습니다. 실제로는 HTML 외에 다른 것도 필요합니다. 아마도 페이지에 몇 가지 상호작용을 추가하기 위한 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>

사용해 보기

JavaScript 및 CSS를 추가하기 전에:

DOM CRP

자바스크립트 및 CSS 사용:

DOM, CSSOM, JS

외부 CSS 및 JavaScript 파일을 추가하면 워터폴에 두 개의 요청이 추가되며, 이 모든 요청이 거의 동시에 전달됩니다. 그러나 이제 domContentLoaded 이벤트와 onload 이벤트 간에 훨씬 작은 시간 차이가 있습니다.

어떻게 되었을까요?

  • 일반 HTML 예와 달리 CSSOM을 생성하기 위해 CSS 파일도 가져오고 파싱해야 하며, 렌더링 트리를 빌드하기 위해 DOM과 CSSOM이 모두 필요합니다.
  • 페이지에는 파서 차단 JavaScript 파일도 포함되어 있으므로 CSS 파일이 다운로드되고 파싱될 때까지 domContentLoaded 이벤트가 차단됩니다. JavaScript가 CSSOM을 쿼리할 수 있으므로 JavaScript를 실행하기 전에 다운로드될 때까지 CSS 파일을 차단해야 합니다.

외부 스크립트를 인라인 스크립트로 대체하면 어떻게 되나요? 스크립트가 페이지에 직접 인라인으로 삽입되더라도, CSSOM이 생성될 때까지는 브라우저에서 스크립트를 실행할 수 없습니다. 간단히 말해, 인라인 자바스크립트도 파서를 차단합니다.

CSS를 차단하더라도 스크립트를 인라인 처리하면 페이지 렌더링 속도가 빨라질까요? 한번 해 보고 어떻게 되는지 봅시다.

외부 자바스크립트:

DOM, CSSOM, JS

인라인 자바스크립트:

DOM, CSSOM, 인라인 JS

요청을 하나 더 적게 수행하지만 onload 시간과 domContentLoaded 시간은 사실상 동일합니다. 왜냐하면 우리는 JavaScript가 인라인이든 외부이든 상관이 없다는 것을 압니다. 브라우저가 스크립트 태그에 도달하는 즉시 차단되고 CSSOM이 생성될 때까지 대기하기 때문입니다. 또한 첫 번째 예에서 브라우저는 CSS와 JavaScript를 동시에 다운로드하고 거의 같은 시간에 다운로드를 완료합니다. 이 경우 JavaScript 코드를 인라인 처리해도 별 도움이 되지 않습니다. 그러나 페이지 렌더링 속도를 높일 수 있는 몇 가지 전략이 있습니다.

첫째, 모든 인라인 스크립트는 파서를 차단하지만 외부 스크립트의 경우 'async' 키워드를 추가하여 파서 차단을 해제할 수 있다는 점을 기억하세요. 인라인 처리를 취소하고 시도해 보겠습니다.

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Async</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 async src="timing.js"></script>
  </body>
</html>

사용해 보기

파서 차단 (외부) 자바스크립트:

DOM, CSSOM, JS

비동기 (외부) 자바스크립트:

DOM, CSSOM, 비동기 JS

훨씬 낫습니다! domContentLoaded 이벤트는 HTML이 파싱된 직후에 실행됩니다. 브라우저가 JavaScript를 차단하지 않는다는 것을 알고 있으며 다른 파서 차단 스크립트가 없으므로 CSSOM 생성도 동시에 처리될 수 있습니다.

또는 CSS와 JavaScript를 모두 인라인 처리할 수도 있습니다.

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Inlined</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <style>
      p {
        font-weight: bold;
      }
      span {
        color: red;
      }
      p span {
        display: none;
      }
      img {
        float: right;
      }
    </style>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script>
      var span = document.getElementsByTagName('span')[0];
      span.textContent = 'interactive'; // change DOM text content
      span.style.display = 'inline'; // change CSSOM property
      // create a new element, style it, and append it to the DOM
      var loadTime = document.createElement('div');
      loadTime.textContent = 'You loaded this page on: ' + new Date();
      loadTime.style.color = 'blue';
      document.body.appendChild(loadTime);
    </script>
  </body>
</html>

사용해 보기

DOM, 인라인 CSS, 인라인 JS

domContentLoaded 시간은 이전 예와 사실상 동일합니다. JavaScript를 비동기로 표시하는 대신 CSS와 JS를 모두 페이지 자체에 인라인 처리했습니다. 이로 인해 HTML 페이지가 훨씬 커지지만 장점은 브라우저가 외부 리소스를 가져올 때까지 기다릴 필요가 없다는 것입니다. 모든 것이 페이지에 바로 있습니다.

위에서 볼 수 있듯이, 매우 간단한 페이지에서도 주요 렌더링 경로를 최적화하는 것은 사소한 작업이 아닙니다. 서로 다른 리소스 간의 종속성 그래프를 이해해야 하고, 어떤 리소스가 '중요한' 리소스인지 식별해야 하며, 이러한 리소스를 페이지에 포함하는 방법에 관한 다양한 전략 중에서 선택해야 합니다. 이 문제에 대한 하나의 해결책은 없습니다. 페이지마다 다릅니다. 스스로 유사한 프로세스를 따라 최적의 전략을 찾아야 합니다.

이제 한 걸음 물러나 일반적인 성능 패턴을 식별할 수 있는지 살펴봅시다.

성능 패턴

가장 간단한 페이지는 CSS, 자바스크립트 또는 기타 유형의 리소스 없이 HTML 마크업으로만 이루어져 있습니다. 이 페이지를 렌더링하려면 브라우저가 요청을 시작하고 HTML 문서가 도착할 때까지 기다렸다가 파싱하고 DOM을 빌드한 다음 마지막으로 화면에 렌더링해야 합니다.

<!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>

사용해 보기

Hello World CRP

T0에서 T1 사이의 시간은 네트워크 및 서버 처리 시간을 나타냅니다. 최상의 경우 (HTML 파일이 작을 경우) 한 번의 네트워크 왕복만으로 전체 문서를 가져옵니다. TCP 전송 프로토콜이 작동하는 방식으로 인해 큰 파일은 더 많은 왕복이 필요할 수 있습니다. 결과적으로, 최상의 경우에는 위 페이지에 (최소) 1회 왕복의 주요 렌더링 경로가 있습니다.

이제 외부 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>

사용해 보기

DOM + CSSOM CRP

다시 한번, HTML 문서를 가져오기 위해 네트워크 왕복이 발생하고, 가져온 마크업을 통해 CSS 파일도 필요하다는 것을 알 수 있습니다. 즉, 브라우저가 화면에 페이지를 렌더링하기 전에 서버로 돌아가서 CSS를 가져와야 합니다. 따라서 이 페이지가 표시되려면 최소 두 번의 왕복이 발생합니다. 다시 말하지만, CSS 파일은 여러 번의 왕복이 필요할 수 있으므로 '최소'를 강조합니다.

주요 렌더링 경로를 설명하는 데 사용하는 어휘를 정의해 보겠습니다.

  • 중요한 리소스: 페이지의 초기 렌더링을 차단할 수 있는 리소스입니다.
  • 주요 경로 길이: 왕복 횟수 또는 모든 주요 리소스를 가져오는 데 필요한 총 시간입니다.
  • 주요 바이트: 페이지를 처음 렌더링하는 데 필요한 총 바이트 수입니다. 이는 모든 중요한 리소스의 전송 파일 크기의 합계입니다. HTML 페이지가 하나인 첫 번째 예는 주요 리소스 (HTML 문서)를 하나만 포함하고, 주요 경로 길이도 네트워크 왕복 1회와 같았으며 (파일이 작다고 가정), 총 주요 바이트는 HTML 문서 자체의 전송 크기였습니다.

이제 위의 HTML + CSS 예제의 주요 경로 특성과 비교해 보겠습니다.

DOM + CSSOM CRP

  • 중요 리소스 2
  • 최소 주요 경로 길이의 경우 2회 이상의 왕복
  • 중요한 바이트 9KB

렌더링 트리를 생성하려면 HTML과 CSS가 모두 필요합니다. 따라서 HTML과 CSS는 모두 중요한 리소스입니다. CSS는 브라우저가 HTML 문서를 가져온 후에만 가져오므로 주요 경로 길이는 최소 2번의 왕복입니다. 두 리소스의 합은 총 9KB의 주요 바이트입니다.

이제 여기에 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"></script>
  </body>
</html>

사용해 보기

페이지의 외부 JavaScript 애셋인 동시에 파서 차단 리소스 (중요한)인 app.js를 추가했습니다. 더 안 좋은 점은 JavaScript 파일을 실행하기 위해 차단하고 CSSOM을 기다려야 한다는 것입니다. JavaScript가 CSSOM을 쿼리할 수 있으므로 style.css가 다운로드되고 CSSOM이 생성될 때까지 브라우저가 일시중지된다는 점을 기억하세요.

DOM, CSSOM, 자바스크립트 CRP

하지만 실제로는 이 페이지의 '네트워크 워터폴'을 살펴보면 CSS와 JavaScript 요청이 모두 거의 동시에 시작되는 것을 알 수 있습니다. 브라우저가 HTML을 가져오고 두 리소스를 검색한 다음 두 요청을 모두 시작합니다. 결과적으로, 위 페이지는 다음과 같은 주요 경로 특성을 갖습니다.

  • 중요 리소스 3
  • 최소 주요 경로 길이의 경우 2회 이상의 왕복
  • 중요한 바이트 11KB

이제 총 11KB의 주요 바이트가 되는 세 개의 주요 리소스가 있지만, 주요 경로 길이는 여전히 2번의 왕복입니다. CSS와 JavaScript를 동시에 전송할 수 있기 때문입니다. 주요 렌더링 경로의 특성을 파악하면 중요한 리소스를 식별할 수 있으며 브라우저에서 가져오기를 예약하는 방식을 이해할 수 있습니다. 예시를 계속 살펴보겠습니다.

사이트 개발자와 채팅한 결과, 페이지에 포함한 JavaScript를 차단할 필요가 없다는 사실을 깨달았습니다. 페이지 렌더링을 차단할 필요가 없는 분석 및 기타 코드가 있습니다. 이러한 정보를 바탕으로 'async' 속성을 스크립트 태그에 추가하여 파서 차단을 해제할 수 있습니다.

<!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>

사용해 보기

DOM, CSSOM, 비동기 JavaScript CRP

비동기 스크립트에는 여러 가지 장점이 있습니다.

  • 스크립트가 더 이상 파서를 차단하지 않고 주요 렌더링 경로의 일부가 아닙니다.
  • 다른 중요한 스크립트가 없기 때문에 CSS에서 domContentLoaded 이벤트를 차단할 필요가 없습니다.
  • domContentLoaded 이벤트가 빨리 실행될수록 다른 애플리케이션 로직이 더 빨리 실행될 수 있습니다.

그 결과, 최적화된 페이지가 이제 두 개의 주요 리소스 (HTML 및 CSS)로 다시 전환되었으며, 최소 주요 경로 길이는 2회 왕복, 총 9KB의 주요 바이트입니다.

마지막으로, CSS 스타일시트가 인쇄에만 필요한 경우 어떻게 보일까요?

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" media="print" />
  </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>

사용해 보기

DOM, 비차단 CSS, 비동기 자바스크립트 CRP

style.css 리소스는 인쇄에만 사용되므로 브라우저가 페이지를 렌더링하기 위해 차단할 필요가 없습니다. 따라서 DOM 생성이 완료되자마자 브라우저가 페이지를 렌더링하기에 충분한 정보를 갖게 됩니다. 따라서 이 페이지에는 하나의 주요 리소스 (HTML 문서)만 있으며 최소 주요 렌더링 경로 길이는 1회 왕복입니다.

의견