이 글은 "https://developers.google.com/web/fundamentals/performance/critical-rendering-path/"에 기고된 글입니다. HTML, CSS, JavaScript를 제대로 짜는 법, 최적화하는 법에 대해 고민을 해봤다. 뿐만아니라 코드 만드는데 급급해 이해하지 못하고 넘어간 DOM, CSSOM, JavaScript가 어떤식으로 돌아가는지 이해하기 위해 포스팅합니다.
주요 렌더링 경로
브라우저가 HTML,CSS 및 JavaScript를 사용하여 화면에 렌더링된 픽셀로 변환하는 과정을 알아보자.
성능을 최적화하려면 HTML, CSS 및 자바스크립트 바이트를 수신한 후 렌더링된 픽셀로 변환하기 위해 필요한 처리까지, 그 사이에 포함된 중간 단계에서 어떠한 일이 일어나는지를 파악하기만 하면 됩니다.
이러한 단계가 바로 주요 렌더링 경로입니다.
주요 렌더링 경로를 최적화하면 최초페이지 렌더링에 걸리는 시간을 상당히 단축시킬 수 있다.
또한, 주요 렌더링 경로에 대한 이해를 토대로 뛰어난 성능의 대화형 애플리케이션을 빌드할 수도 있습니다. 대화형 업데이트 프로세스도 이와 동일합니다. 연속 루프에서 실행되며 이상적인 속도는 초당 60프레임입니다.
객체 모델 생성
브라우저가 페이지를 렌더링하려면 먼저 DOM 및 CSSOM트리를 생성해야 합니다.
따라서 HTML 및 CSS를 가능한 빨리 브라우저에 제공해야 합니다.
ㅇ바이트 → 문자 → 토큰 → 노드 → 객체 모델.
ㅇHTML 마크업은 DOM(Document Object Model)으로 변환되고, CSS 마크업은 CSSOM(CSS Object Model)으로 변환됩니다.
ㅇDOM 및 CSSOM은 서로 독립적인 데이터 구조입니다.
ㅇChrome DevTools Timeline을 사용하면 DOM 및 CSSOM의생성 및 처리 비용을 수집하고 점검할 수 있습니다.
DOM(Document Object Model)
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
가장 단순한 경우인 몇몇 텍스트와 하나의 이미지만 포함하는 일반 HTML 페이지부터 살펴보도록 하겠습니다. 브라우저가 이 페이지를 어떻게 처리하나요?
1. 변환: 브라우저가 HTML의 원시 바이트를 디스크나 네트워크에서 읽어와, 해당 파일에 대한 지정된 인코딩(UTF-8)에 따라 개별 문자로 변환합니다.
2. 토큰화: 브라우저가 문자열 W3C HTML5 표준에 지정된 고유 토큰으로 변환합니다.(ex) '', '' 및 꺽쇠 괄호로 묶인 기타 문자열). 각 토큰은 특별한 의미와 고유한 규칙을 가집니다.
[W3C HTML5 표준 :https://www.w3.org/TR/html5/]
3. 렉싱: 방출된 토큰은 해당 속성 및 규칙을 정의하는 '객체'로 변환됩니다.
4. DOM 생성: 마지막으로, HTML 마크업이 여러 태그(일부 태그는 다른 태그 안에 포함되어 있음) 간의 관계 정의하기 때문에 생성된 객체는 트리 데이터 구조 내에 연결됩니다. 이 트리 데이터 구조에는 원래 마크업에 정의된 상위-하위 관계도 포함됩니다.
HTML 객체는 body 객체의 상위이고, body는 paragraph 객체의 상위인 식입니다.
이 전체 프로세스의 최종 출력이 바로 이 간단한 페이지의 DOM이며, 브라우저는 이후 모든 페이지 처리에 이 DOM을 사용합니다.
브라우저는 HTML마크업을 처리할 때마다 위의 모든 단계를 수행합니다. 즉, 바이트를 문자로 변환하고,토큰을 식별한 후 노드로 변환하고 DOM트리를 빌드합니다. 이 전체 프로세스를 완료하려면 시간이
약간 걸릴 수 있으며, 특히 처리해야 할 HTML이 많은 경우 그렇습니다.
참고: Chrome DevTools 사용방법에 대해 알아야함. - 네트워크 워터폴을 캡처하거나 타임라인을 기록하는 방법
Discover DevTools 학습: https://www.pluralsight.com/search?q=chrome%20developer%20tools
매끄러운 애니메이션을 만드는 경우, 브라우저가 대량의 HTML을 처리해야 한다면 쉽게 병목 현상이 발생할 수 있습니다.
DOM트리는 문서 마크업의 속성 및 관계를 포함하지만 요소가 렌더링될 때 어떻게 표시될지에 대해서는알려주지 않습니다. 이것은 CSSOM의 책임입니다.
CSSOM(CSS Object Model)
브라우저는 단순한 페이지의 DOM을 생성하는 동안 외부 CSS 스타일시트인 style.css를 참조하는 문서의헤드 섹션에서 링크 태그를 접합니다. 페이지를 렌더링하는데 이 리소스가 필요할 것이라고 판단한 브라우저는이 리소스에 대한 요청을 즉시 발송하고 요청의 결과로 다음 콘텐츠가 반환됩니다.
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
HTML 마크업내에 직접 스타일을 선언할 수도 있지만 CSS를 HTML과 별도로 유지하면 콘텐츠와 디자인을 별도의 항목으로 처리할 수 있습니다. 즉, 디자이너는 CSS를 처리하고, 개발자는 HTML에만 집중할 수 있습니다.
HTML과 마찬가지로, 수신된 CSS 규칙을 브라우저가 이해하고 처리할 수 잇는 형식으로 변환해야 합니다. 따라서 HTML 대신 CSS에 대해 HTML프로세스를 반복합니다.
CSS바이트가 문자로 변환된 후 차례로 토큰과 노드로 변환되고 마지막으로 'CSS Object Model'(CSSOM)이라는 트리 구조에 링크됩니다.
CSSOM이 트리 구조를 가지는 이유느 페이지에 있는 객체의 최종 스타일을 계산할 때 브라우저는 해당노드에적용 가능한 가장 일반적인 규칙(EX: body요소의 하위인 경우 모든 body 스타일 적용)으로 시작한 후 더욱 구체적인 규칙을 적용하는 방식으로계산된 스타일을 재귀적으로 세분화합니다.
CSSOM 및 DOM은 서로 독립적인 데이터 구조입니다. 알고보니 브라우저에서 숨겨진 중요한 단계가 있습니다.
렌더링 트리 생성, 레이아웃 및 페인트
CSSOM 및 DOM트리는 결합하여 렌더링 트리를 형성합니다. 이 렌더링 트리는 표시되는 각 요소의 레이아웃을계산하는데 사용되고 픽셀을 화면에 렌더링하는 페인트 프로세스에 대한 입력으로 처리됩니다. 최적의 렌더링 성능을 얻기 위해서는 이러한 단계 각각을 최적화하는 것이 중요합니다.
객체 모델을 생성하는 방법을 설명한 이전 섹션에서 우리는 HTML 및 CSS 입력을 기반으로 DOM 및 CSSOM 트리를 빌드했습니다. 하지만, 이들 모두 문서의 각기 다른 측면을 캡처하는 서로 독립적인 객체입니다. 하나는 콘텐츠를 설명하고, 다른 하나는 문서에 적용되어야 하는 스타일 규칙을 설명합니다. 이 두 가지를 병합하여 브라우저가 화면에 픽셀을 렌더링하도록 하려면어떻게 해야 할까요?
1. DOM 및 CSSOM 트리는 결합되어 렌더링 트리를 형성합니다.
2. 렌더링 트리에는 페이지를 렌더링하는 데 필요한 노드만 포함됩니다.
3. 레이아웃은 각 객체의 정확한 위치 및 크기를 계산합니다.
4. 마지막 단계는 최종 렌더링 트리에서 수행되는 페인트이며, 픽셀을 화면에 렌더링합니다.
먼저, 브라우저가 DOM 및 CSSOM을 '렌더링 트리'에 결합합니다. 이 트리는 페이지에 표시되는 모든 DOM 콘텐츠와 각 노드에 대한 모든 CSSOM 스타일 정보를 캡처합니다.
렌더링 트리를 생성하려면 브라우저가 대략적으로 다음 작업을 수행합니다.
1. DOM 트리의 q루트에서 시작하여 표시되는 노드 각각을 트래버스합니다.
일부 노드는 표시되지 않으며, 렌더링된 출력에 반영되지 않으므로 생략됩니다.( ex)스크립트 태그, 메타 태그 등)
일부 노드는 CSS를 통해 숨겨지며 렌더링 트리에서도 생략됩니다.
예를 들어, 노드의 경우 속성을 설정하는 명시적 규칙이 있기 때문에 렌더링 트리에서 누락됩니다.
2. 표시된 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용합니다.
3. 표시된 노드를 콘텐츠 및 계산된 스타일과 함께 내보냅니다.
최종 출력은 화면에 표시되는 모든 노드의 콘텐츠 및 스타일 정보를 모두 포함하는 렌더링 트리입니다.
렌더링 트리가 생성되었응므로 '레이아웃' 단계로 진행할 수 있습니다.
지금까지 표시할 노드와 해당 노드의 계산된 스타일을 계산했습니다. 하지만 기기의 뷰포트 내에서 이러한 노드의 정확한 위치와 크기를 계산하지는 않았습니다.---이것이 바로 '레이아웃' 단계이며, 경우에 따라 '리플로우'라고도 합니다.
페이지에서 각 객체의 정확한 크기와 위치를 파악하기 위해 브라우저는 렌더링 트리의 루트에서 시작하여 렌더링 트리를 트래버스합니다.
레이아웃 프로세스에서는 뷰포트 내에서 각 요소의 정확한 위치와 크기를 정확하게 캡처하는 '상자 모델'이 출력됩니다. 모든 상대적인 측정값은 화면에서 절대적인 픽셀로 변환됩니다.
마지막으로, 이제 표시되는 노드와 해당 노드의 계산된 스타일 및 기하학적 형태에 대해 파악했으므로, 렌더링 트리의 각 노드를 화면의 실제 픽셀로 변환하는 마지막 단계로 이러한 정보를 전달할 수 있습니다. 이 단계를 흔히 '페인팅' 또는 '래스터화'라고 합니다.
다음은 브라우저의 단계를 빠르게 되짚어 보겠습니다.
1. HTML 마크업을 처리하고 DOM 트리를 빌드합니다.
2. CSS 마크업을 처리하고 CSSOM 트리를 빌드합니다.
3. DOM 및 CSSOM을 결합하여 렌더링 트리를 형성합니다.
4. 렌더링 트리에서 레이아웃을 실행하여 각 노드의 기하학적 형태를 계산합니다.
5. 개별 노드를 화면에 페인트합니다.
여기에 표시된 데모 페이지는 간단해 보일 수 있지만, 이 페이지에도 꽤 많은 작업이 필요합니다. DOM 또는 CSSOM이 수정된 경우, 화면에 다시 렌더링할 필요가 있는 픽셀을 파악하려면 이 프로세스를 다시 반복해야 합니다.
주요 렌더링 경로를 최적화하는 작업 은 위 단계에서 1단계~5단계를 수행할 때 걸린 총 시간을 최소화하는 프로세스입니다. 이렇게 하면 콘텐츠를 가능한 한 빨리 화면에 렌더링할 수 있으며, 초기 렌더링 후 화면 업데이트 사이의 시간을 줄여 줍니다. 따라서 대화형 콘텐츠의 새로고침 속도를 높일 수 있습니다.
자바스크립트로 상호작용 추가
자바스크립트를 사용하면 콘텐츠, 스타일 지정, 사용자 상호작용에 대한 응답 등 페이지의 거의 모든 측면을 수정할 수 있습니다. 하지만, 자바스크립트는 DOM 생성을 차단하고 페이지가 렌더링될 때 지연시킬 수도 있습니다. 최적의 성능을 제공하려면 자바스크립트를 비동기로 설정하고 주요 렌더링 경로에서 불필요한 자바스크립트를 제거하세요.
1. 자바스크립트는 DOM 및 CSSOM을 쿼리하고 수정할 수 있습니다.
2. 자바스크립트 실행은 CSSOM을 차단합니다.
3. 자바스크립트는 명시적으로 비동기로 선언되지 않은 경우 DOM 생성을 차단합니다.
자바스크립트는 브라우저에서 실행되고 페이지 동작 방식에 대한 거의 모든 측면을 변경할 수 있게 하는 동적 언어입니다. DOM 트리에서 요소를 추가하고 제거하여 콘텐츠를 수정하거나, 각 요소의 CSSOM 속성을 수정하거나, 사용자 입력을 처리하는 등의 많은 작업을 수행할 수 있습니다.
자바스크립트를 사용하면 DOM에서 새로운 요소를 생성, 추가, 제거하고 이 요소의 스타일을 지정할 수 있습니다. 기술적으로 볼 때, 전체 페이지는 요소를 하나씩 생성하고 이 요소의 스타일을 지정하는 하나의 커다란 자바스크립트 파일일 수 있습니다. 이 파일도 작동하기는 하지만 실제로는 HTML 및 CSS를 이용하는 것이 휠씬 더 쉽습니다. 자바스크립트 함수의 두 번째 부분에서 새로운 div 요소를 생성하고, 해당 텍스트 콘텐츠를 설정하고, 스타일을 지정하고, 본문에 추가합니다.
그러나 자바스크립트는 성능이 뛰어난 반면, 페이지의 렌더링 방식과 시기에 있어 많은 제한이 있습니다.
HTML 파서는 스크립트 태그를 만나면 DOM 생성 프로세스를 중지하고 자바스크립트 엔진에 제어 권한을 넘깁니다. 자바스크립트 엔진의 실행이 완료될 후 브라우저가 중지했던 시점부터 DOM 생성을 재개합니다.
다시 말해서, 요소가 아직 처리되지 않았기 때문에 스크립트 블록이 페이지의 뒷부분에서 어떠한 요소도 찾을 수 없습니다. 즉, 인라인 스크립트를 실행하면 DOM 생성이 차단되고, 이로 인해 초기 렌더링도 지연되게 됩니다.
이 뿐만 아니라, 스크립트가 DOM뿐만 아니라 CSSOM 속성도 읽고 수정할 수 있다는 점입니다.
하지만, 스크립트를 실행하려는 경우 브라우저가 CSSOM을 다운로드하고 빌드하는 작업을 완료하지 않았으면 어떻게 될까요? 답은 간단하지만 성능에는 그다지 좋지 않습니다. 브라우저가 CSSOM을 다운로드하고 생성하는 작업을 완료할 때까지 스크립트 실행 및 DOM 생성을 지연시킵니다.
간단히 말해서, 자바스크립트에서는 DOM, CSSOM 및 자바스크립트 실행 간에 여러 가지 새로운 종속성을 도입합니다. 이 때문에 브라우저가 화면에서 페이지를 처리하고 렌더링할 때 상당한 지연이 발생할 수 있습니다.
1. 문서에서 스크립트의 위치는 중요합니다.
2. 브라우저가 스크립트 태그를 만나면 이 스크립트가 실행 종료될 때까지 DOM 생성이 일시 중지됩니다.
3. 자바스크립트는 DOM 및 CSSOM을 쿼리하고 수정할 수 있습니다.
4. 자바스크립트 실행은 CSSOM이 준비될 때까지 일시 중지됩니다.
일반적으로 '주요 렌더링 경로 최적화'란 HTML, CSS 및 자바스크립트 간의 종속성 그래프를 이해하고 최적화하는 것을 말합니다.
파서 차단 대 비동기 자바스크립트
기본적으로, 자바스크립트 실행은 '파서를 차단'합니다. 브라우저가 문서 내에서 스크립트를 만나면 DOM 생성을 중지시키고, 자바스크립트 런타임에 제어 권한을 넘겨 스크립트가 실행되도록 한 후 DOM 생성을 계속합니다. 우리는 앞의 예시를 통해 인라인 스크립트에서 이러한 동작이 수행되는 것을 확인했습니다. 실제로, 실행을 지연시킬 추가적인 코드를 작성하지 않는 한 인라인 스크립트는 항상 파서를 차단합니다.
저희가 'script' 태그를 사용하든 인라인 자바스크립트 스니펫을 사용하든 간에 여러분은 이 둘이 동일한 방식으로 동작할 것으로 기대합니다. 두 경우 모두 브라우저가 일시 중지하고 스크립트를 실행해야만 문서의 나머지 부분을 처리할 수 있습니다. 하지만, 외부 자바스크립트 파일의 경우 브라우저가 일시 중지하고 디스크, 캐시 또는 원격 서버에서 스크립트를 가져올 때까지 기다려야 합니다. 이로 인해 주요 렌더링 경로에 수십~수천 밀리초의 지연이 추가로 발생할 수 있습니다.
기본적으로 모든 자바스크립트는 파서를 차단합니다. 브라우저는 스크립트가 페이지에서 무엇을 수행할지 모르기 때문에, 브라우저는 최악의 시나리오를 가정하고 파서를 차단합니다. 스크립트가 참조되는 바로 그 지점에서 이 스크립트를 실행할 필요가 없음을 브라우저에 신호로 알려준다면, 브라우저가 계속해서 DOM을 구성할 수 있고 준비가 끝난 후에 스크립트를 실행할 수 있습니다 (예: 파일을 캐시나 원격 서버에서 가져온 후에 스크립트를 실행).
async 키워드를 스크립트 태그에 추가하면, 스크립트가 사용 가능해질 때까지 기다리는 동안 DOM 생성을 차단하지 말라고 브라우저에 지시하는 것입니다. 이 경우 성능이 크게 향상됩니다.