게시일: 2014년 3월 31일
주요 렌더링 경로 성능 병목 현상을 식별하고 해결하려면 흔히 있는 함정들에 대해 잘 파악하고 있어야 합니다. 일반적인 실적 패턴을 파악하는 안내를 통해 페이지를 최적화할 수 있습니다.
주요 렌더링 경로를 최적화하면 브라우저가 가능한 한 빨리 페이지를 그릴 수 있습니다. 더 빠른 페이지는 더 높은 참여도, 더 많은 페이지 조회 수, 전환율 향상으로 이어집니다. 방문자가 빈 화면에서 보내는 시간을 최소화하기 위해 어떤 리소스를 어떤 순서로 로드할지 최적화해야 합니다.
이 프로세스를 설명하는 데 도움이 되도록 가능한 가장 간단한 사례부터 시작하고 점차적으로 페이지를 확대하여 추가 리소스, 스타일 및 애플리케이션 로직을 포함해 보도록 하겠습니다. 이 과정에서 각 사례를 최적화하고 어떤 부분이 잘못될 수 있는지도 살펴봅니다.
지금까지는 처리할 리소스(CSS, JS 또는 HTML 파일)가 준비가 되면 브라우저에 어떤 일이 일어나는지에 대해서만 초점을 맞췄습니다. 캐시나 네트워크에서 리소스를 가져오는 데 걸리는 시간을 무시했습니다. 다음을 가정해 보겠습니다.
- 서버에 대한 네트워크 왕복 (전파 지연 시간) 비용은 100ms입니다.
- 서버 응답 시간은 HTML 문서의 경우 100ms, 기타 모든 파일의 경우 10ms입니다.
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에서 네트워크 패널을 열고 결과로 나타나는 리소스 폭포식을 검사합니다.
예상대로, HTML 파일의 다운로드 시간은 약 200ms가 걸렸습니다. 파란색 선의 투명한 부분은 브라우저가 응답 바이트를 수신하지 않고 네트워크에서 대기하는 시간을 나타내는 반면, 진한 부분은 첫 응답 바이트가 수신된 후에 다운로드가 완료된 시간을 보여줍니다. HTML 다운로드 크기는 매우 작기 때문에(4K 미만) 전체 파일을 가져오기 위해서는 한 번의 왕복만 필요합니다. 결과적으로, HTML 문서를 가져오는 데 약 200ms가 걸리며, 절반은 네트워크에서 대기하는 데, 나머지 절반은 서버 응답을 기다리는 데 사용됩니다.
HTML 콘텐츠를 사용할 수 있게 되면 브라우저가 바이트를 파싱하여 토큰으로 변환하고 DOM 트리를 빌드합니다. DevTools는 개발자가 편하게 볼 수 있도록 DOMContentLoaded 이벤트 시간(216ms)을 맨 아래에 표시합니다. 이는 파란색 수직선에 해당합니다. HTML 다운로드 완료 시점과 파란색 수직선(DOMContentLoaded) 사이의 격차는 브라우저가 DOM 트리를 빌드하는 데 소요된 시간을 나타냅니다. 이 경우에는 몇 밀리초만 걸렸습니다.
'awesome photo'가 domContentLoaded
이벤트를 차단하지 않았다는 점에 주목하세요. 이는 페이지의 각 자산을 기다릴 필요 없이 렌더링 트리를 생성하고 페이지를 그릴 수 있음을 의미합니다. 일부 리소스는 페이지를 신속하게 처음 그리는 데 중요하지 않습니다. 실제로, 우리가 주요 렌더링 경로에 대해 이야기할 때 일반적으로 HTML 마크업, CSS, 자바스크립트에 대해 언급합니다. 이미지는 페이지의 초기 렌더링을 차단하지 않습니다. 물론 이미지를 가능한 한 빨리 그리도록 해야 합니다.
즉, 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 추가 전:
JavaScript 및 CSS 사용:
외부 CSS 및 JavaScript 파일을 추가하면 위의 워터폴에 두 개의 요청이 더 추가됩니다. 두 개 모두 브라우저가 거의 같은 시간에 발송합니다. 그러나 이제 domContentLoaded
및 onload
이벤트 간에 훨씬 작은 시간 차이가 있습니다.
어떻게 된 것일까요?
- 일반 HTML 예시와 달리 CSSOM을 생성하기 위해 CSS 파일도 가져오고 파싱해야 하며, 렌더링 트리를 빌드하기 위해 DOM과 CSSOM이 모두 필요합니다.
- 페이지에는 또한 파서 차단 JavaScript 파일이 포함되기 때문에, CSS 파일이 다운로드되어 파싱될 때까지
domContentLoaded
이벤트가 차단됩니다. JavaScript가 CSSOM을 쿼리할 수도 있기 때문에, 자바스크립트를 실행하기 전에 CSS 파일이 다운로드될 때까지 차단해야 합니다.
외부 스크립트를 인라인 스크립트로 바꾸면 어떻게 되나요? 스크립트가 페이지에 바로 인라인 처리되더라도, CSSOM이 생성될 때까지는 브라우저가 이 스크립트를 실행할 수 없습니다. 간단히 말해서, 인라인 자바스크립트도 파서를 차단합니다.
CSS를 차단하더라도 스크립트를 인라인 처리하면 페이지 렌더링 속도가 빨라질까요? 시도해 보고 어떤 일이 일어나는지 확인해 보세요.
외부 JavaScript:
인라인 자바스크립트:
요청을 하나 더 적게 수행하지만 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>
파서 차단 (외부) JavaScript:
비동기(외부) JavaScript:
훨씬 낫네요! 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>
domContentLoaded
시간은 이전 예와 사실상 동일합니다. JavaScript를 비동기로 표시하는 대신 CSS와 JS를 모두 페이지 자체에 인라인으로 추가했습니다. 이로 인해 HTML 페이지가 더 커지지만, 장점은 페이지 안에 필요한 모든 요소가 있기 때문에 브라우저가 외부 리소스를 가져올 때까지 기다릴 필요가 없다는 점입니다.
보시다시피 매우 기본적인 페이지에서도 중요한 렌더링 경로를 최적화하는 것은 간단한 작업이 아닙니다. 여러 리소스 간의 종속성 그래프를 이해하고, '중요'한 리소스를 파악하고, 이러한 리소스를 페이지에 포함하는 방법에 관한 다양한 전략 중에서 선택해야 합니다. 이 문제를 해결할 수 있는 방법이 한 가지만 있는 것은 아닙니다. 각 페이지가 서로 다르기 때문에 자신만의 유사한 프로세스에 따라 최적의 전략을 찾아야 합니다.
이제 위의 과정에서 몇 가지 일반적인 성능 패턴을 찾을 수 있는지 살펴봅시다.
성능 패턴
가장 간단한 페이지는 CSS, JavaScript 및 기타 유형의 리소스 없이 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>
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>
다시 한 번, HTML 문서를 가져오기 위한 네트워크 왕복을 발생시킵니다. 그러면 가져온 마크업이 CSS 파일도 필요하다고 알려줍니다. 즉, 브라우저가 화면에 페이지를 렌더링하기 전에 서버로 돌아가서 CSS를 가져와야 합니다. 따라서 이 페이지는 표시되기 전에 최소 두 번의 왕복이 발생합니다. 다시 말하자면 CSS 파일은 여러 번의 왕복이 필요할 수 있으므로 '최소'라는 표현을 썼습니다.
다음은 주요 렌더링 경로를 설명하는 데 사용하는 몇 가지 용어입니다.
- 주요 리소스: 페이지의 초기 렌더링을 차단할 수 있는 리소스입니다.
- 주요 경로 길이: 왕복 횟수 또는 모든 주요 리소스를 가져오는 데 필요한 총 시간입니다.
- 주요 바이트: 페이지의 첫 번째 렌더링을 실행하는 데 필요한 총 바이트 수로, 모든 주요 리소스의 전송 파일 크기의 합계입니다. 단일 HTML 페이지가 있는 첫 번째 예에는 단일 주요 리소스 (HTML 문서)가 포함되어 있었습니다. 주요 경로 길이도 1회의 네트워크 왕복 (파일이 작다고 가정)과 같고 총 주요 바이트는 HTML 문서 자체의 전송 크기일 뿐입니다.
이제 이전 HTML 및 CSS 예시의 주요 경로 특성과 비교해봅시다.
- 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>
app.js
를 추가했습니다. 이것은 페이지에서 외부 JavaScript 애셋이자 파서 차단(즉, 중요한) 리소스입니다. 더 안 좋은 경우, JavaScript 파일을 실행하기 위해 작업을 차단하고 CSSOM이 처리될 때까지 기다려야 합니다. JavaScript가 CSSOM을 처리할 수 있기 때문에 style.css
가 다운로드되고 CSSOM이 생성될 때까지 브라우저가 일시 중지됩니다.
하지만 실제로는 이 페이지의 '네트워크 폭포식 구조'를 보면 CSS와 JavaScript 요청이 모두 거의 같은 시간에 시작된다는 것을 알 수 있습니다. 브라우저가 HTML을 가져오고 두 리소스를 검색한 후 두 요청을 모두 실행합니다. 따라서 위 이미지에 표시된 페이지는 다음과 같은 주요 경로 특성을 갖습니다.
- 3개의 주요 리소스
- 2번 이상의 왕복(최소 주요 경로 길이)
- 11KB의 주요 바이트
이제 총 11KB의 주요 바이트에 해당하는 3개의 주요 리소스를 갖게 되었습니다. 하지만 주요 경로 길이는 여전히 2번 왕복입니다. 그 이유는 CSS와 JavaScript를 동시에 전송할 수 있기 때문입니다. 주요 렌더링 경로의 특성을 파악하면 중요한 리소스를 식별하고 브라우저가 리소스 가져오기를 예약하는 방식을 이해할 수 있습니다.
Google은 사이트 개발자와 얘기를 나눈 후 페이지에 포함한 JavaScript를 차단할 필요가 없다는 사실을 알게 되었습니다. 몇 가지 분석 방법과 페이지 렌더링을 차단할 필요가 없는 다른 코드가 있습니다. 이를 바탕으로 <script>
요소에 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>
비동기 스크립트는 다음과 같은 여러 가지 이점이 있습니다.
- 스크립트가 더 이상 파서를 차단하지 않으며 주요 렌더링 경로의 일부가 아닙니다.
- 다른 중요한 스크립트가 없으므로 CSS가
domContentLoaded
이벤트를 차단할 필요가 없습니다. domContentLoaded
이벤트가 빨리 실행될수록 다른 애플리케이션 로직도 빨리 실행될 수 있습니다.
그 결과, 최적화된 페이지가 이제 다시 2개의 주요 리소스(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>
style.css 리소스는 인쇄에만 사용되기 때문에 브라우저가 페이지를 렌더링하기 위해 차단할 필요가 없습니다. 따라서, DOM 생성이 완료되자마자 브라우저가 페이지를 렌더링하는 데 충분한 정보를 갖게 됩니다. 그 결과, 이 페이지는 하나의 주요 리소스(HTML 문서)만 가지며, 최소 주요 렌더링 경로 길이는 1회 왕복이 됩니다.