이 포스트에서는 지난 포스트에 이어 크롬의 브라우저 프로세스가 렌더러 프로세스에게 html data를 넘겨준 이후에 렌더러 프로세스에서 일어나는 일들에 대해 공부한 내용을 기록해두었다.
렌더러 프로세스에도 여러 가지 스레드가 존재한다. 대표적으로 Blink(크롬의 렌더링 엔진)이 포함된 메인 스레드, 메인 스레드에서 만들어진 레이어를 타일화하여 Rester 스레드에 전달하는 Compositor 스레드, GPU를 사용하여 화면을 픽셀화하고 모니터에 그리는 Rester 스레드, 그리고 웹 워커와 서비스 워커를 담당하는 Worker 스레드가 있다.
포스트에서 설명할 렌더링 파이프라인을 다이어그램으로 나타내면 아래와 같다.
1. HTML 파싱
DOM tree 구축
메인 스레드의 Blink 엔진은 2진수(Byte) 형태로 전달된 HTML을 브라우저 프로세스의 네트워크 스레드로부터 받는 순서대로, 특별하고 복잡한 HTML 파싱 알고리즘에 따라 문자열 변환(UTF-8) 토큰화(Tokenization) 과정을 거친다. 여기서 토큰화란 각 문자열을 여는 태그, 닫는 태그, 속성 및 이름 값, 텍스트 콘텐츠 등의 토큰으로 분리하는 과정을 의미한다. HTML 토큰으로 파싱되는 알고리즘은 HTML 표준에 나와 있다. HTML 파일은 파싱이 완료되는 부분부터 차례대로 객체의 일종인 node로 묶이고 tree 형태로 구조화된다. 이렇게 완성된 결과물을 DOM(Document Object Model) tree라고 한다.
하위리소스 불러오기
메인 스레드의 Blink 엔진은 HTML 파일을 받은 순서대로(위에서부터) 파싱한다. 이때 link, script, img 등의 태그를 만나면 위에서부터 순서대로 진행하던 파싱을 멈추고 브라우저 프로세스의 네트워크 스레드에게 각 src 경로에 위치한 파일을 요청한다. 만약 파싱 중 script 태그에서 JavaScript를 만나게 되면 script가 완전히 로드되고 크롬의 JavaScript 엔진인 V8이 script 실행을 모두 마칠 때까지 Blink는 HTML 파싱을 중단한다. 이는 JavaScript의
document.write()
등의 메소드가 DOM 전체 파싱을 다시 요구할 수도 있기 때문에, 엔진의 효율적인 파싱을 위해 그렇게 작동하도록 만들어졌다. 반면 link 태그 등에서 CSS를 만나는 경우에는 파싱을 중단하지 않는다.만약 불러오려는 자바스크립트에
document.write()
등의 메소드가 없어 Blink로부터 HTML 파싱을 멈추게 하고 싶지 않으면 어떻게 할까? 이때는 JavaScript를 호출하는 script 태그에 async
혹은 defer
속성을 추가할 수 있다. async
속성이 있으면 Blink는 JavaScript 코드를 HTML 파싱과 비동기적으로 로드하고 실행한다. defer
속성이 포함된 경우 HTML의 파싱이 완료될 때까지 script의 실행이 지연된다. 이 스크립트는 HTML 파싱이 모두 끝난 후, DOMContentLoaded 이벤트가 발생하기 직전에 실행된다.
2. 스타일 계산
CSSOM
DOM이 화면에 표시되기 위해서는 CSSOM(CSS Object Model)이 필요하다. Blink에서 CSSOM은 다음 과정을 거쳐 만들어진다.
CSSOM 생성 과정
- 먼저 모든 스타일 시트에 존재하는 CSS Rule(선택자+프로퍼티+프로퍼티값)을 파싱하고 인덱싱한다.
- 그리고 각 요소들을 방문하여 적용되는 모든 Rule들을 찾는다.
- 최종적으로 Rule과 기타 정보들을 결합해서 최종적으로 표시될 스타일(ComputedSytle)을 계산한다.
이 과정을 위해 Blink는 먼저 내장 기능인 CSSParser로 모든 스타일시트를 하나로 묶고, StyleSheetContents를 빌드한다. 여기에는 모든 CSS Rules가 포함되어 있다. 이렇게 하나로 묶는 이유는, CSS에는 선택자간 프로퍼티 중복이 빈번하게 발생하는데 이때 우선순위(important)를 계산하기 위함이다. 이후 StyleSheetContents에 우선순위를 반영하여 최종적으로 화면에 그릴 스타일인 ComputedSytle를 계산한다.
Render tree
DOM과 CSSOM이 모두 완성되었다면 Blink는 각 요소를 연결하여 Render tree를 만든다. Render tree는 웹페이지를 브라우저에 시각적으로 표현하기 위해 사용되는 구조로 시각적으로 렌더링 되어야 하는 요소는 모두 포함하고 있으며 각 노드는 해당 요소의 스타일을 포함한다. 이때 DOM에는 존재하지 않지만 CSS에 포함되어 있는 의사 클래스(Pseudo-classes) 콘텐츠(p::before{content:"Hello World} 등)도 Render tree에 포함된다. 반면 DOM에는 있지만 화면에 포함되지 않는 요소들은 Render tree에서 제외되는데 대표적으로 DOM에는 포함되어 있는 head 태그나, css 선언(declaration) 중 display : none이 있다.
3. 레이아웃 계산(Reflow)
Layout tree
Render tree로 각 요소들을 어떻게 그릴 것인지에 대한 정보는 계산이 되었지만, 각 요소를 어디에 그려야 할지, 배치는 어때야 할지에 대한 정보를 추가적으로 계산해줘야 한다. Blink는 Render tree를 훑어가며 Layout tree를 만든다. Layout tree는 각 요소의 x,y 좌표, 박스영역(bounding box - width,height)과 같은 기하학적 정보를 포함하며 Render tree와 마찬가지로 의사클래스의 콘텐츠에 대한 정보도 포함되어 있다. 이렇게 레이아웃을 계산하는 과정을 Reflow라고도 부른다. Layout 계산의 방법은 아래와 같다.
재귀적인 Layout 계산
- 부모(Element)가 자신의 너비를 결정한다.
- 부모가 자식(Element)을 검토한다.
- 자신을 기준으로 자식의 x,y 좌표를 결정한다.
- 자식의 layout을 호출하여 자식의 높이를 계산한다.
- 부모는 자식의 누적된 높이와 여백, 패딩을 사용하여 자신의 높이를 설정한다. 이 값은 부모 렌더러의 부모가 사용하게 된다.
- 더티 비트 플래그를 제거한다.
- 더티 비트 플래그 데이터나 상태의 변경 여부를 나타내는 플래그를 말한다. 주로 캐시와 같은 메모리 구조에서 사용되며, 변경된 데이터를 추적하고 필요한 경우에만 업데이트를 수행하는 데 활용된다. 여기서는 Layout의 계산을 다시 할 필요가 있는 요소를 의미한다.
NodeRenderingData
위에서 만들어진 DOM, CSSOM, Layout에 대한 정보는 모두 NodeRenderingData로 합쳐진다. 즉 NodeRenderingData에는 DOM tree에 관련된 정보와 ComputedSytle에 대한 데이터, 그리고 LayoutObject의 포인터에 관한 데이터가 모두 포함되어 있다.
3-1. Pre-Paint
최신 크롬브라우저는 레이아웃 계산 과정까지 모두 마친 이후에도 좀 더 효과적인 렌더링을 위해 추가적으로 한 가지 작업을 더 수행한다. 이것은 바로 Property tree(속성 트리)들을 분리하는 작업이다. Property tree에는 Transform tree, Clip tree, Effect tree(opacity, filter 등을 포함), Scroll tree 등이 있다. 현대 웹개발에서 좋은 UI/UX를 구현하기 위해 이러한 속성이 자주 변경되는 경우가 많은데 크롬에서는 스타일 속성의 계산과 레이아웃을 분리해 계산을 병렬처리하거나, 또는 재사용하기 위해 이러한 방법을 택하고 있다.
4. 페인트(Paint)
Paint tree
Paint 단계에서 Blink는 Paint record(페인트 기록)을 생성하기 위해 Layout tree를 순회한다. Paint record는 배경 ⇒ 텍스트 → 직사각형 등과 같이 페인팅 과정을 기록한 것이다. z-index 등으로 인해 Layout tree의 순서(DOM에 선언된 노드의 순서)와 실제 화면에 그려야 하는 순서는 다를 수 있기 때문에 이 과정이 필요하다. 이 과정에서 요소의 스타일이 변경되거나, DOM 구조가 수정되어야 할 때가 있는데 이때 발생하는 과정을 Repaint라고 한다.
5. 레이어화
Layerize
이전 브라우저의 작동 방식은 레이어화 없이 페인트 트리를 레스터화(rasterizing - 화면의 정보를 픽셀로 변환)하는 방식으로 이루어졌다. 뷰포트의 안쪽 부분을 레스터화하고, 스크롤이 발생할 경우 새로 뷰포트에 들어온 부분을 다시 레스터화하는 방식으로 비교적 단순하게 작동했다.
크롬을 포함한 현대 브라우저는 화면의 렌더링에 하드웨어 가속(GPU를 사용한 그래픽 가속)을 사용할 수 있게 되었다. 좀 더 큰 용량의 화면도 효율적으로 렌더링 할 수 있도록, Blink는 Layout tree를 순회하며 특정한 기준에 따라 레이어화(Layerize)하고 (OS의 성능이 충분한 경우) 메인 스레드에서 Compositer 스레드로 전달해 합성한다. 크롬의 개발자 도구에서 more option > layer 탭을 확인하면 현재 페이지의 레이어가 어떻게 구분되어 있는지 확인할 수 있다. Blink가 레이어를 구분하는 기준에는 다음과 같은 것들이 있다.
레이어 분리 기준
- Position: fixed or absolute
- video, canvas, iframe 태그가 사용된 경우
- Transform 3D 관련 속성이 사용된 경우
- will-change 등의 속성이 사용된 경우 …
특정 경우에서는 레스터화 비용 < 레이어 합성 비용일 수 있고, 또 레이어를 메모리에 가지고 있어야 하는 부담도 있기 때문에 크롬은 레이어가 과도하게 많아지지 않도록 레이어를 생성하지 않거나 합치는 경우도 있다.
Tiling
레이어화가 모두 완료되었지만 일부 레이어는 화면을 다 채울 만큼 큰 경우도 있다. 합성을 담당하는 Compositer 스레드는 Rester 스레드 간의 우선 순위를 지정할 수 있기 때문에, 뷰포트 혹은 뷰포트 근처의 것들이 먼저 레스터화 되도록 할 수 있다. 이 때문에 Compositer 스레드는 전달받은 레이어를 더 작은 사이즈의 타일로 나눠 뷰포트 내외부의 렌더링을 구분하므로서 좀 더 효율적으로 작동할 수 있게 된다. 각각의 레이어는 줌인 같은 동작을 매끄럽게 처리하기 위해 해상도 별로 타일세트를 가지고 있다. 타일링이 완료되면 Compositer 스레드는 각각의 타일을 Rester 스레드로 보낸다. Rester 스레드는 각 타일을 픽셀별로 비트맵을 생성(레스터화)하여 GPU 메모리에 저장한다.
6. 액티베이트(Activate)
타일이 모두 레스터화되고 나면, Compositer 스레드는 합성 프레임을 생성하기 위해 타일의 정보를 모은다. 이 타일의 정보를 '드로 쿼드(draw quads)'라고 부른다. 이후 합성 프레임이 IPC를 통해 브라우저 프로세스로 전송된다.
7. Aggregate
합성 프레임은 렌더러 프로세스뿐만 아니라, 크롬의 사이트 분리 정책에 의해 iframe 내부에서도 만들어지고, 브라우저 프로세스(브라우저의 전체적인 UI)에서도 만들어진다. 만들어진 여러 합성 프레임들은 GPU 프로세스로 전송되어 하나의 합성프레임으로 합쳐진다.