최근 브라우저가 어떻게 작동하는지 궁금하던 차에 회사 직원분의 공유를 통해 브라우저는 웹페이지를 어떻게 그리나요? - Critical Rendering Path라는 글을 접하게 되었다. 해당 글에서 흥미로운 주제인 주요 렌더링 경로(Critical Rendering Path, 이하 CRP) 에 대해서 알게 되어 이와 관련된 Udacity 강좌와 여러 글들에 대해 찾아보게 되었고, 이를 바탕으로 현재 재직하고 있는 회사의 웹페이지에 해당 개념을 적용해보는 도전기를 공유하기 위해 이 글을 작성하고자 한다.
본 글에서는 CRP에 대해 상세한 내용을 다루지 않을 것이기 때문에 상세한 내용이 궁금하다면 글 맨 하단의 참조 링크들을 확인하길 바란다. 특히 Google에서 제작한 Udacity의 Website Performance Optimization와 Critical Rendering Path는 쉽게 설명해주어 좋은데, 심지어 번역도 되어있으니 처음 본다면 꼭 한번 보면 좋을 것 같다.
앞서 언급하였듯이 이 글은 재직하고 있는 회사의 웹페이지에 CRP 최적화를 시도해본 도전기를 공유하고자 작성하였으며, 측정 도구로는 Google에서 제작한 Lighthouse를 사용하였다. Lighthouse를 사용하여 측정할 때, CRP에 대해서만 측장하기 위해 Performacne옵션만 설정하여 측정하였다.
HTML, CSS 및 JavaScript 바이트를 수신한 후 렌더링된 픽셀로 변환하기 위해 필요한 처리까지 그 사이에 포함된 단계, 즉, 브라우저가 서버에서 응답을 받아 하나의 화면을 그려내는 것을 주요 랜더링 경로(CRP)라고 한다.
브라우저는 웹페이지를 어떻게 그리나요? - Critical Rendering Path에서 아래와 같이 해당 과정에 대해서 잘 요약해서 설명해주셨다.
브라우저는 웹페이지를 어떻게 그리나요? - Critical Rendering Path
CRP를 최적화하는 작업은 위 단계에서 1단계~6단계를 수행할 때 걸린 총 시간을 최소화하는 프로세스이다.
Critical Rendering Path - Constructing the Object Model
HTML과 CSS는 위와 같은 과정을 통해 DOM과 CSSOM으로 변환된다.
basic_dom.html
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
style.css
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
Critical Rendering Path - Constructing the Object Model
DOM은 HTML 문서의 객체 표현이고 외부를 향하는 JavaScript와 같은 프로그래밍 언어와 HTML 문서의 연결 지점(인터페이스)이다. JavaScript를 이용해 DOM에 접근하는 경우가 많이 있지만, 그렇다고 해서 JavaScript의 한 부분은 아니며, 기타 언어들로 DOM에 접근할 수도 있다. 트리의 최상위 객체는 문서이다.
Critical Rendering Path - Constructing the Object Model
HTML을 파싱하면 위와 같은 DOM Tree가 생성된다. DOM Tree는 문서 마크업의 속성 및 관계를 포함하지만 요소가 렌더링될 때 어떻게 표시될지에 대해서는 알려주지 않으며, 이것은 CSSOM의 책임입니다.
CSSOM은 JavaScript와 같은 프로그래밍 언어가 CSS를 조작 할 수 있게 해주는 API 세트이며, 이를 통해 CSS양식을 동적으로 읽고 수정할 수 있다.
Critical Rendering Path - Constructing the Object Model
Style Sheet를 파싱하면 위와 같은 CSSOM Tree가 생성된다.
HTML 및 CSS 입력을 기반으로 빌드한 서로 독립적인 객체인 DOM 및 CSSOM 트리를 병합하여 브라우저가 화면에 픽셀을 렌더링하도록 Render Tree를 형성한다. 이때 Render Tree에는 페이지를 렌더링하는데 필요한 노드만 포함된다.
Critical Rendering Path - Constructing the Object Model
모든 노드의 콘텐츠 및 스타일 정보를 모두 포함하는 Render Tree가 생성되었으므로 Layout단계로 진행할 수 있다.
브라우저는 Layout단계에서 앞서 생성한 표시할 노드와 해당 노드의 계산된 스타일을 포함하는 Render Tree를 사용하여, 루트에서 시작하여 트래버스하며 페이지에서 각 객체의 정확한 크기와 위치를 파악한다.
Layout 프로세스에서는 뷰포트 내에서 각 요소의 정확한 위치와 크기를 정확하게 캡처하는 상자 모델이 출력되며, 모든 상대적인 측정값은 화면에서 절대적인 픽셀로 변환된다.
nested.html
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
Critical Rendering Path - Constructing the Object Model
위 예제에는 두 가지 중첩된 div
가 포함되어 있다. 첫 번째(상위) div
는 노드의 표시 크기를 뷰포트 너비의 50%
로 설정하며, 두 번째 div
(상위 div
에 포함된)는 해당 너비를 상위 항목 너비의 50%
(즉, 뷰포트 너비의 25%
)로 설정하였다.
이때 각 div
의 크기는 다음과 같다.
body
: 320px
(width:100%
)div
: 160px
(width:50%
)div
: 80px
(width:50%
)이 단계에서 브라우저는 Render Tree의 각 노드들을 실제로 화면에 그리게된다.
Render Tree 생성, Layout 및 Paint 작업을 수행하는데 필요한 시간은 문서의 크기, 적용된 스타일 및 실행 중인 기기에 따라 달라진다. 즉, 문서가 클수록 브라우저가 수행해야 하는 작업도 더 많아지며, 스타일이 복잡할수록 페인팅에 걸리는 시간도 늘어난다.
예를 들면, 단색은 페인트하는데 시간과 작업이 적게 필요한 반면, 그림자 효과는 계산하고 렌더링하는데 시간과 작업이 더 필요하다.
위 영상은 Gecko에서 reflow를 거쳐서 화면에 paint되기까지를 보여준다.
Webkit과 Gecko 브라우저에서의 동작 과정 예이다.
앞에서 설명한 각 과정들은 많은 부분이 생략되어 있다. 특히 Layout과 Paint 부분은 많이 생략되어 있으므로, 해당 부분이 궁금하다면 브라우저는 어떻게 동작하는가? - D2를 참고하면 좋을 것 같다.
Render Tree를 생성하는데 DOM 및 CSSOM이 둘다 필요하기 때문에 HTML 및 CSS는 둘다 렌더링 차단 리소스이다. 또한, JavaScript를 사용하면 DOM 및 CSSOM을 쿼리하고 수정할 수 있기 때문에, JavaScript는 DOM 생성을 차단하고 페이지가 렌더링될 때 지연시킬 수도 있다.
HTML의 경우 DOM이 없으면 렌더링 할 것이 없기 때문에 렌더링 차단 이유가 명확하지만, CSS나 JavaScript는 요구 사항은 상황에 따라 이유가 다소 불명확할 수 있다.
아래는 렌더랑 차단 요소의 요약이다.
<link href="style.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" media="all" />
<link href="portrait.css" rel="stylesheet" media="orientation:portrait" />
<link href="print.css" rel="stylesheet" media="print" />
<link href="other.css" rel="stylesheet" media="(min-width: 40em)" />
<script>
document.write('Hello, world');
</script>
<script src="app.js"></script>
<script src="app.js" defer></script>
<script src="app.js" async></script>
Speed up Google Maps(and everything else) with async & defer
최적화를 하고자 하면 최적화하고자 하는 대상에 대해서 측정이 필요하다. 본 글에서는 아래의 몇가지 방법에 대해서 살표보고자 한다.
Chrome DevTools는 Google Chrome에 내장되어있는 웹 저작 및 디버깅 도구이며, DevTools를 이용하여 사이트를 반복하고, 디버깅하고, 프로파일링할 수 있다. 자세한 도구의 사용법은 타임라인 도구 사용법를 참고하기 바란다. DevTools를 사용하여 CRP뿐만 아니라 다양한 요소에 대해서 측정이 가능이 가능하지만, CRP를 측정면에서 본다면 다음 언급된 Lighthouse라는 도구가 더 유용하다.
Lighthouse는 웹 앱 감사 도구이며 해당 페이지에 대해 일련의 테스트를 수행한 다음, 이 페이지의 결과를 통합된 보고서로 표시해준다. Lighthouse를 Chrome 확장 프로그램이나 NPM 모듈로서 실행할 수 있으며, 이는 Lighthouse와 지속적 통합 시스템을 통합하는데 유용하다.
특히, 아래와 같이 별도로 해당 페이지에 대한 CRP 측정 결과를 보여주므로 CRP 측정에 유용하다.
Navigation Timing API와 기타 여러 브라우저 이벤트를 조합해서 사용는 Navigation Timing API 접근방식에서는 RUM(Real User Monitoring) 지표를 캡처하며, 이 지표는 실제 사용자의 사이트 상호작용으로부터 캡처되며, 다양한 기기와 네트워크 조건에서 사용자가 경험하는 실제 CRP 성능을 정확하게 보여준다.
<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>
주요 렌더링 경로를 최적화하기 위해서는 서로 다른 리소스 간의 의존성 그래프를 파악해야 하며, 어떤 리소스가 중요한지 식별해야 하고, 이러한 리소스를 페이지에 포함할 방법에 대한 다양한 전략 중에서 선택해야 한다. 이때 문제를 해결할 수 있는 방법이 한 가지만 있는 것은 아니며, 각 페이지가 서로 다르기 때문에 자신만의 유사한 프로세스에 따라 최적의 전략을 찾아야 한다.
domContentLoaded
(파란색)은 DOM이 준비되고 JavaScript의 실행을 차단하는 CSS가 없는 시점을 표시하며, Render Tree를 생성할 수 있다. load
(빨간색)은 페이지에 필요한 모든 리소스가 다운로드되고 처리되는 시점을 표시(즉, 이미지에서 차단됨)하며, 브라우저 로딩 스피너가 회전을 멈춘다.
CSS와 파서 차단 JavaScript가 포함되어 있다면, Render Tree를 빌드하기 위해 DOM과 CSSOM이 모두 필요하다. 또한 파서 차단 JavaScript 파일이 포함되기 때문에, CSS 파일이 다운로드되어 파싱될 때까지 domContentLoaded
이벤트가 차단된다.
외부 스크립트의 경우 async
키워드를 추가하여 파서의 차단을 해제할 수 있으며, 이 경우에는 CSSOM 생성 또한 동시에 하기 때문에, domContentLoaded
이벤트는 HTML이 파싱된 후 바로 실행된다.
CSS와 JS를 모두 페이지 내에 인라인으로 추가하는 경우에는 HTML 페이지가 더 커지지만, 페이지 안에 필요한 모든 요소가 있기 때문에 브라우저가 외부 리소스를 가져올 때까지 기다릴 필요가 없다. 이 경우 외부 스크립트를 비동기로 부르는 것과 비슷한 domContentLoaded
시간을 가진다.
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
<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>
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
<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>
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
<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>
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
<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>
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
<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>
Critical Rendering Path - Analyzing Critical Rendering Path Peformance
최초 렌더링 시 최대한 빠르게 렌더링하려면 아래 세 가지 변수를 최소화해야 한다.
주요 렌더링 경로를 최적화하기 위한 일반적인 단계는 아래와 같다.
주요 렌더링 경로를 최적화할 때 주의해야 할 사항은 다음과 같다.
@import
) 피하기Critical resources | Critical path length | Critical bytes(KB) | First meaningful paint(ms) | First interactive(ms) | Consistently Interactive(ms) | |
---|---|---|---|---|---|---|
PC | 23 | 23 | 140.5 | 3,090 | 4,510 | 측정 실패 |
Mobile | 15 | 15 | 281.27 | 3,380 | 5,280 | 11,490 |
link
태그보다 먼저 호출되도록 이동async
를 적용하고, 의존 관계가 물려있는 경우는 defer
을 적용window
의 load
이벤트가 발생한 뒤에 수행되도록 수정link
태그보다 먼저 호출되도록 이동Critical resources | Critical path length | Critical bytes(KB) | First meaningful paint(ms) | First interactive(ms) | Consistently Interactive(ms) | |
---|---|---|---|---|---|---|
PC | 2 | 2 | 15.59 | 1,370 | 5,710 | 19,150 |
Mobile | 1 | 1 | 24.8 | 1,200 | 5,810 | 5,810 |
Critical resources | Critical path length | Critical bytes | First meaningful paint | First interactive | Consistently Interactive | |
---|---|---|---|---|---|---|
PC | 91.3% 향상 | 91.3% 향상 | 88.9% 향상 | 55.7% 향상 | 26.6% 저하 | 측정불가 |
Mobile | 93.3% 향상 | 93.3% 향상 | 91.2% 향상 | 64.5% 향상 | 10.0% 저하 | 49.4% 향상 |
First interactive에서 다소 성능이 저하되었지만, 나머지 요소들에서는 많은 성능들이 향상되었다. First interactive 저하의 원인은 렌더링 차단 요소인 JS들을 minify와 uglify 같이 JS 자체에 대한 최적화 없이 렌더링 차단만 해결하여 First meaningful paint 자체는 해결되었지만, 필수 스크립트들이 로드되는 부분 자체가 최적화 된 것이 아니기 때문에 First interactive에서 성능 저하가 일어난 것이 아닐까 추즉하고 있다. 이 부분에 대해서는 좀 더 찾아볼 예정이다.
렌더링 차단 JavaScript를 제거하는 정도의 적은 노력으로 생각하던 것보다 노력 대비 성능 향상을 얻을 수 있는 것 같다. CRP 최적화는 다른 최적화들보다 개발할 때 조금만 더 신경을 써준다면 좋은 결과를 얻을 수 있을 것 같다.
또한 Lighthouse나 Chrome DevTools 등 다양한 측정 도구가 있고, 특히 Lighthouse의 경우 NPM도 지원하니 CI에 적용한다면 도움이 될 것 같다.