게시일: 2014년 3월 31일
주요 렌더링 경로 성능 병목 현상을 식별하고 해결하려면 일반적인 함정에 대한 충분한 지식이 필요합니다. 일반적인 성능 패턴을 파악할 수 있는 둘러보기를 사용하면 페이지를 최적화하는 데 도움이 됩니다.
핵심 렌더링 경로를 최적화하면 브라우저가 가능한 한 빨리 페이지를 그릴 수 있습니다. 페이지가 빨라지면 참여도, 페이지 조회수 및 전환율이 높아집니다. 방문자가 빈 화면에서 보내는 시간을 최소화하기 위해 어떤 리소스를 어떤 순서로 로드할지 최적화해야 합니다.
이 프로세스를 설명하는 데 도움이 되도록 가능한 가장 간단한 사례부터 시작하고 점차적으로 페이지를 확대하여 추가 리소스, 스타일 및 애플리케이션 로직을 포함해 보도록 하겠습니다. 이 과정에서 Google은 각 사례를 최적화합니다. 또한 어디에서 문제가 발생할 수 있는지도 볼 것입니다.
지금까지는 처리할 리소스(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 트리를 빌드하는 데 소요된 시간을 나타냅니다. 이 경우에는 몇 밀리초만 걸렸습니다.
'멋진 사진'이 domContentLoaded
이벤트를 차단하지 않았습니다. 즉, 페이지의 각 애셋을 기다리지 않고 렌더링 트리를 생성하고 페이지를 그릴 수도 있습니다. 빠르게 첫 페인트를 제공하는 데 모든 리소스가 중요한 것은 아닙니다. 실제로 중요한 렌더링 경로에 대해 이야기할 때 일반적으로 HTML 마크업, CSS 및 JavaScript에 대해 이야기합니다. 이미지는 페이지의 초기 렌더링을 차단하지 않습니다. 물론 이미지를 가능한 한 빨리 그리도록 해야 합니다.
하지만 load
이벤트(onload
라고도 함)는 이미지에서 차단됩니다. DevTools는 335ms에 onload
이벤트를 보고합니다. onload
이벤트는 페이지에 필요한 모든 리소스가 다운로드되고 처리되는 시점을 표시한다는 것을 기억하세요. 이 시점에서 로딩 스피너가 브라우저에서 회전을 멈출 수 있습니다(워터폴에서 빨간색 수직선).
JavaScript 및 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를 차단하더라도 스크립트를 인라인 처리하면 페이지 렌더링 속도가 빨라질까요? 시도해 보고 어떤 일이 일어나는지 확인해 보세요.
외부 자바스크립트:
인라인 자바스크립트:
요청을 하나 더 적게 수행하지만 onload
및 domContentLoaded
시간은 사실상 동일합니다. 왜냐하면 브라우저가 스크립트 태그에 도달하는 즉시 차단하고 CSSOM이 생성될 때까지 대기하기 때문에, JavaScript가 인라인이든 외부이든 상관없습니다. 또한 첫 번째 예시에서 브라우저가 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
시간은 이전 예와 사실상 동일합니다. 자바스크립트를 비동기로 표시하는 대신 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를 차단할 필요가 없다는 사실을 알게 되었습니다. 몇 가지 분석 방법과 페이지 렌더링을 차단할 필요가 없는 다른 코드가 있습니다. 즉, async
속성을 <script>
요소에 추가하여 파서 차단을 해제할 수 있습니다.
<!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회 왕복이 됩니다.