posts

Browser 렌더링 파이프라인

Apr 23, 2026 updated Apr 23, 2026 assetsbrowsercssrendering

이 문서는 최신 크로미움 베이스 브라우저의 렌더링 동작 방식을 이해하여 개발 및 퍼포먼스 최적화를 진행하는데 도움이 되기위해 작성되었습니다.

크로미움이란 구글에서 2008년 9월 시작한 오픈소스 프로젝트 입니다.

크로미움은 오픈소스 프로젝트 이기 때문에 구글 직원이 아닌 사람들도 참가할 수 있으며 이 프로젝트를 통해 만들어진 소스코드에서 컴파일 된 브라우저가 크로미움 입니다.

크로미움은 V8이라는 자바스크립트 엔진과 Blink( webkit 을 베이스로한 엔진 28 버전부터 사용 )라는 렌더링 엔진을 사용하는 브라우저입니다.

크롬이 크로미움 기반으로 만들어졌다는 것은 오픈 소스인 크로미움 브라우저 코드 위에 살을 덧붙여 개발되었다는 의미입니다.

크로미움 기반으로 만들어진 브라우저들은 크롬, 엣지, 오페라, 네이버 웨일, 삼성인터넷 등등으로 요즘 우리가 사용하는 대다수의 브라우저들이 해당됩니다.

이 글에선 크로미움을 기반으로 렌더링 파이프라인을 설명하지만 약간의 차이들을 보일뿐 렌더링 절차나 방식은 다른 엔진을 사용하는 브라우저들도 대부분 비슷합니다.

배경

본격적인 설명에 앞서 브라우저가 어떤 구조로 이루어져 있는지에 대한 이해가 필요합니다.

모던 웹브라우저 구조← 글 중간에 나오는 링크들은 꼭 한번씩은 보시길 권합니다.

위 글을 짧게 요약하자면 최신 크로미움 브라우저는 각각의 독립된 메모리를 할당받은 여러 프로세스들을 가지는 멀티 프로세스 형태로 구현되어있습니다.

각 프로세스에는 여러 스레드가 존재하고 각자 독립적인 역할의 일들을 수행합니다.

프로세스들은 서로 통신이 필요할때InterProcessCommunication (IPC)를 통해 통신합니다.

그림 6

브라우저는 각 역할에 따라 많은 프로세스들을 가지고 있습니다.

Browser 렌더링 파이프라인 이미지 2

우리가 브라우저 사용할때 쓰는 탭 하나가 랜더러 프로세스 하나이고 이러한 여러 프로세스를 가지는 구조를 가지고 있기 때문에

하나의 탭이 문제가 생겨도 다른 탭에 영향이 가지 않게 되는 장점이 있습니다.

Browser 렌더링 파이프라인 이미지 3

렌더러 프로세스는 브라우저 탭 안에서 일어나는 모든 일들을 담당합니다. 각 스레드의 역할을 살펴보면

Main thread → 우리가 구현한 대부분의 코드를 처리합니다.

worker thread → 웹 워커 혹은 서비스 워커를 사용한 경우 워커 스레드 자바스크립트 코드 일부분을 처리합니다.

compositor, raster thread → 렌더러 프로세스 내부에서 페이지를 효율적이며 매끄럽게 렌더하기 위해 존재합니다.

렌더러 프로세스는 위 역할을 가지는 스레드들을 통해 HTML, CSS 그리고 자바스크립트를 사용하여 사용자가 인터렉션 가능한 웹페이지를 만듭니다.

Browser 렌더링 파이프라인 이미지 4

각 스레드가 실제 어떤 일들을 어떻게 처리하는지는 뒤에서 자세히 설명하도록 하겠습니다.

파이프라인 분석

우리의 일반적인 브라우저 사용은 보통 검색창에 URL 입력에서 부터 시작됩니다. (검색창에 입력하면 무슨일이 일어날까?)

URL을 입력하면 브라우저 내부의 여러 동작과 네트워크를 거쳐 랜더러 프로세스가 화면에 표시하는데 필요한 HTML, CSS, Javascript가 다운로드 되고 렌더러 프로세스의 메인스레드로 전달됩니다.

메인스레드는 전달받은 파일들을 기반으로 매 Vsync 신호마다 정해진 프로세스를 진행합니다.

과정 전체를 그림으로 보면 다음과 같습니다.

화면에 픽셀을 가져오는 프로세스입니다.

vsync and input data (frame start)를 시작으로 여러 단계의 파이프라인을 거쳐 프레임이 종료 될때 commit을 통해 gpu process 에 전달되는 과정을 나타 냈는데

vsync 및 input event handlers, requestAnimationFrame 등에 대한 설명은 뒤에서 자세하게 드리고 일단 MainThread에서 진행되는 렌더링과정부터 살펴보도록 하겠습니다.

모든 영광의 메인 스레드.

웹킷 렌더링 엔진 동작 과정

Main Thread 과정에 대한 디테일

Parse HTML (DOM 구축)

파싱은 서버로부터 전송받은 문서의 문자열을 브라우저가 이해할 수 있는 구조로 변환하는 과정을 파싱이라고 합니다.

파싱 결과는 문서 구조를 나타내는 노드 트리인데, 파싱 트리 또는 문법 트리라고 합니다.

파싱의 결과로 만들어진 DOM을 통해 추후 노드들을 쉽게 관리 할 수 있게됩니다.

브라우저는 이후 모든 페이지 처리에 DOM을 사용합니다.

DOM 파싱 요약

HTML 파싱 과정을 좀 깊게 살펴보면

DOM Parsing

변환: HTML의 원시 바이트를 읽어와 해당 파일에 지정된 인코딩(UTF-8 등…)에 따라 문자열로 변환하는 과정입니다.

토큰화: 문자열을W3C HTML5 표준에 따라 고유 토큰(,등, 꺽쇠괄호로 묶인 문자열)으로 변환합니다. 각 토큰은 특별한 의미와 고유한 규칙을 가집니다.

렉싱: 토큰을 해당 속성 및 규칙을 정의한 객체로 변환합니다.

DOM 생성: HTML은 상위-하위 관계로 정의할 수 있어, 트리 구조로 나타낼 수 있습니다. 렉싱 과정을 거쳐 생성된 노드들을 트리 구조로 변환합니다.

devtools 를 통해 HTML Parse 과정에 대한 내용이 표시됩니다.

Browser 렌더링 파이프라인 이미지 10

Parse CSS (CSSOM 구축)

DOM을 생성하는 과정 그대로 CSSOM을 생성합니다. (이 과정은 Devtools 에 표시되지 않습니다 )

브라우저는 DOM을 생성하는 동안 외부 CSS를 참조하는태그를 만나게 되면 브라우저에 리소스를 요청합니다. (외부 리소스 가져오는동안 지연이 발생함)

CSS 파싱 요약

CSS의 원시 바이트가 문자열로 변환된 후 차례로 토큰과 노드로 변환되고 마지막으로 CSSOM(CSS Object Model)이라는 트리 구조를 만듭니다.

CSSOM 생성

CSSOM Tree

CSSOM이 트리 구조를 가지는 이유는 페이지에 있는 객체의 최종 스타일을 계산할 때 브라우저는 해당 노드에 적용 가능한 가장 일반적인 규칙(예: body 요소의 하위인 경우 모든 body 스타일 적용)으로 시작한 후 더욱 구체적인 규칙을 적용하는 방식으로, 즉 '하향식'으로 규칙을 적용하는 방식으로 계산된 스타일을 재귀적으로 세분화합니다.

JavaScript

자바스크립트는 파서 차단 리소스입니다. 브라우저는 문서를 파싱 하다가 자바스크립트를 만나면 진행하던 파싱을 중지하고

자바스크립트 엔진에게 권한을 넘겨 자바스크립트를 파싱하고 실행합니다. ( why?document.write() 같은 api로 Dom 구조를 바꿀수 있기 때문 )

예를들어 다음과 같은 html 코드가 있습니다.

Browser 렌더링 파이프라인 이미지 14

Browser 렌더링 파이프라인 이미지 15

위 예시에서DOMContentLoaded핸들러는 문서가 로드되었을 때 실행됩니다.

따라서 핸들러 아래쪽에 위치한뿐만 아니라 모든 요소에 접근할 수 있습니다.

그렇지만 이미지가 로드되는 것은 기다리지 않기 때문에alert창엔 이미지 사이즈가 0이라고 뜹니다.

load– HTML로 DOM 트리를 만드는 게 완성되었을 뿐만 아니라 이미지, 스타일시트 같은 외부 자원도 모두 불러오는 것이 끝났을 때 발생합니다.

load이벤트는onload프로퍼티를 통해서도 사용할 수 있습니다.

위 예시에서window.onload는 이미지가 모두 로드되고 난 후 실행되기 때문에 이미지 사이즈가 제대로 출력되는 것을 확인할 수 있습니다.

Attachment

CSSOM 트리와 DOM 트리를 결합하여, 표시해야 할 순서로 내용을 그려낼 수 있도록 하기 위해 렌더 트리를 형성합니다.

이 과정을 웹킷에서는 Attachment라고 합니다.

렌더 트리는 화면에 표시되는 각 노드의 위치를 계산하는 레이아웃에 사용되고 픽셀을 화면에 그리는 페인트 과정에도 사용됩니다.

렌더 트리 구축

Attachment 요약

브라우저가 DOM 및 CSSOM을 렌더 트리에 결합합니다.

렌더 트리는 페이지에 표시되는 모든 DOM 콘텐츠와 각 노드에 대한 모든 CSSOM 스타일 정보를 가집니다.

Render Tree 형성

렌더 트리를 생성하려면 브라우저는 대략 3가지 작업을 수행합니다.

DOM 트리의 루트에서 시작하여 화면에 표시되는 노드 각각을 탐색합니다.

화면에 표시되지 않는 일부 노드들(script,meta태그 등..)은 렌더 트리에 반영되지 않습니다.

CSS에 의해 화면에서 숨겨지는 노드들은 렌더 트리에 반영되지 않습니다. 위의 예시에서span노드의 경우display:none이 설정되기 때문에 렌더 트리에 반영되지 않습니다.

화면에 표시되는 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용합니다.

화면에 표시되는 노드를 콘텐츠 및 계산된 스타일과 함께 내보냅니다.

DOM 트리와 렌더 트리의 관계

화면에 표시되지 않는 노드들은 렌더 트리에 포함되지 않습니다.

예를 들어,태그와 같은 비시각적 DOM 노드는 렌더 트리에 추가되지 않습니다.

뿐만 아니라 CSS로 인해display속성에none값이 할당된 노드들을 렌더 트리에 추가되지 않습니다.

하지만,visibility:hidden은 렌더 트리에 포함됩니다.(visibility속성에hidden값이 할당된 노드는 화면에 공간을 차지하기 때문에 렌더 트리에 포함됩니다.)

CSS 처리에 시간이 얼마나 걸리는지 알기 위해, DevTools에서 타임라인을 기록하고 'Recalculate Style' 이벤트를 찾을 수 있습니다.

DOM 파싱과 달리, 타임라인에 'Parse CSS' 항목이 별도로 표시되지 않으며,

대신 파싱 및 CSSOM 트리 생성과 계산된 스타일의 재귀적 계산이 이 단일 이벤트에서 캡처됩니다.

Browser 렌더링 파이프라인 이미지 20

Layout ( reflow )

Layout 요약

렌더 트리가 생성되고, 기기의 뷰포트 내에서 렌더 트리의 노드가 정확한 위치와 크기를 계산하는 과정을 Layout(혹은 Reflow)라고 합니다.

노드의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하면서 계산하고, 레이아웃 결과로 각 노드의 정확한 위치와 크기를 픽셀값으로 렌더트리에 반영합니다.

모든 상대적인 측정값은 화면에서 절대적인 픽셀로 변환됩니다. 즉 CSS에 상대적인 값인 %로 할당된 값들은 절대적인 값은 px 단위로 변환 됩니다.

코드

Browser 렌더링 파이프라인 이미지 22

레이아웃 전

Browser 렌더링 파이프라인 이미지 23

레이아웃 후

05-layout-after

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

Browser 렌더링 파이프라인 이미지 25

Update Layer Tree

Render Tree를 기반으로 렌더링에 사용될 최종 Layer들을 계산 해서 생성하는 과정입니다.

이렇게 여러 레이어로 나누는 이유는 기존에 화면을 그리는 방식의 단점을 보완하기 위해서 입니다.

기존의 방식은 화면에 보이는 부분을 그리는 방식이었습니다.

만약 사용자가 페이지를 스크롤하면, 프레임을 움직이고 부족한 부분을 메꿉니다.

( 이 과정에서 매번 스크롤이 생기거나 특정 레이어가 움직일때 새로 viewport 부분을 다시 그려야하는데 많은 비용이 소요됩니다. )

하지만, 모던 브라우저는 컴포지팅이라는 더 세련된 방식으로 동작합니다.

그림 14

모던 브라우저의 컴포지팅 방식

그림

컴포지팅은 한 페이지의 부분들을 여러 레이어로 나누고 그 것들을 각각 레스터하며 컴포지터 스레드에서 페이지를 합성하는 기술입니다.

만약 스크롤이 발생하면, 레이어들이 이미 레스터되었기 때문에, 해야 할 것은 새로운 프레임을 합성하는 것입니다.

에니메이션은 레이어들을 움직이는 동일한 방식으로 이뤄지고 새로운 프레임을 합성합니다.

그림 15

각 레이어 생성 조건은 아래와 같습니다.

레이어 관리 링크

Browser 렌더링 파이프라인 이미지 29

Browser internal layers 라는 부분이 있는데 브라우저가 내부적으로 관리하기 위해 생성하는 레이어들입니다.

예를들어 같은 z 인덱스를 가지는 두 레이어가 겹치는 경우 레이어를 자동으로 나누거나 스크롤바도 하나의 레이어로 나눠집니다.

웹 사이트가 여러 레이어로 나뉘는 지 개발자 도구의Layers panel에서 볼 수 있습니다.

Browser 렌더링 파이프라인 이미지 30

Paint

메인 스레드는 페인트 기록을 생성합니다.

이 단계에서 입력은레이아웃 트리와레이어 트리이고 출력은페인트 레코드입니다.

Browser 렌더링 파이프라인 이미지 31

레코드는 3가지로 구성됩니다.

동작

좌표(x, y) 및 크기(너비, 높이)를 포함한 위치입니다.

스타일

예를 들어 레코드는 다음과 같이 구성됩니다.

Action: Draw Rect

Pos: 0, 0, 300, 300

backgroundColor: red

단계의 이름은 Paint 이지만 실제 화면에 그린것은 아니고 레코드만 생성하고 생성된 레코드들을compositor thread 로 넘겨

추가적인 단계를 거쳐 화면에 나타납니다.

전체적인 단계를 보면 다음과 같습니다.

브라우저 렌더링 과정

Tilling

레이어를 나누는것 만으로는 하나의 레이어가 화면 전체에 해당될 정도로 클 수 있기 때문에 효율 적이지 않습니다.

페인트 레코드(레이어) 기반으로 타일들을 만들어 내는 과정입니다.

타일링할 때 레이어가 타일로 분리되고 브라우저는 뷰포트 위치를 기반으로 렌더링 우선 순위를 지정합니다.

Browser 렌더링 파이프라인 이미지 33

모든 타일을 렌더링하려면 비용이 많이 듭니다.

현재 뷰포트에 있는 보이는 타일이 우선 순위를 갖습니다.

우선순위가 높은 타일들이 먼저 레스터화 됩니다.

Browser 렌더링 파이프라인 이미지 34

Raster (Skia)

타일링이 끝나고 타일이 들어오면compositor thread는 래스터 스레드 풀을 생성합니다.

여러 래스터 스레드가 동시에 여러 타일들을 처리합니다.

프로세스를 가속화하기 위해 래스터 스레드는 IPC를 통해 브라우저의 GPU 프로세스에 타일을 보냅니다.

그런 다음 GPU 프로세스는 타일에서 비트맵을 생성하고 GPU 메모리에 비트맵을 저장합니다.

비트맵이 준비되면 GPU 프로세스는 다음 단계를 위해 렌더링 프로세스에서 비트맵을compositor thread로 다시 전달합니다.

Browser 렌더링 파이프라인 이미지 35

Draw Quad and Display

타일들이 레스터되서 비트맵들이 생성되면 컴포지터 스레드는쿼드 군집(draw quads)라 하는 타일 정보를 모아컴포지터 프레임을 생성합니다.

draw quads메모리에서 타일의 위치 및 페이지 합성을 고려하여 타일을 그릴 페이지의 위치와 같은 정보를 포함합니다.

compositor frame한 페이지의 프레임을 나타내는 쿼드 군집의 컬렉션입니다.

Browser 렌더링 파이프라인 이미지 36

필요한 모든 타일이 처리되면 컴포지터 스레드는 Draw Quad라는 명령을 브라우저 프로세스에 보냅니다.

브라우저 프로세스 내부에서 Draw Quad 명령을 수신하고 Display Compositor 명령을 실행하고 브라우저 프로세스가

컴퓨터 메모리 ( gpu 메모리 ) 에 그립니다.

Browser 렌더링 파이프라인 이미지 37

전체 그림을 보면 이렇습니다.

Browser 렌더링 파이프라인 이미지 38

Html Document 가 최초 로드되면서 어떠한 과정을 거처 화면에 표시되는지 알아봤습니다.

하지만 렌더가 한번 이루어진 다음에 유저의 이벤트나 애니메이션에 의해 렌더링 파이프라인 업데이트가 필요한 경우가 있습니다.

렌더링 파이프라인 업데이트는 비용이 많이 드는 작업으로 파이프라인 각 단계가 이전 단계의 결과물을 사용하여 진행된다는 것을 알 수 있습니다.

예를 들어, 만약 레이아웃 트리에서 무엇인가 변한다면 문서에서 영향 받은 부분에 대하여 페인트하는 순서가 갱신될 필요가 있습니다.

그림 10

상호작용 단계(유저 이벤트 등)에서는 일반적으로 JavaScript를 실행하여 페이지 업데이트를 트리거합니다.

이러한 경우 스타일이나 레아이웃 사이즈 변경등이 발생하여대부분의 경우 리플로우 및 다시 그리기를 유발할 수 있습니다.

Browser 렌더링 파이프라인 이미지 40

Reflow & Repaint

JavaScript 줄이 요소 높이를 변경하면 어떻게 될까요?

높이 수정은 DOM 트리에 영향을 주지 않습니다.대신 스타일 계산이 필요합니다.

스타일 계산이 끝나면 렌더러 프로세스는 레이아웃 단계에 대한 높이 변경을 반영하여 요소의 지오메트리 정보를 변환합니다.

따라서 레이아웃 트리를 생성해야 합니다.

레이아웃 트리는 나머지 단계의 종속성이므로 렌더러 프로세스는 모든 단계를 거쳐야 합니다.

이 프로세스를리플로우라고합니다.

많은 속성 검사는 "element.offsetLeft"와 같은 JavaScript에서 호출될 때 리플로우를 트리거할 수 있습니다.(따라서 반복문 같은데서 속성검사를 사용하면 계속해서 리플로우가 트리거 될 수 있으니 주의해야합니다. )

다음은 리플로우를 생성하는전체 목록입니다.

리플로우의 최악의 경우는 DOM을 수정하는 것입니다.

예는 "document.body.appendChild(노드)"입니다.리플로우 프로세스는 DOM 트리를 구축하는 첫 번째 단계에서 시작됩니다.

Browser 렌더링 파이프라인 이미지 41

요소의 배경색을 변경하면 어떻게 될까요?

다시 스타일 계산부터 시작합니다.

새 배경색은 요소의 지오메트리 정보를 수정하지 않으므로 렌더러 프로세스는 이를 건너뜁니다.

새 레이어도 생성하지 않으므로 레이어 단계를 건너뛰도록 합시다.

페인트 단계에서 렌더러 프로세스는 배경 색상 업데이트를 반영하기 위해 새 페인트 레코드를 생성해야 합니다.

그런 다음 나머지 단계를 거칩니다.

이 프로세스를다시 그리기라고합니다.

Browser 렌더링 파이프라인 이미지 42

리플로우와 리페인트 모두 다음 두 가지 이유로 렌더링 성능을 저하시킵니다.

reflow 및 repaint 프로세스는 메인 스레드에서 발생하므로 사용자 상호 작용에 의해 트리거되는 이벤트를 처리할 수 없습니다.그런 일이 발생하면 사용자는 버벅임을 느낍니다.

레이아웃, 레이어 및 페인트 단계의 계산 프로세스는 비용이 많이 듭니다.

다시 그리기는 레이아웃과 레이어 단계를 건너뛰기 때문에 리플로우보다 상대적으로 비용이 덜 듭니다.

reflow 와 repaint를 유발하지 않으면서 변경을 하는 방법은 css 애니메이션을 활용하는 것 입니다.

Browser 렌더링 파이프라인 이미지 43

일반적인 CSS 애니메이션은 "transform" 속성을 사용합니다.

"transform" 값을 수정하면 레이아웃, 레이어 및 페인트 단계를 건너뛰고compositor thread에서 tilling으로 시작됩니다.

메인 스레드를 점유하지 않고 CSS 애니메이션은 사용자의 상호 작용을 차단하지 않습니다.

메인 스레드의 프레임 당 일처리

모든 영광의 메인 스레드.

오늘날 대부분의 모니터 재생 빈도는 60FPS 또는 60Hz입니다. ( 즉 화면 갱신 프레임 주기가 16.6 ms가 됩니다 )

이상적인 애니메이션을 보여주려면 브라우저가 javascript 처리와 렌더링 단계를 완료하고 1/60초(16.6ms) 이내에 프레임을 전달해야 합니다.

즉 위 과정 전부를 모니터 재생 빈도가 60Hz일때 프레임당 16.6ms 안에 모두 수행해야만 부드러운 애니메이션을 보장 할 수 있습니다.

Browser 렌더링 파이프라인 이미지 45

만약 메인스레드에서 한 프레임 16.6ms 안에 javascript 처리와 렌더링 파이프라인 처리를 끝내지 못하면 프레임 드랍이 일어나고 유저는 불편함을 느끼게 됩니다.

Browser 렌더링 파이프라인 이미지 46

그런데 어떻게 브라우저는 매번 진행되는 프레임마다 위 그림과 같은 과정을 반복할 수 있을까요?

그리고 모니터 재생 빈도와 브라우저의 프레임은 어떤 관계가 있을까요?

이 부분을 알기 위해서는 모니터가 화면을 그리는 동작에 대한 이해와 위에서 언급했던 vSync (수직동기화) 라는 개념에 대해 알 필요가 있습니다.

VerticalSynchronization (V-sync)

VSync와 브라우저

vsync 를 알기 위해서는 모니터가 어떻게 화면을 업데이트 하는지 알아야 합니다.

Browser 렌더링 파이프라인 이미지 47

여기서 Applications는 Browser라고 할 수 있고 브라우저가 화면을 완성하면 그래픽 드라이버에 요청하여

그래픽 드라이버 내부의 Back buffer에 화면 정보를 업데이트 한 다음 스왑 이벤트를 발생 시키고

모니터는 그래픽 드라이버의 front Buffer의 정보로 화면을 갱신합니다.

두개의 버퍼를 사용하는것을 더블 버퍼링이라고 합니다.

모니터가 FrontBuffer 정보를 출력하는 동안(Monitor refresh)에 Swap 이 발생하면 Front Tearing 현상이 나타납니다.

Browser 렌더링 파이프라인 이미지 48

Browser 렌더링 파이프라인 이미지 49

vsync를 사용하지 않는다는 것은 Monitor refresh 를 신경쓰지 않고 버퍼스왑을 한다는 것이고 Tearing이 발생할 수 있다는 뜻입니다.

그러면 브라우저는 이 vsync라는 것을 어떻게 활용하여 frame을 관리할까요?

일단 브라우저는 자신의 os에게 vsync 타이밍을 알아내고 활용합니다.

매끄러운 애니메이션을 보여주려면 프레임 드랍이 없으면서 애니메이션 구성의 기준이 되는 각 프레임의

시간차가 작고 일정해야만 합니다.

Browser 렌더링 파이프라인 이미지 50

두번째 케이스 같이 프레임이 들쭉날쭉 하게 실행되면 화면 갱신에 맞춰서 일정하고 부드러운 애니메이션을 보여 줄 수 없습니다.

Browser 렌더링 파이프라인 이미지 51

Browser 렌더링 파이프라인 이미지 52

vSync 신호가 오면 브라우저는 프레임을 시작하는데 해당 프레임이 시작하는 타이밍을 정확히 알아야 최대한의 시간을 벌어서

애니메이션에 필요한 연산등을 수생할 수 있음.

브라우저에서는 두개의 콜백을 제공함

window.requestAnimationFrame() ← drawCallback ( frame 시작 )

Browser 렌더링 파이프라인 이미지 53

request animation frame

window.requestIdleCallback() ← idleCallback (main thread 유휴 시간)

Browser 렌더링 파이프라인 이미지 55

글 앞에서 소개했던 이미지를 다시 보겠습니다.

화면에 픽셀을 가져오는 프로세스입니다.

vsync 신호부터 프레임이 시작해서 rAF 이후 렌더링 과정을 거쳐서 MainThread가 쉬는 타이밍에 requestIdleCallback 이 호출되고

프레임이 끝나는 과정에 대해서 거의 다 알아본것 같습니다.

마지막으로 유저의 입력 이벤트가 프레임 앞쪽에서 어떻게 처리되고 진행되는지 보겠습니다.

'입력 이벤트(input event)'라는 말을 들었을 때 입력란에서 일어나는 값 입력이나 마우스 클릭만 생각할 수 있습니다.

하지만 브라우저의 관점에서 입력이란 모든 사용자의 제스처를 의미합니다.

마우스 휠을 스크롤하는 것도 입력 이벤트이고, 화면을 터치하거나 마우스 포인터를 화면 위에 올리는 것도 입력 이벤트입니다.

유저의 모든 이벤트는 운영체제에서 브라우저 프로세스로 전달됩니다.

브라우저 프로세스는 제스처가 어디에서 발생했는지만 알고 있습니다.

탭 내부의 콘텐츠는 렌더러 프로세스가 처리해야만 합니다.

그래서 브라우저 프로세스는 이벤트 유형(예:touchstart)과 이벤트가 발생한 좌표를 렌더러 프로세스로 보냅니다.

렌더러 프로세스는 이벤트 대상을 찾고 해당 대상과 연결된 이벤트 리스너를 실행해 이벤트를 적절하게 처리합니다.

그림 1

위에서래스터화된 레이어를 컴포지터가 합성(composite)해 스크롤을 부드럽게 처리하는 방법을 살펴봤습니다.

웹 페이지에 이벤트 리스너가 연결되어 있지 않으면 해당 이벤트에 대한 처리를 할 필요가 없기 때문에

컴포지터 스레드는 메인 스레드와 상관없이 새로운 합성 프레임을 만들 수 있습니다.

하지만 이벤트 리스너가 웹 페이지에 연결되어 있다면 어떻게 될까요?

또 이벤트를 처리해야 하는지 컴포지터 스레드가 어떻게 알 수 있을까요?

그림 2

이벤트에 대한 JavaScript 실행은 메인 스레드의 작업입니다.

웹 페이지가 합성될 때 컴포지터 스레드는 이벤트 핸들러가 연결된 영역을 '고속 스크롤 불가 영역(non-fast scrollable region)'이라고 표시합니다.

웹 페이지의 이 영역에서 이벤트가 발생했을 때 컴포지터 스레드가 입력 이벤트를 메인 스레드로 보내야 하는지를 이 정보로 확인할 수 있습니다.

입력 이벤트가 고속 스크롤 불가 영역 밖에서 발생했다면 컴포지터 스레드는 메인 스레드를 기다리지 않고 새 프레임을 합성한다.

그림 3

위와 같은 동작 방식 때문에 document 와 같은 커다란 레이어에 이벤트 리스너를 등록할때는 주의를 해야합니다.

document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault(); } });

코드처럼 document 에 터치이벤트를 등록했을때브라우저의 관점에서 이 코드를 보면 이제 웹 페이지의 모든 영역이 고속 스크롤 불가 영역으로 표시

됩니다. 터치이벤트를 처리 할 필요가 없는 부분에입력이 들어와도 컴포지터 스레드는 입력 이벤트가 들어올 때마다 메인 스레드와 통신해야 하고 메

인 스레드가 일을 끝내기를 기다려야 합니다.

그래서 컴포지터가 스크롤을 부드럽게 할 수 없게 됩니다.

그림 4

이런 문제를 방지하기 위해 이벤트 리스너에서passive: true옵션을 전달할 수 있습니다.

이 옵션은, 여전히 메인 스레드에서 이벤트를 받지만 컴포지터가 메인 스레드의 처리를 기다리지 않고 새 프레임을 만들어도 된다는 힌트를 브라우저

에 주는 옵션입니다.

document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true});

컴포지터 스레드가 입력 이벤트를 메인 스레드로 보낼 때 가장 먼저 하는 일은 이벤트 대상을 찾는 히트 테스트(hit test)입니다.

이벤트가 발생한 좌표에 무엇이 있는지 확인하기 위해 히트 테스트는 렌더링 프로세스에서 생성된 페인트 기록의 데이터를 사용합니다.

그림 6

위에서 일반적인 디스플레이 장치가 화면을 초당 60번 새로 갱신한다는 것과 애니메이션을 화면 갱신 주기(v-sync)에 맞춰야 부드럽게 움직인다는 것

을 이야기했습니다.

입력 이벤트의 경우에는 일반적인 터치스크린 장치는 터치 이벤트를 초당 60~120회 전달한됩니다.

마우스는 이벤트를 초당 100회 전달합니다.즉 입력 이벤트의 전달 주기가 화면 갱신 주기보다 짧습니다.

touchmove이벤트처럼 연속적인 이벤트가 초당 120회씩 메인 스레드로 보내지면 화면이 갱신되는 정도보다 훨씬 많이 히트 테스트를 하거나

JavaScript를 실행할 수도 있습니다.

그림 7

따라서 브라우저는 메인 스레드의 과도한 호출을 막기 위해 연속적인 이벤트(예wheel,mousewheel,mousemove,pointermove,touchmove)를

합쳐서 바로 다음번requestAnimationFrame()메서드 실행 직전까지 전송하지 않고 기다립니다.

하지만 keydown,keyup,mouseup,mousedown,touchstart,touchend와 같은 비연속적인 이벤트는 즉시 전달됩니다.

그림 8

대부분의 웹 애플리케이션에서는 이벤트를 합쳐서 처리해도 사용자가 만족스럽게 사용할 수 있습니다.

하지만 드로잉 앱 같은 것에서touchmove이벤트의 좌표를 기반으로 경로를 만들어야 할 때에는 사이사이에 경로가 누락돼서 선을 매끄럽게 그리지

못할 수 있습니다.

이런 경우에 포인터 이벤트의getCoalescedEvents()메서드를 사용하면 합쳐진 이벤트에 대한 정보를 얻을 수 있습니다.

그림 9

window.addEventListener('pointermove', event => { const events = event.getCoalescedEvents(); for (let event of events) { const x = event.pageX; const y = event.pageY; // draw a line using x and y coordinates. } });

이벤트 처리에 대한 부분을 다이어그램으로 표현하면 아래와 같습니다.

Browser 렌더링 파이프라인 이미지 65

정리하면 vSync 타이밍이 시작되면 Browser Process로 부터 전달된 이벤트가 Renderer Process의 Composite Thread로 전달되고 이벤트 리스너가 등록된 경우 메인스레드가 해당 이벤트에대한 Javascript 처리를 한 후 Main Thread 프레임이 시작되면서 스크롤을 처리한후 requestAnimationFrame() 콜백을 호출하고 랜더링 과정을 거쳐서 화면에 표시됩니다.

여기까지 해서 현재 브라우저들이 어떻게 렌더링과 이벤트 처리하는지 알아 봤습니다.

2021년 11월 기준으로 크로미움 공식 블로그에RenderingNG라는 새로운 렌더링 아키텍처가 공개되었습니다.

이부분에 대해서도 자료들이 많이 더 나오면 추후에 알아보도록 하겠습니다.

참조

브라우저는 vSync를 어떻게 활용하고 있을까 ->https://www.slideshare.net/deview/133-vsync

웹 성능 최적화에 필요한 브라우저의 모든것 ->https://www.slideshare.net/deview/125-119068291

브라우저 동작 원리와 vSync ->https://coffeeandcakeandnewjeong.tistory.com/55

브라우저 렌더링 ->https://ibocon.tistory.com/251?category=879638

How does browser work step by step ->https://cabulous.medium.com/how-does-browser-work-in-2019-part-iii-rendering-phase-i-850c8935958f

browser Load (MDN) ->https://developer.mozilla.org/ko/docs/Web/Performance/How_browsers_work

d2 최신 브라우저 동작 ->https://d2.naver.com/helloworld/6204533

paint 부하 줄이기 ->https://developers.google.com/web/fundamentals/performance/rendering/simplify-paint-complexity-and-reduce-paint-areas?hl=ko

toast Ui 성능 최적화 ->https://ui.toast.com/fe-guide/ko_PERFORMANCE