본문 바로가기

브라우저 동작 원리와 VSync

이 그림을 알고 있으면 노룩 패스

(출처) The Anatomy of a Frame


본 포스팅은 사내 기술 교육 '브라우저 동작 원리' 강의 영상을 수강 후 정리한 글입니다.

1. 브라우저 동작 원리 (Chrome DevTool 기반)

크롬 개발자 도구 예시

* 중간중간 음영 색상이 들어간 표시는 위의 그림에서 마킹된 색상처럼 크롬 개발자 도구에서 확인할 수 있는 과정을 말한다.

1.1 HTML, CSS 파싱

위의 개발자도구 화면에서 Parse HTML에 해당하는 부분이다.

 

- HTML 파싱하는 이유? : DOM 을 생성하여 엘리먼트를 가공 및 관리하는 표준을 구성하기 위해

- DOM 트리를 만드는 이유 : 자바스크립트 등에 의해 동적으로 변하는 엘리먼트를 쉽게 수정하고 관리하기 위해

파싱하는 과정

바이트 => 문자열 => tokenizing(태깅) => node => DOM 트리 구성

HTML 파싱 과정

1.2 CSS 파싱 및 CSSOM과 DOM Tree 연결

CSS 파싱 과정은 개발자 도구에서 표시되지 않는다. Recalculate Style은 CSS 파싱 결과물인 CSSOM(CSS Object Model)의 시각정보와 DOM Tree를 연결하는 데 걸리는 시간을 말한다. 이 작업을 Attachment라고도 부르기도 하며 그 결과로 렌더 트리를 얻을 수 있다.

CSSOM의 특징

외부 링크(<link>)로 css가 정의된 경우, 렌더링이 블로킹되어 heavy한 스타일 작업의 경우 first paint가 느려질 수 있다.

또한 css는 부모 스타일이 자식 스타일에 상속되는 cascade down을 위해 트리 형태로 구성된다.

1.3 렌더 트리

위의 Recalculate Style의 결과물로 렌더 트리가 나온다. 기억해야 할 점은 렌더트리는 화면에 보이는 요소들로만 구성된다. 따라서 display: none 의 경우에는 렌더 트리에 포함되지 않는다.

1.4 Layout

CSS 2.1 Visual formatting model을 기준으로 노드들의 좌표를 계산하는 작업이다. 위에서 렌더트리의 요소들을 박스라고 칭하며 이 박스의 크기와 위치를 계산하는 작업이다. Global incremental Layout인 특징이 있어 전체 윈도우 사이즈가 변할 시 전역적으로 계산하면서도 부분적으로 요소의 크기와 위치가 변하면 해당 요소와 관련한 주변의 크기와 위치만을 변경한다.

Global Layout 발생

- viewport (너비)

- font-size, font-family 등 폰트 변경 (height이 폰트의 높이에 의해 결정됨)

 

위의 상태 이외의 변경은 Incremental Layout이 발생한다.

HTML5 블록 요소 종류

더보기

address

article

aside

audio

blockquote

canvas

dd

div

dl

fieldset

figcaption

figure

footer

form

h1, h2, h3, h4, h5, h6

header

hgroup

hr

noscript

ol

output

p

pre

section

table

ul

video

{ display: block; }

1.5 Update Layer Tree (new)

렌더링에 사용될 최종 레이어들을 계산해서 생성하는 작업이다. 아래 그림처럼 하나의 화면을 레이어 생성 조건에 따라 여러 레이어들로 나누고 생성한 후 이들을 합성하여 화면을 완성한다. 레이어를 활용하면 빠르게 화면을 구성할 수 있지만 메모리를 많이 쓰게 되어 자원이 낭비될 수 있다. 보통 30개 내외의 레이어를 사용할 것을 권장한다.

(출처) FrontEnd 개발자가 수행하는 성능 개선 작업- 손찬욱

 

레이어 생성 조건

  • root object
  • position: relative, absolute
  • overflow, alpha mask, reflection
  • css filter
  • transform, animations
  • <canvas>, <video>
  • will-change : 브라우저가 메모리를 최적화할수 있음, 주의할 점은 링크를 참조
  • Browser Internal layers

1.6 Paint

위의 레이어 정보와 layout 정보를 기반으로 각 레이어를 픽셀 단위로 그리는 작업이다. 변화된 타일만을 갱신하는 Tiled Backing Store 기법으로 진행한다. Paint 작업은 픽셀 단위로 그리기 때문에 느리다는 것에 유념한다.

1.7 Composite Layers

위에서 완성된 레이어들을 합성하여 한 장의 bitmap으로 만든다. 합성 과정이 오래 걸리면 레이어를 많이 사용하고 있음을 예상할 수 있다.

2. 브라우저가 프레임을 그리는 과정

프레임이란 위의 브라우저 동작 과정을 통해 나오는 독립적인 단위를 말한다.

 

프레임은 모니터에서 프레임 버퍼에 저장된 내용을 가져와서 GPU로 전달되는데 이 때, 메모리에서 프레임을 fetch하는 속도는 일반적으로 60hz(16.6ms)이다. 이말은 즉슨 HTML 파싱부터 레이어 합성까지의 단계를 16.6ms에 완료해야 한다. 하지만 보통은 16.6ms 내로 끝내기 어려우며 더불어 CPU와 GPU의 작업 완료시점이 다름에 따라 어긋나게 동작하여 어떤 시점에서는 화면이 보이지 않는 janking 현상을 겪을 수도 있다. VSync는 프레임을 생성하는 작업의 sync를 맞춰주어 부드러운 화면 동작을 가능케 하며 프레임을 그리는 이외의 작업(보통은 쓸모없는)을 방지한다.

(출처) 웹 성능 최적화에 필요한 브라우저의 모든 것 - Naver Deview 2018

2.1 Main thread rendering

브라우저는 싱글 스레드로 동작하기 때문에 1의 모든 과정을 16.6ms안에 끝내는 건 현실적으로는 어렵다.

(참고) 브라우저는 멀티코어 왜 못쓸까?
웹 아키텍쳐 (HTML, JS)가 동기적이기 때문에 JS가 동작할 때 DOM의 형태가 그 때의 결과물과 맞아야 하기 때문이다.
또는 Tree 개수가 너무 많아 스레드별로 job의 분리가 어려워 오히려 병렬 처리를 위해 Tree를 copy하는 시간 낭비가 발생하기 때문이다.

따라서 기본적으로는 싱글 스레드이나 별도의 스레드를 두어 작업을 병렬적으로 진행하는 방법을 통해 16.6ms라는 시간을 맞추는 방식을 사용한다.

Compositor Thread

먼저 Compositor Thread를 두어 Composite 과정만 스레드를 분리하여 진행한다. 합성은 레이어와 z-index보만 있으면 가능하기 때문이다.

 

Compositor Thread 덕분에 레이어 합성만 다시하면 가능한 기능들의 속도가 더욱 빨라지게 되었다. 대표적으로 스크롤링, 애니메이션, 줌인/아웃 등이 있다.

 

화면을 스크롤 또는 줌인/아웃을 하다보면 도중에 아무것도 그려지지 않아 흰 화면이 노출되기도 하는데 Compositor Thread는 가지고 있는 레이어 정보만을 그릴 수 있어 그 영역을 벗어나면 그릴 수 없기 때문이다.

Raster Thread

Paint 과정의 일부를 Raster Thread로 분리하였다. Paint 과정 중 Record만 메인스레드에서 수행하고 Raster Thread가 기록된 recordplay back해서 raster를 수행한다.

Raster? 기록된 Graphic Command를 bitmap으로 만드는 과정

이 두 개의 쓰레드가 추가된 과정은 아래의 그림으로 표현할 수 있다.

3. VSync 기반 브라우저 프로세스

브라우저는 멀티 프로세스 기반으로 다수의 렌더러 프로세스가 작업을 수행하고 이를 GPU 프로세스에 전달한다.

 

아까 위에서 프레임 버퍼의 데이터가 GPU에게 전달되어 VSync tick에 맟춰 프레임을 fetch하는데 만약 VSync tick 중에 디바이스 기반의 사용자 인풋(텍스트 입력, 화면 터치 등)이 그 과정에서 들어온다면 어떻게 처리될까?

3.1 VSync aligned input handling

브라우저는 사용자 인풋이 들어오면 반드시 VSync 이전에 이벤트를 처리한 이후에 VSync tick에 맞춰 프레임을 생성하는 작업을 수행하는 것을 보장한다.

 

사용자 인풋을 맨 처음 수행하는 이유는 대부분의 경우에 사용자 인풋으로 인한 화면 상의 변화가 일어나기 때문에 인풋으로 인한 결과를 먼저 생성한 후 VSync tick에서 이에 맞춰 화면을 그리면 효율적이기 때문이다.

requestAnimationFrame (이하 rAF)
rAF는 VSync tick의 시작과 동시에 실행하는 함수로 rendering pipeline에서 input을 처리한 후에 rAF를 실행한 이후에 레이아웃 및 페인트 작업을 수행한다.

(출처) Event dispatch diagram

3.2 VSync based browser processing

단일 VSync tick에서 프레임을 생성하는 작업이 완료되고 남은 시간이 생길 수 있다.

 

이러한 빈 시간을 idle period라고 하며 idle한 시간동안 브라우저는 GC를 실행하거나 연결된 idle callback을 수행한다.

Idle Callback
로깅 등 중요도가 떨어지는 작업을 idle callback에 연결해두면 VSync pipeline을 효율적으로 활용 가능하다.

4. Rendering Pipeline Stage Costs

이제 브라우저가 화면을 렌더링하는 동작 방식을 알았으니 실 서비스에서 프론트엔드 단의 성능 개선을 위해 어느 비용을 줄여햐 하는지에 대해 알아본다.

 

렌더링하는 과정에서 비용이 발생할 수 있는 부분을 축약하면 다음과 같은데

JS -> Layout -> Paint -> Composite

각 단계에 영향을 주는 속성과 동작을 이해하는 것이 중요하다.

 

이전 글인 [프론트엔드] 성능 최적화 정리에서 해당 내용에 대해 한번 다룬 적이 있어 중복된 내용은 생략하고 새로 알게 된 부분만 정리하였다.

 

Layout, Paint, Composite가 발생하는 css 속성은 Css Triggers에서 일목요연하게 정리해둔 표로 확인할 수 있다.

Css Triggers 화면 예시

그외의 특징으로는 다음을 주의한다.

Layout

  • 리액트와 같은 최신 프레임워크는 virtual DOM 등을 사용하여 실제 DOM 요소의 개수를 줄였다.
  • 애니메이션을 구현할 때는 DOM을 직접 건드리는 액션(top, width 등)을 지양한다. 따라서 transform, rAF를 활용한다.

Composite

  • 레이어가 너무 많이 쌓이면 그만큼 메모리를 사용해서 Composite 과정이 지연되어 되려 느려질 수도 있으므로 약 30개 정도 내에서 사용한다.