중요 렌더링 경로 (Critical Rendering Path)는 브라우저가 HTML, CSS, Javascript를 픽셀로 화면에 그리기까지의 일련의 실행순서를 의미합니다.
웹 성능은 서버의 요청과 응답, 로딩, 스크립팅, 렌더링, 레이아웃, 픽셀로 변환하는 과정을 모두 포함해서 결정됩니다.
- 브라우저는 HTML,CSS,JS,Image,Font등 필요한 리소스에 대한 요청을 발생 & 서버로부터 응답
- 브라우저의 렌더링엔진이 HTML 및 CSS 파싱 후 DOM트리, CSSOM을 생성
- DOM, CSSOM를 결합해 렌더트리 생성
- JS 파싱과 실행
- 레이아웃 생성
- 페이지 렌더링
먼저 웹사이트에 접속하게 되면 브라우저는 서버에 페이지를 요청하고 이후 html에 대한 응답을 받게 됩니다.
html파일을 요청하면 서버는 html을 포함한 CSS, JS, image, font file등도 함께 응답해주는 것을 확인할 수 있습니다. 브라우저의 렌더링 엔진이 HTML(index.html)를 파싱하는 도중에 외부 리소스를 로드하는 태그를 만나게 되면 HTML의 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청하는 과정을 거치게 됩니다.
외부 리소스를 로드하는 태그 예시
- CSS을 로드하는
<link>
- image파일을 로드하는
<img>
- 자바스크립트를 로드하는
<script>
브라우저와 서버의 통신을 위해서 HTTP(hyperText Transger Protocol)규약을 이용합니다. HTTP/1.1과 HTTP/2의 차이로 커넥션당 처리 가능한 요청의 수를 살펴보면, 페이지로드 속도에 유의미한 영향을 준다는 것을 확인할 수 있습니다.
HTTP/1.1은 connection당 하나의 요청과 응답을 처리합니다. 따라서 HTML문서 내에 여러개의 리소스가 개별적으로 요청과 응답이 전송되는 구조를 갖으며, 이는 리소스의 개수에 비례해 응답시간도 증가한다는 단점으로 이어집니다. 반면, HTTP/2은 다중 요청/응답이 가능하므로 HTTP/1.1에 비해서 페이지 로드 속도가 50%정도 빠르다고 알려져있습니다.
브라우저는 HTTP를 통해 응답받은 html을 분석해서 DOM을 만들기 시작합니다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul>
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script src="app.js"></script>
</body>
</html>
브라우저는 서버에게 HTML문서를 바이트(10110111...)형식으로 응답받습니다. 이후 응답헤더에 담긴 <meta charset="UTF-8">
을 확인하고 인코딩 방식(UTF-8)을 기준으로 문자열로 변환합니다. 문자열로 변환된HTML문서에 한해서 토큰으로 분해하고, 분해된 각 토큰이 객체로 변환하여 DOM을 구성하는 기본 요소가 되는 노드(node)를 생성합니다.
토큰: 문법적 의미를 갖는 코드의 최소단위
{ startTag:'html', contents:{ ... } endTag:'html' }
1개의 DOM노드는 시작태그 토큰으로 시작해서 끝태그 토근으로 끝나고 각 노드는 HTML 요소에 대한 모두 연관성 있는 정보를 포함합니다. HTML 요소간의 중첩관계에 의해 생성된 부자관계를 반영해 모든 노드들을 트리 자료구조로 구성하고, 이 구성된 트리 자료구조를 DOM이라고 부릅니다.
더불어 점진적을 증가하는 구조를 갖으므로 콘텐츠가 페이지에 나타날 때 전체문서를 로드할 필요가 없이 필요한 부분에 한해서 부분적으로 실행이 가능합니다. 하지만 CSS,JS가 페이지 렌더링을 차단할 수는 있습니다.
따라서 DOM은 HTML문서를 파싱한 결과물이라고 생각 할 수 있으며, HTML이 js를 요청하는 과정에서 DOM의 변경도 가능합니다.
이어서 분석되는 CSSOM은 DOM과 더불어 DOM을 Styling하기 위한 페이지 내의 모든 스타일 정보를 포함합니다. 렌더링엔진이 HTML을 순차적으로 파싱하며 DOM을 생성하다, css를 로드하는 tag를 만나면 DOM의 생성을 일시중단합니다. 이후 css 파일을 서버에 요청하고 css를 html과 동일하게 파싱(바이트->문자->토큰->노드->cssom)하여 CSSOM을 생성합니다. 이후 HTML파싱이 중단된 지점부터 다시 HTML파싱을 재개합니다.
예시 css.css
body{ font-size: 18px; } ul{ list-style-type:none; }
HTML은 CSSOM을 만들기 위한 style을 포함한다. DOM의 구조는 HTML을 분석함에 따라서 점진적으로 증가하지만 CSSOM은 증가하지 않는다는 특징을 갖기때문데 브라우저는 모든 CSS를 처리하고 수신할때까지의 페이지 렌더링을 막습니다. 따라서 CSSOM을 완료하지 않았다면 렌더트리의 구성요소가 될 수 없음을 의미합니다. C(Cascade)SS는 유효한 토큰을 인식하기 위해 규칙를 갖는데, 토큰을 노드로 변환할때 하위노드가 스타일을 상속합니다. 연속적인 규칙들이 이전의 규칙과 덮여쓰여지는 것은 방지하고자 증감 처리 기술은 CSS에 적용되지 않습니다. CSS를 분석할대 CSSOM이 빌드되지만 완전 분석될때까지 렌더트리를 생성하는데 사용할 수 없는 것입니다.
선택자 성능은 덜 구체적인 선택자가 구체적인 선택자보다 더 빠릅니다. .foo 찾을때, .foo {} 는 .bar .foo {} 보다 빠릅니다. .bar .foo {}는 .bar 를 갖고있는지 확인하기위해서 DOM을 확인해야하기때문입니다. 하지만 선택자 성능 최적화와 개선은 microsecond수준으로 개선 되는 부분이므로 최적화하지 않아도 되는 요소입니다. 축소화와 미디어 쿼리를 사용함으로써 지연된 CSS를 논-블로킹 요청으로 분리하는 것과 같은 CSS 최적화 (en-US)를 위한 다른 방법이 있습니다.
JavaScript 파일이 실행되기 전에 CSSOM이 구성될 때까지 기다려야 하기 때문입니다.
자바스크립트의 파싱 및 실행은 브라우저의 렌더링 엔진이 아닌 자바스크립트 엔진이 처리한다는 차이를 갖습니다. 따라서 렌더링엔진과 자바스크립트엔진이 제어권을 주고 받으며 실행을 하게됩니다.
JavaScript는 "파서 차단 리소스" 로 간주됩니다 . 이는 HTML 문서 자체의 구문 분석이 JavaScript에 의해 차단됨을 의미합니다.
단순한 문자열인 자바스크립트 소스코드를 어휘분석해 토큰으로 분해하는 토크나이징tokenizing을 실행합니다. 토크나이징 과정을 거쳐 생성된 토큰을 분석해 의미를 부여하는 렉싱lexing과정도 존재합니다. 렉싱 과정을 렉스타임이라고 하는데, 렉스타임에서 스코프가 결정된다하여 렉시컬 스코프라고 합니다.
파서가 태그에 도달하면 <script>
태그가 내부이든 외부이든 가져오기를 중지하고(외부인 경우) 실행합니다. 이것이 문서 내의 요소를 참조하는 JavaScript 파일이 있는 경우 해당 문서가 나타난 뒤에 위치해야 하는 이유입니다.
JavaScript가 파서 차단이 되지 않도록 async속성을 적용하여 비동기식으로 로드할 수 있습니다.
<script async src="script.js">
브라우저의 자바스크립트 엔진은 서버로부터 응답된 자바스크립트를 파싱하여 AST(abstract syntax tree)를 생성하고 바이트 코드로 변환하여 실행, 이때 js는 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있고, 변경된 COM과 CSSOM은 다시 렌더트리로 결합된다.
이렇게 생성된 DOM과 CSSOM 트리는 렌더 트리에 결합됩니다. 렌더 트리를 구성하기 위해 브라우저는 DOM 트리의 root에서 시작해 모든 노드는 확인하면서 어떤 CSS 규칙들을 첨부할지 결정합니다. 여기서 주의할점은 렌더트리는 보여지는 콘텐츠만 캡쳐합니다는 것입니다. 요소에 display: none 이 적용되어 있다면, 해당 요소 또는 하위 요소는 포함되지 않습니다.
렌더트리 요소들에대한 위치와 크기가 정의된 렌더트리가 생성되고 나면 화면에 크기에 의존해서 레이아웃을 생성할 수 있어집니다. 레이아웃 단계는 요소들이 페이지에서 배치되는 위치와 방법, 각 요소의 너비와 높이 그리고 서로 관련된 위치를 결정합니다. 요소의 너비를 정의한 것에 따르면 블럭 수준의 요소들은 그 부모 너비의 기본 너비값의 100%이다. body 는 뷰포트 너비의 100%를 의미하는 너비입니다. 디바이스의 너비는 레이아웃에 영향을 미칩니다.뷰포트 메타 태그는 레이아웃에 영향을 미치는 뷰포트 레이아웃의 너비로 정의합니다. 이 태그 없다면, 브라우저는 뷰포트 기본값을 사용합니다.
<meta name="Viewport" content="width=device-witdh">
로 세팅함으로써 기본 뷰포트 너비 대신에 디바이스의 너비를 사용합니다. 디바이스 너비는 사용자가 디바이스를 가로(landscape) 또는 세로(portrait) 모드 사이로 돌릴때마다 바뀝니다. 레이아웃은 디바이스가 회전하거나 브라우저의 사이즈가 조정될 때마다 발생합니다.
노드의 수가 많아지면 레이아웃이 더 길어지며, 더욱 애니메이션이 필요하다면 jank를 일으키는 병목현상이 발생할 수 있습니다. 레이아웃 이벤트의 반복과 형성시간을 줄이기 위해서 일괄 업데이트 해야하고, 박스 모델 속성을 애니메이션화 하지 말아야 합니다.
화면에 픽셀을 그리는 단계는 렌더트리가 생성되고 레이아웃이 나타나기 시작한 이후에 가능합니다. 이는 로드시에 전체화면을 그리고, 이후에는 브자우저가 최소영역만 repaint하도록 최적화되어있스빈다. 따라서 영향을 받는 영역만 화면에 다시 그립니다.페인팅은 매우 빠르게 진행되는 과정이기 때문에 성능 향상에 집중해야 하는 가장 큰 영향있는 부분이 아닐 수 있습니다.
자원 로드 순서를 관리하고, 파일 사이즈를 줄이며 어떤 자원을 먼저 로드할지 정함
자원 다운로드를 연기함으로써 중요 자원들의 수를 최소화
각 요청에 대한 파일 사이즈에 따라 필수적인 요청 횟수 최적하
다운받을 중요 에셋의 우선순위를 정함으로써 중요 자원 불러오는 순서 최적화, 중요 경로 길이 최소화
CRP의 최적화 = 첫번째 렌더링 시간을 개선시키는 것
1초당 60프레임에 리플로우와 리페인트가 발생할 수 있도록 해야하는데, 이를 돕기위해 최적화가 요구된다
렌더링 과정은 반복해서 실행될 수 있는데,
- 자바스크립트에 의한 노드 추가 및 삭제
- 브라우저 창의 리사이징에 의한 뷰포트 크기 변경
- html 요소의 레이아웃에 변경을 발생시키는 스타일의 변경
이 대표적인 예시에 해당합니다. 리렌더링은 비용이 많이들고 성능에 악영향을 주는 작업이니 해당 작업이 발생되지 않게 주의가 필요합니다.