원문 : https://developers.google.com/web/updates/2018/09/inside-browser-part3 (by. Mariko Kosaka)
시리즈 세 번째 글에서는 렌더러 프로세스가 HTML 문서를 받았을 때 어떤 절차를 거쳐 화면을 구성하는지를 설명합니다
이 과정을 효율적으로 처리하기 위해 렌더러 프로세스가 어떤 아키텍처를 가지고 있는지 살펴봅니다
또한 웹 개발자가 고려하면 좋을 내용을 소개합니다
렌더러 프로세스는 탭 내부에서 발생하는 모든 작업을 담당
렌더러 프로세스의 메인 스레드가 브라우저로 전송된 대부분 코드를 처리
간혹 웹 워커나 서비스 워커를 사용하는 경우에는 워커 스레드가 JavaScript 코드의 일부를 처리
웹 페이지를 효율적이고 부드럽게 렌더링하기 위해 별도의 컴포지터 스레드와 래스터 스레드가 렌더러 프로세스에서 실행
렌더러 프로세스의 주요 역할은 HTML과 CSS, JavaScript를 사용자와 상호작용을 할 수 잇는 웹 페이지로 변환하는 것이다
페이지를 이동하는 내비게이션 실행 메시지를 렌더러 프로세스가 받음
HTML 데이터를 수신하기 시작
렌더러 프로세스의 메인 스레드는 문자열(HTML)을 파싱해서 DOM(Document object model)으로 변환하기 시작
DOM은 브라우저가 내부적으로 웹 페이지를 표현하는 방법
또한 JavaScript를 통해 상호작용을 할 수 있는 데이터 구조이자 API
HTML 문서를 DOM으로 파싱하는 방법은 HTML 표준에 정의 https://html.spec.whatwg.org/
브라우저에서 HTML 문서를 열었을 때 오류를 반환받은 적이 없었을 것
예를 들어 닫는 </p> 태그가 누락된 HTML도 유효한 HTML이다
오류를 우아하게 처리하도록 HTML 명세가 설계됐기 때문
이러한 일이 어떻게 처리되는지 궁금하다면 아래의 글을 읽어보기
https://html.spec.whatwg.org/multipage/parsing.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser

메인 스레드는 CSS를 파싱하고 각 DOM 노드에 해당되는 계산된 스타일(computed style)을 확정한다
계산된 스타일은 CSS 선택자(selector)로 구분되는 요소에 적용될 스타일에 관한 정보이다
개발자 도구의 computed 패널에서 이 정보를 볼 수 있다
CSS를 전혀 적용하지 않아도 DOM 노드에는 계산된 스타일이 적용되어 있다
h1 태그는 h2 태그보다 크게 표시되며 바깥 여백(margin)이 모든 요소에 적용된다
브라우저에 기본 스타일 시트가 있기 때문이다
Chromium 소스 코드의 html.css https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/resources/html.css

계산된 스타일이 있는 DOM 트리를 돌며 레이아웃 트리를 생성하는 메인 스레드
웹 페이지의 레이아웃을 결정하는 것은 어려운 작업
가장 단순하게 위에서 아래로 펼쳐지는 블록 영역 하나만 있는 웹 페이지의 레이아웃을 결정할 때에도 폰트의 크기가 얼마이고 줄 바꿈을 어디서 해야 하는지 고려해야 함
단락의 크기와 모양이 바뀔 수 있고, 다음 단락의 위치에 영향이 있기 때문
CSS는 요소를 한쪽으로 흐르게(float) 하거나, 크기를 벗어난 부분을 보이지 않게 하거나, 글이 쓰이는 방향을 변경할 수 있음
레이아웃 단계가 엄청난 임무를 맡고 있다는 것을 알 수 있음
Chrome에서는 한 팀이 레이아웃만 전담하고 있을 정도
레이아웃 전담 팀이 하는 일을 자세히 알고 싶다면 https://www.youtube.com/watch?v=Y5Xa4H2wtVA
파싱, 스타일 계산, 레이아웃에 관한 더 자세한 내용
브라우저는 어떻게 동작하는가? https://d2.naver.com/helloworld/59361
레이아웃 ~ 페인트 사이에 한 가지 작업이 더 있음
레이아웃 트리를 순회하면서 속성 트리(property tree)를 만드는 작업
속성 트리는 clip, transform, opacity등의 속성 정보만 가진 트리
기존에는 이런 정보를 분리하지 않고 노드마다 가지고 있었음
그래서 특정 노드의 속성이 변경되면 해당 노드의 하위 노드에도 이 값을 다시 반영하면서 노드를 순회해야 했음
최신 Chrome에서는 이런 속성만 별도로 관리하고 각 노드에서는 속성 트리의 노드를 참조하는 방식으로 변경되고 있음
DOM, 스타일, 레이아웃을 가지고도 여전히 페이지를 렌더링할 수 없음
요소의 크기, 모양, 위치를 알더라도 어떤 순서로 그려야 할지 판단해야 함
예를 들어 어떤 요소에 z-index 속성이 적용되었다면 HTML에 작성된 순서로 요소를 그리면 잘못 렌더링된 화면이 나옴
페인트 단계에서 메인 스레드는 페인트 기록(paint record)을 생성하기 위해 레이아웃 트리를 순회 함
페인트 기록은 '배경 먼저, 다음은 텍스트, 그리고 직사각형'과 같이 페인팅 과정을 기록한 것

DOM 트리 및 스타일
레이아웃 트리
페인트 트리의 순서로 생성
렌더링 파이프라인에서 중요한 점 : 각 단계에서 이전 작업의 결과가 새 데이터를 만드는데 사용
예를 들어 레이아웃 트리에서 변경이 생겨 문서의 일부가 영향을 받으면 페인팅 순서도 새로 생성해야 함
요소에 애니메이션을 적용하면 브라우저는 모든 프레임 사이에서 이러한 작업을 해야 함
요소의 움직임이 모든 프레임에 반영되어야 사람이 볼 때 부드럽게 느껴짐
애니메이션에서 프레임이 누락되면 웹 페이지가 '버벅대는(janky)' 것처럼 보임
화면 주사율에 맞추어 렌더링 작업이 이루어져도 이 작업은 메인 스레드에서 실행되기 때문에 애플리케이션이 JavaScript르 실행하는 동안 렌더링이 막힐 수 있음
JavaScript 작업을 작은 덩어리로 나누고 requestAnimationFrame() 메서드를 사용해 프레임마다 실행하도록 스케쥴을 관리할 수 있음
자세한 내용은 https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution 을 참고
메인 스레드를 막지 않기 위해 웹 워커에서 JavaScript를 실행할 수 도 있음 https://www.youtube.com/watch?v=X57mh8tKkgE
참고
requestAnimationFrame() 메서드를 통해 등록한 콜백 함수는 프레임마다 실행된다
프레임의 간격은 모니터의 주사율에 따라 다를 수 있다
브라우저는 VSync 시그널로 프레임 간격을 파악한다
브라우저와 VSync에 관한 더 자세한 내용은
"브라우저는 VSync를 어떻게 활용하고 있을까" 발표의 자료를 참고한다
https://deview.kr/2015/schedule#session/87
이제 브라우저는 문서의 구조와, 각 요소의 스타일, 요소의 기하학적 속성, 페인트 순서를 알고 있다
브라우저는 웹 페이지를 어떻게 그릴까?
이 정보를 화면의 픽셀로 변환하는 작업을 래스터화(rasterizing)라고 한다
가장 단순한 래스터화는 아마 뷰포트 안쪽을 래스터하는 것일 것이다
사용자가 웹 페이지를 스크롤하면 이미 래스터화한 프레임을 움직이고 나머지 빈 부분을 추가로 래스터화한다
이 방식은 Chrome이 처음 출시되었을 때 래스터화한 방식이다
그러나 최신 브라우저는 합성(compositing)이라는 보다 정교한 과정을 거친다
역주
렌더링 파이프라인에서는 이 단계부터 GPU가 많이 사용된다
참고로 소개했던 "Mythbusters Demo GPU versus CPU" 영상을 생각해보면 레이어를 합성할 때
GPU가 좀더 유리하다는 것을 쉽게 이해할 수 있을 것이다
https://www.youtube.com/watch?v=-P28LKWTzrI
렌더링 파이프라인을 설명할 때 '페인트(paint)와' '그리기(draw)'라는 용어가
다르게 사용되고 있다는 점에서 주목해야 한다
페인트는 페인트 작업(paint operation)을 만들어 내는 것을 의미하고,
그리기는 페인트 작업을 기반으로 비트맵이나 텍스처를 만들어 내는 것을 의미한다
좀 더 정확히는 합성 프레임(compositing frame)을 만들어 내는 것을 지칭한다
어떤 요소가 어떤 레이어에 있어야 하는지 확인하기 위해 메인 스레드는 레이아웃 트리를 순회하며 레이어 트리를 만든다
개발자 도구의 Performance 패널에서 Update Layer Tree라고 되어 있음
뷰포트로 미끄러져 들어오는 슬라이드인 메뉴처럼 별도의 레이어여야 하는 웹 페이지의 어떤 부분이 별도의 레이어가 아니라면 CSS의 will-change 속서을 사용해 브라우저가 레이어를 생성하게 힌트를 줄 수 있다
모든 요소에 레이어를 할당하면 좋을 것 같지만 수많은 레이어를 합성하는 작업은 웹 페이지의 작은 부분을 매 프레임마다 새로 래스터화하는 작업보다 더 오래 걸릴 수 있다
애플리케이션의 렌더링 성능은 직접 측정해봐야 한다
레이어 합성과 레이어 수, 성능에 관한 더 자세한 내용은
컴포지터 전용 속성 고수와 레이어 수 관리 글을 참고 한다 https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count
역주
레이어가 많으면 합성 비용이 높을 뿐만 아니라
레이어를 메모리에 가지고 있어야 하는 부담도 있다
Chrome은 레이어가 과도하게 많아지는 것을 막기 위해
특정한 경우에는 레이어를 생성하지 않거나 합치기도한다
레이어 트리가 생성되고 페인트 순서가 결정되면 메인 스레드가 해당 정보를 컴포지터 스레드에 넘긴다(commit)
그러면 컴포지터 스레드는 각 레이어를 래스터화한다
어떤 레이어는 페이지의 전체 길이만큼 클 수 있다
그래서 컴포지터 스레드는 레이어를 타일(tile) 형태로 나눠 각 타일을 래스터 스레드로 보낸다
래스터 스레드는 각 타일을 래스터화해 GPU 메모리에 저장한다
컴포지터 스레드는 래스터 스레드간의 우선순위를 지정할 수 있어서 뷰포트 안이나 근처의 것들이 먼저 래스터화될 수 있다
또한 레이어는 줌인 같은 동작을 처리하기 위해 여러 해상도별로 타일 세트를 여러벌 가지고 있다
타일이 래스터화되면 컴포지터 스레드는 '합성 프레임'을 생성하기 위해 타일의 정보를 모은다
이 타일의 정보를 '드로 쿼드(draw quads)'라고 부른다
드로 쿼드 : 메모리에서 타일의 위치와 웹 페이지 합성을 고려해 타일을 웹 페이지의 어디에 그려야 하는지에 관한 정보를 가지고 있음
합성 프레임 : 웹 페이지의 프레임을 나타내는 드로 쿼드의 모음
이후에 합성 프레임이 IPC를 통해 브라우저 프로세스로 전송 됨
이 시점에 브라우저 UI의 변경을 반영하려는 UI 스레드나 확장 앱을 위한 달느 렌더러 프로세스에 의해 합성 프레임이 더 추가될 수 있음
이러한 합성 프레임은 GPU로 전송되어 화면에 표시
스크롤 이벤트가 발생하면 컴포지터 스레드는 GPU로 보낼 다른 합성 프레임을 만듬
