[CS] Browser rendering pipeline

Pakxe·2024년 5월 4일
33

CS

목록 보기
1/1
post-thumbnail

마지막 업데이트: 240503

서론

보통 브라우저 렌더링 과정은 DOM -> CSSOM -> render 정도로 이해되곤 합니다. 최근에 virtual DOM의 최적화 방식을 접하면서 그럼 '모던 브라우저들은 최적화에 아예 손을 놓고있나' 생각이 들었습니다. 다만 최적화에 대해 공부하려면 원래 어떻게 돌아가는지부터 알아야하기 때문에 이렇게 공부하게 되었습니다.

너무 깊은 내용은 추상화해서 작성하였습니다.

브라우저의 프로세스

브라우저를 켜게 되면 여러 프로세스가 함께 실행됩니다. 싱글 스레드인 js와는 다르게 말이죠.

실행되는 여러 프로세스는 다음과 같습니다.


(크롬 작업 관리자)

(viz process = visualization(GPU) process)
위 작업들을 보면 렌더러 프로세스는 탭마다 할당되어있고, GPU와 브라우저 프로세스는 하나만 존재합니다. 렌더러 프로세스가 각자 할당되어있는 이유는 하나의 탭에 문제가 발생해도 다른 탭에 이 문제를 전파시키지 않기 위해서이며, 보안을 위해 서로 다른 메모리 공간을 사용하도록 하기 위해서입니다.

renderer process

renderer process에는 2개의 스레드가 존재합니다. main 스레드와 compositor 스레드입니다.

  • main thread: 화면을 그리기 위한 명령어 생성
  • compositor thread: viz process에 명령어를 전달

두 개의 스레드로 나뉘는 이유는 시간 줄이기(최적화)와 병렬 처리를 위함입니다.

main thread는 구체적으로 아래와 같은 작업을 합니다.

  • js 실행
  • style
  • layout
  • paint

compositor thread는 구체적으로 아래와 같은 작업을 합니다.

  • layer 관리
  • scroll 처리
  • layer 합성: 레이어들을 GPU에서 만들고 합성해 최종 이미지 생성

이렇게 compositor thread로 분리함으로써 병렬 작업이 가능하게 되었고, 스크롤과 같은 작업을 분리할 수 있게 되었습니다. 이렇게 main thread에 부하를 줄이고 더 부드러운 반응을 제공합니다.

rendering pipeline

위에 나열한 다양한 프로세스와 스레드로 브라우저의 렌더링을 위해 아래와 같은 과정을 거치게 됩니다.

(input, output으로 연결된 추상화된 pipeline)

이제부터 pipeline의 각 phase에 대해서 살펴보겠습니다.
(너무 깊은 phase의 경우는 생략하였습니다. )

주요 phase

바로 글 부터 읽는 것 보단 전체적으로 추상화된 흐름을 이해하고 읽으면 더 좋을 것 같아 그림 자료를 첨부합니다.

시작은 서버에 http request를 날려 html을 받아오는 것 부터 시작합니다.








1. parse

브라우저의 주소창에 url을 적고 접속하면 브라우저는 서버에 HTML을 요청해 바이트로 된 데이터를 받아옵니다.

이 바이트를 어떤 방식으로 decoding 할 지는 HTTP response header에서 찾습니다.

위 문서에서는 utf-8로 해석해야한다고 적혀있네요. 대부분의 문서는 utf-8로 해석합니다.

이렇게 바이트를 해석하게되면 우리가 잘 아는 html 파일의 모습이 됩니다.

텍스트에 불과한 html를 파싱해 의미있는 구조로 만들어야 합니다. 그래야 개발자들이 js로 모달을 띄우거나 위치를 바꾸는 등의 작업을 할 수 있게 됩니다. 이런 의미있는 구조를 DOM 이라고 합니다.

html(아직은 plain text)를 한 글자씩 읽으면서 token을 만듭니다. 이를 tokenization이라고 합니다.

tokenization의 과정은 state machine으로 표현됩니다. 간단하게 말하면 아래와 같습니다.

  • <text(ex body)가 연달아 있으면 open body tag 토큰을 만듭니다.
  • >/가 연달아 있으면 close {태그명} tag토큰을 만듭니다.
  • <div>hello world</div> 같은 경우 open div tag, text token, close div tag 토큰이 만들어지게 됩니다.

이런 식으로 html의 끝까지 읽어 text들을 토큰으로 만들게됩니다. 그리고 이렇게 만들어진 토큰들을 부모-자식 관계로 엮어 DOM tree를 만들게 됩니다. open html tag 다음에 open body tag가 온다면 html의 자식이 body가 되는 식입니다.

이렇게 모든 토큰을 처리하면 parse과정이 끝납니다.

만들어진 output은 DOM 입니다.

2. style

DOM 만으로는 완성된 페이지의 모습을 알 수 없습니다. 색이나 모양이 없기 때문입니다. 이런 스타일은 css로 적혀있으니 이런 정보들을 긁어모아 DOM과 연결해주는 작업이 필요합니다.

style 과정은 css파일, <style>태그의 스타일, html의 inline style을 읽는 것 부터 시작합니다. 이런 정보 역시 text에 불과하므로 위 parse과정에서 했던 것처럼 토큰으로 만들고 의미있게 묶는 작업이 필요합니다.

css는 선택자와 속성명, 속성값으로 구성됩니다. 따라서 이런 부분을 토큰으로 만들게 됩니다. 이렇게 만들어진 선택자 토큰과 속성 토큰을 묶어 하나의 노드로 변환합니다. 예를 들어 .box { height: 10px } 라면 .box 선택자에 대한 노드로 변환되는 것입니다.

이렇게 CSSOM이라는 자료구조가 만들어지게 됩니다. CSSOM을 이용해 DOM의 각 노드에 적용될 스타일을 결정하는데 이 스타일을 computed style이라고 합니다.

이렇게 DOM에 어떤 스타일을 적용해야될지 까지 구했습니다.

현재까지의 상태는 위 이미지와 같습니다. DOM에 computed style이 붙어있는 render tree가 완성되었습니다.

3. layout

다만 아직까지는 DOM의 각 노드들이 실제 브라우저에서 어디에 위치해야하는지는 알 수 없습니다. 자식이 부모의 top에서 10px아래에 있다면 이게 브라우저 상에서는 어떤 좌표에 있을지는 css만으로는 알 수 없는 것처럼 말입니다.

따라서 DOM의 각 노드들이 화면 상에서 어디에 위치하는지를 계산하는 과정이 필요합니다.

이전 style 단계에서 만들어진 render tree를 순회하면서 각 노드가 화면 상에서 어느 위치에 얼마의 크기로 위치하는지(x, y, height, width)를 정확하게 캡처해 자료 구조로 만드는데 이를 fragment tree라고 합니다.

4. prepaint

아래의 4가지 시각화와 스크롤링 속성들이 있습니다.

  • transform
  • clip
  • effect(blur, filter, opacity 등)
  • scroll
    ...

이 속성들은 이 자체만으로도 매우 복잡합니다. 따라서 property tree라는 구조에 담아 처리합니다. property tree는 이 복잡한 속성들을 rendering tree에서 분리해 보관하는 역할을 합니다.

이렇게 복잡한 연산이 렌더링 파이프라인에서 분리되어 계산될 수 있게 되었습니다.

5. paint

이제 DOM의 각 노드를 어떻게 그려져야할지를 명령해야합니다.


위 그림의 경우 파란 네모를 아래에 그리고, 초록 네모를 그리고, 위에 글씨를 써야 합니다.

이런 그리기 명령을 아래와 같은 display list로 표현하게 됩니다.

이때 파란 네모가 먼저 그려지면 초록 네모가 안보이게 됩니다. 이는 fragment tree를 순차적으로 순회하는 것이 아니라 stacking order(z-index 등을 고려한 렌더링 순서)를 따라 순회하며 display list(PaintRecord 라고도 합니다)를 생성합니다.

6. layerize

DOM의 각 노드가 어떤 레이어에 있어야하는지 정해야합니다.
아래처럼 개발자 도구의 layer 탭에서 볼 수 있는 layer를 말합니다.

아래 조건에 해당하는 경우 별개의 레이어를 갖습니다. (생략한 것도 많음)

  • position: fixed
  • video, iframe, canvas 태그
  • 애니메이션
    ...

아래는 framer-motion 사이트의 레이어인데, 애니메이션이 들어가있는 요소들은 다 별개의 레이어를 갖는 것을 볼 수 있습니다.

이렇게 계산되어 만들어진 레이어 목록을 composited layer list라고 합니다.

7. commit

이제 각 레이어안에 있는 요소들이 어떻게 그려져야하는지, 어디에 위치하는지, 얼마만큼의 크기를 갖는지 모두 알게 되었습니다. 드디어 그릴 수 있겠어요!

그리기 위해선 이제 GPU에 준비된 계획들을 밀어넣어야 합니다.

main thread에서 composited layer list, property tree가 compositor thread로 복사됩니다. 이를 commit이라고 합니다. 이렇게 main thread에서의 모든 작업이 끝났습니다. 따라서 다른 작업을 받아 수행할 수 있는 상태가 되었습니다. 병렬 처리가 가능하다는 뜻입니다.

compositor thread에는 다음과 같은 트리가 존재합니다.

  • pending tree: 작업중인 트리
  • active tree: 작업 완료된 트리

commit이 수행되면 composited layer list, property tree가 compositor thread안의 pending tree로 복사됩니다.

8. tiling

너무 긴 레이어(지금 이 글만 해도 엄청 길겠습니다)또는 화려한 애니메이션이 들어간 레이어 등의 경우는 그리는 데에 시간이 오래걸릴 수 있습니다. 따라서 이 하나의 레이어를 여러 타일로 만들어 작업합니다.

이렇게 쪼개개되면 큰 레이어에 변화가 생겼을 때 이 큰 레이어를 다시 그릴 필요 없이 일부만 수정하면 되고, viewport에 포함되지 않는 타일에 대해서는 그리지 않아도 되어 빠릅니다.


위 gif를 보면 가운데 viewport 상자의 근처만 그려진 화면인 것을 볼 수 있습니다. 타일링이 적용된 모습입니다. 현재 필요하지 않은 영역은 그려지지 않도록 최적화되어 있습니다.

이때 각 타일에는 어떻게 화면을 그려야하는지 저장했던 PaintRecord의 정보가 포함됩니다.

9. raster

이제 보여줘야하는 타일들(ex viewport 근처 등의 기준)을 래스터화 합니다. 비트맵 이미지를 생성한다는 뜻입니다. 이렇게 만들어진 비트맵 이미지는 GPU 메모리에 저장됩니다. 저장된다는 것은 지금 당장 안쓰이고 두었다가 나중에 사용할 수 있다는 겁니다.

이렇게 래스터화된 결과로 Quad라는 데이터가 나오게 됩니다. 쿼드는 담당하는 영역의 크기, 어떻게 그려질지(texture coordinates)에 대한 정보를 포함하며 이 정보는 layer와 property tree의 정보를 바탕으로 생성됩니다. 쿼드는 예를들면 주어진 영역에 단색을 칠하는 SolidColorDrawQuad와 GPU 메모리에 저장된 이미지를 가져와 보여주는 TextureDrawQuad등이 있습니다.

(단색 칠하는 SolidColorDrawQuad)

다양한 종류의 쿼드들

10. activate

현재 화면에 보이는 프레임은 active tree로 그려진 frame입니다. raster 단계를 거치며 다음 프레임이 완성되었으니 새롭게 업데이트 해야합니다. 이는 pending tree -> active tree로 수행됩니다. commit 단계에서 pending tree에 넣어둔 것을 active tree로 이전하는 것입니다. 이 순간을activate라고 합니다.

그리고 이전 단계에서 만들어진 쿼드들은 render pass(넘어가도 괜찮습니다)배열의 요소로 들어가고, 이런 render pass들이 모여 compositor frame으로 묶입니다. 쿼드들이 퍼즐이라면 이 쿼드들을 잘 짜맞춘 것이 compositor frame이 됩니다.

11. aggregation

만약 iframe 요소가 있어, a.com에서 b.com을 작은 모습을 띄우고 있다고 해봅시다. 각 페이지는 독립된 renderer process를 갖기 때문의 각자의 compositor frame을 생성하게 됩니다. 사용자는 둘을 하나의 합쳐진 페이지로 봐야하기 때문에 이 프레임들이 viz process에 전달됩니다. 그리고 이들은 합쳐져서 하나의 프레임이 됩니다.

이렇게 합쳐진 프레임을 aggregated compositor frame라고 합니다.

12. draw

이렇게 하나의 프레임으로 합쳐진 compositor frame을 화면에 출력하면 렌더링이 종료됩니다.

생각해보기

위 과정에서 layout, pre-paint 와 paint 는 스킵될 수 있습니다. 스킵되면 바로 compositor thread로 바로 진입할 수 있기 때문에 빠른 렌더링이 가능하게 됩니다.

또한 layout 단계는 스킵이면서 paint는 진행될 수 있습니다(ex 색이 달라지는 경우). 이 경우 시간이 조금 빨라집니다.

pre-paint 단계에서 걸리는 css인 transition, clip.. 같은 경우는 paint 단계를 스킵할 수 있습니다.

참고

https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/platform/graphics/paint/README.md#Paint-properties
https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/paint/#Building-paint-property-trees
https://developer.chrome.com/docs/chromium/renderingng-data-structures#compositor_frames_surfaces_render_surfaces_and_gpu_texture_tiles
https://docs.google.com/document/d/1aitSOucL0VHZa9Z2vbRJSyAIsAz24kX8LFByQ5xQnUg/edit
https://web.dev/articles/critical-rendering-path/render-tree-construction?hl=ko
https://dongmi.dev/chromium-rendering-pipeline/
https://bscnote.tistory.com/72
https://d2.naver.com/helloworld/5237120
https://ibocon.tistory.com/251

틀린 내용이 있을 경우, 이메일이나 댓글을 통해 알려주시면 감사하겠습니다. 여러분의 소중한 피드백을 기다리겠습니다!

profile
내가 꿈을 이루면 나는 또 누군가의 꿈이 된다.

4개의 댓글

comment-user-thumbnail
2024년 5월 17일

멋진글이네요 잘 읽고 갑니다!

1개의 답글
comment-user-thumbnail
2024년 5월 25일

커버 일러스트는 직접 그리신건가요? 엄청 이뻐요

1개의 답글