브라우저 렌더링

hansol yang
10 min readMar 1, 2022

--

브라우저 주소창에 입력하는 주소, 즉 URL은 인터넷에서 웹 페이지, 이미지, 비디오 등 리소스의 위치를 가리키는 문자열입니다. 브라우저는 해당 위치로 자원을 요청하고, 응답 받습니다. (해당 과정의 수행을 모두 브라우저가 하는 것은 아닙니다. 참고: (⚡️ 브라우저 주소창에 URL을 치면 일어나는 일들))

응답을 받으면 렌더링이 시작됩니다. 과정은 대략 다음과 같습니다.

  1. HTML, CSS를 파싱하고 각각 DOM, CSSOM 트리를 생성한다.
  2. DOM 및 CSSOM을 결합하여 렌더 트리를 형성합니다.
  3. 렌더 트리에서 레이아웃을 실행하여 각 노드의 기하학적 형태를 계산합니다. — layout, reflow
  4. 개별 노드를 화면에 페인트합니다. — paint

HTML, CSS를 파싱하고 각각 DOM, CSSOM 트리를 생성한다.

HTML, CSS 파싱 등의 작업은 렌더링 엔진이 수행합니다. 렌더링 엔진의 역할은 요청 받은 내용을 브라우저 화면에 표시하는 일입니다. 렌더링 엔진은 HTML, XML, 이미지를 표시할 수 있으며, 플러그인이나 브라우저 확장 기능을 이용해 PDF와 같은 다른 유형도 표시할 수 있습니다. 대표적인 렌더링 엔진으로 게코, 웹킷이 있습니다.

브라우저가 페이지를 렌더링하려면 DOM 및 CSSOM 트리를 생성해야합니다. 따라서 렌더링 속도를 높이려면 HTML 및 CSS 를 가능한 빨리 브라우저에 제공해야 합니다.

참고로 DOM과 CSSOM 이 트리구조를 가지는 이유는 DOM은 HTML의 ‘관계’를 표현하기 위하여, CSSOM은 규칙 적용 순서(cascade down)를 판단하기 위함입니다.

DOM(Document Object Model)

브라우저는 HTML 마크업을 처리할 때마다 다음 단계를 수행합니다.

HTML의 원시 바이트를 읽어와서, 해당 파일에 대해 지정된 인코딩(예: UTF-8)에 따라 문자로 변환하고, HTML5 표준에 지정된 토큰으로 변환합니다. 토큰은 속성 및 규칙을 정의하는 ‘객체’로 변환되고, HTML 마크업이 여러 태그간의 관계를 정의하기 때문에 생성된 객체는 트리 데이터 구조 내에 연결됩니다. 이러한 과정을 거쳐 DOM 트리를 빌드합니다.

HTML 파싱 및 DOM 빌드 과정: `Bytes -> Characters -> Tokens -> Nodes -> DOM`

브라우저는 HTML 마크업을 처리할 때마다 위의 모든 단계를 수행합니다. 브라우저가 대량의 HTML을 처리해야 한다면 병목 현상이 발생할 수 있습니다.

DOM 트리는 문서 마크업의 속성 및 관계를 포함하지만 요소가 렌더링 될 때 어떻게 표시될지에 대해서는 알려주지 않습니다. 이것은 CSSOM 의 책임입니다.

CSSOM(Css Object Model)

브라우저는 DOM을 생성하는 동안 외부 CSS 스타일 시트를 참조하는 문서의 head 영역에서 link 태그를 접할 경우 해당 자원에 대한 요청을 발송합니다.

HTML과 마찬가지로, 수신된 CSS 규칙을 브라우저가 이해하고 처리할 수 있는 형식으로 변환해야 합니다. 따라서 HTML 대신 CSS에 대해 파싱 프로세스를 반복합니다.

CSS 바이트가 문자로 변환된 후 차례로 토큰과 노드로 변환되고 마지막으로 ‘Css object Model(CSSOM)’ 이라는 트리 구조에 링크됩니다.

CSS 파싱 및 CSSOM 빌드 과정: `Bytes -> Characters -> Tokens -> Nodes -> CSSOM`

CSSOM이 트리 구조를 가지는 이유는 페이지에 있는 객체의 최종 스타일을 계산할 때 브라우저는 해당 노드에 적용 가능한 가장 일반적인 규칙으로 시작한 후 더윽 구체적인 규칙을 적용하는 방식, 즉 ‘하향식(cascade down)’으로 규칙을 적용하는 방식으로 계산된 스타일을 재귀적으로 세분화하기 때문입니다. 하나 이상의 속성이 정의될 때 cascade down 순서가 이 문제를 해결하게 됩니다.

DOM 및 CSSOM을 결합하여 렌더 트리를 형성합니다.

CSSOM 및 DOM 트리는 결합하여 렌더 트리를 형성합니다. 렌더 트리는 표시되는 각 요소의 레이아웃을 계산하는데 사용되고 픽셀을 화면에 렌더링하는 페인트 프로세스에 대한 입력으로 처리됩니다. 렌더링 성능을 높이기 위해서는 이러한 단계 각각을 최적화하는 것이 중요합니다.

이전 단계에서 생성한 DOM과 CSSOM은 문서의 각기 다른 측면을 가지고있는 서로 독립적인 객체입니다. 하나는 콘텐츠를 설명(DOM)하고, 다른 하나는 문서에 적용되어야 하는 스타일 규칙을 설명(CSSOM)합니다. 두 객체를 병합하여 브라우저가 화면에 픽셀을 렌더링하도록 해야합니다.

먼저, 브라우저가 DOM 및 CSSOM을 렌더 트리에 결합합니다. 렌더 트리는 페이지에 표시되는 모든 DOM 콘텐츠와 각 노드에 대한 모든 CSSOM 스타일 정보를 가집니다. ‘페이지에 표시되는’이라는 말은, 다시 말하면 페이지를 렌더링하는데 필요한 노드만을 포함한다는 뜻이기도 합니다.

렌더 트리를 생성하려면 브라우저가 대략적으로 다음 작업을 수행합니다.

  1. DOM 트리의 루트에서 시작하여 포시되는 노드 각각을 순회합니다.
    - 일부 노드는 표시되지 않으며(예: 스크립트 태그, 메타 태그 등), 렌더링된 출력에 반영되지 않으므로 생략됩니다.
    - 일부 노드는 CSS를 통해 숨겨지며 렌더 트리에서도 생략됩니다.(예: display: none)
  2. 표시된 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용합니다.
  3. 표시된 노드를 콘텐츠 및 계산된 스타일과 함께 내보냅니다.

참고: visibility: hiddendisplay: none 과 다릅니다. visibility: hidden 은 요소를 보이지 않게 만들지만, 여전히 레이아웃에서 공간을 차지합니다.(즉, 비어 있는 상자로 렌더링 됨). 반면, display: none 은 요소가 보이지 않으며 레이아웃에 포함되지도 않도록 렌더 트리에서 요소를 완전히 제거합니다.

최종 출력은 화면에 표시되는 모든 노드의 콘텐츠 및 스타일 정보를 모두 포함하는 렌더 트리입니다. 렌더 트리가 생성되었으므로 ‘레이아웃’ 단계를 진행할 수 있습니다.

렌더 트리에서 레이아웃을 실행하여 각 노드의 기하학적 형태를 계산합니다. — layout, reflow

지금까지의 과정을 통해 표시할 노드와 해당 노드의 스타일을 계산했습니다. 하지만 기기의 뷰포트 내에서 노드의 정확한 위치와 크기를 계산하지는 않았습니다. 이것이 바로 ‘레이아웃’ 단계이며, 경우에 다라 ‘리플로우’라고도 합니다.

페이지에서 각 객체의 정확한 크기와 위치를 파악하기 위해 브라우저는 렌더 트리의 루트에서 시작하여 렌더 트리를 순회합니다.

레이아웃 프로세스에서는 뷰포트 내에서 각 요소의 정확한 위치와 크기를 정확하게 캡쳐하는 ‘박스 모델’이 출력됩니다. 모든 상대적인 측정값은 화면에서 절대적인 픽셀로 변환됩니다.

개별 노드를 화면에 페인트합니다. — paint

레이아웃 과정에서 표시되는 노드와 해당 노드의 계산된 스타일 및 기하학적 형태에 대해 파악했으므로, 렌더 트리의 각 노드를 화면에 실제 픽셀로 변환하는 마지막 단계로 이러한 정보를 전달할 수 있습니다. 이 단계를 흔히 ‘페인팅’ 또는 ‘래스터화’라고 합니다.

Layout, Paint

‘Layout’ 이벤트는 타임라인에서 렌더 트리 생성, 위치 및 크기 계산을 캡쳐합니다.

레이아웃이 완료될 때 브라우저가 ‘Paint Setup’ 및 ‘Paint’ 이벤트를 발생시킵니다. 이러한 작업은 렌더 트리를 화면의 픽셀로 변환합니다.

최적화

지금까지 렌더링 과정을 살펴보았습니다. 일련의 과정들이 점진적으로 진행된다는 것을 아는것이 중요합니다. 렌더링 엔진은 좀 더 나은 사용자 경험을 위해 가능하면 빠르게 내용을 표시하는데, 모든 HTML을 파싱할 때 까지 기다리지 않고 배치와 그리기 과정을 시작합니다. 네트워크로부터 나머지 내용이 전송되기를 기다리는 동시에 받은 내용의 일부를 먼저 화면에 표시하는 것입니다.

렌더 트리 생성, 레이아웃 및 페인트 작업을 수행하는데 필요한 시간은 문서의 크기, 적용된 스타일 및 실행 중인 기기에 따라 달라집니다. 즉, 문서가 클수록 브라우저가 수행해야 하는 작업도 더 많아지며, 스타일이 복잡할수록 페인팅에 걸리는 시간도 늘어납니다. 예를 들어, 단색은 페인트하는데 시간과 작업이 적게 필요한 반면, 그림자 효과는 계산하고 렌더링하는데 시간과 작업이 더 필요합니다.

또한 DOM 또는 CSSOM이 수정된 경우, 화면에 다시 렌더링할 필요가 있는 픽셀을 파악하려면 이 프로세스를 다시 반복해야 합니다.

즉, 렌더링 최적화는 위의 단계들을 수행할 때 걸린 총 시간을 최소화하는 것이라고 할 수 있습니다. 그를 통해 콘텐츠를 가능한 한 빨리 화면에 렌더링 할 수 있으며, 초기 렌더링 후 화면 업데이트 사이의 시간을 줄일 수 있습니다.

CSS

기본적으로, CSS는 렌더링 차단 리소스로 취급됩니다. 즉, CSSOM이 생성될 때까지 브라우저는 처리되는 모든 콘텐츠를 렌더링하지 않습니다. CSS를 간단하게 유지하고 가능한 한 빨리 제공하고, 미디어 유형과 미디어 쿼리를 사용하여 렌더링 차단을 해제해야 합니다.

<link href="style.css" rel="stylesheet" /><link href="print.css" rel="stylesheet" media="print" /><link href="other.css" rel="stylesheet" media="(min-width: 40em)" />

미디어 쿼리를 사용하면 우리가 특정한 사용 사례(예: 표시 또는 인쇄)와 동적인 조건(예: 화면 방향 변경, 크기 조정 이벤트 등)에 맞게 프레젠테이션을 조정할 수 있습니다. 스타일시트 자산을 선언할 때 미디어 유형과 미디어 쿼리에 세심한 주의를 기울여야 합니다. 이러한 요소들은 주요 렌더링 경로의 성능에 큰 영향을 미칩니다.

JavaScript

자바스크립트를 사용하면 콘텐츠, 스타일 지정, 사용자 상호작용에 대한 응답 등 페이지의 거의 모든 측면을 수정할 수 있습니다. 하지만, 자바스크립트는 DOM 생성을 차단하고 페이지가 렌더링될 때 지연시킬 수도 있습니다. 최적의 성능을 제공하려면 자바스크립트를 비동기로 설정하고 주요 렌더링 경로에서 불필요한 자바스크립트를 제거하세요.

자바스크립트는 브라우저에서 실행되고 페이지 동작 방식에 대한 거의 모든 측면을 변경할 수 있게 하는 동적 언어입니다. DOM 트리에서 요소를 추가하고 제거하여 콘텐츠를 수정하거나, 각 요소의 CSSOM 속성을 수정하거나, 사용자 입력을 처리하는 등의 많은 작업을 수행할 수 있습니다.

자바스크립트를 사용하면 DOM에서 새로운 요소를 생성, 추가, 제거하고 이 요소의 스타일을 지정할 수 있습니다. 기술적으로 볼 때, 전체 페이지는 요소를 하나씩 생성하고 이 요소의 스타일을 지정하는 하나의 커다란 자바스크립트 파일일 수 있습니다. 이 파일도 작동하기는 하지만 실제로는 HTML 및 CSS를 이용하는 것이 휠씬 더 쉽습니다.

그러나 자바스크립트는 성능이 뛰어난 반면, 페이지의 렌더링 방식과 시기에 있어 많은 제한이 있습니다.

자바스크립트에서는 DOM, CSSOM 및 자바스크립트 실행 간에 여러 가지 새로운 종속성을 가지게 됩니다. 그렇기 때문에 브라우저가 화면에서 페이지를 처리하고 렌더링할 때 상당한 지연이 발생할 수 있습니다.

문서에서 스크립트의 위치는 중요합니다. 자바스크립트는 DOM 생성을 차단합니다. 브라우저는 `<script>` 태그를 만나면 해당 스크립트가 실행 종료될 때까지 DOM 생성을 일시 중지합니다. 그로인해 초기 렌더링은 지연됩니다. 또한 자바스크립트에서 DOM 요소를 참조하는 경우 참조를 찾을 수 없을 수 있습니다.

또한 자바스크립트를 실행하려는 경우 브라우저가 CSSOM을 다운로드하고 빌드하는 작업을 완료하지 않았다면 브라우저가 CSSOM이 준비될 때까지 스크립트 실행 및 DOM 생성을 지연시킵니다.

위의 언급한 경우들은 역시 성능에 좋지않습니다. 그러므로 HTML, CSS 및 자바스크립트 간의 종속성을 이해하고 최적화하는 것은 중요합니다.

스크립트는 보통 바디의 아래부근에 두거나 `defer`, `async` 등의 사용을 권하는 이유 역시 위의 설명과 같은 맥락입니다. 스크립트 실행을 가장 뒤로 미룸으로써 초기 렌더링을 지연시키지 않기 위함입니다.

주요 렌더링 경로를 최적화하기 위한 일반적인 단계

1. 주요 경로(리소스 수, 바이트 수, 길이)를 분석하고 파악합니다.

2. 주요 리소스를 제거하거나 이에 대한 다운로드를 연기하거나 비동기로 표시하는 등의 방법으로 주요 리소스 수를 최소화합니다.

3. 주요 바이트 수를 최적화하여 다운로드 시간(왕복 수)을 단축합니다.

4. 나머지 주요 리소스가 로드되는 순서를 최적화합니다. 주요 경로 길이를 단축하려면 가능한 한 빨리 모든 주요 자산을 다운로드합니다.

참고

--

--

No responses yet