위 출처의 내용을 번역본 입니다.
자세한 내용은 원문을 참고해주세요.
이 글은 브라우저가 어떻게 동작하는지 살펴보는 4부작 시리즈의 3편입니다.
이전에 멀티프로세스 아키텍쳐 및 탐색흐름에 대해 다뤄보았습니다.
이번 글에선, renderer프로세스의 내부에선 어떤 일이 일어나는지 알아보도록 하겠습니다.
renderer프로세스는 웹 성능의 많은 면에 관여합니다.
renderer프로세스에서 많은 일이 발생하기 때문에 이 글에선 일반적인 개요만 다루겠습니다.
더 깊이있게 알아보고 싶다면, the Performance section of Web Fundamentals
이 글을 참고해보세요.
renderer프로세스는 tab에서 발생하는 모든 것을 처리합니다.
renderer프로세스의 내부에선,
main스레드가 당신이 유저에게 전달한 대부분의 코드를 처리합니다.
때로는 당신이 web worker나 service worker를 사용했다면, JavaScript의 일부는 worker스레드가 처리하기도 합니다.
Compositor와 raster스레드 또한 renderer프로세스 내부에서 페이지가 효과적이고 부드럽게 렌더링되도록 실행됩니다.
renderer프로세스의 핵심 작업은 유저가 상호작용할 수 있도록
HTML, CSS, JavaScript를 웹페이지로 전환하는 것입니다.
renderer프로세스가 탐색에 대한 커밋메시지를 받고 HTML데이터를 받기시작할 때,
main스레드는 문자열인 HTML을 파싱하기 시작하여 이것을 Document Object Model (DOM)으로 변환합니다.
DOM이란 페이지의 내부를 나타낸 것일 뿐만 아니라
웹 개발자가 JavaScript를 통해 상호작용할 수 있도록 해주는 데이터 구조이며 API라고 할 수 있습니다.
HTML문서를 DOM으로 파싱하는 것은 HTML Standard에 정의되어있습니다.
브라우저에 HTML을 추가해도 오류가 발생하지 않는다는 것을 이미 알고있을 지도 모릅니다.
예를 들어, closing이 누락된 </p>
태그는 유효한 HTML입니다.
Hi! <b>I'm <i>Chrome</b>!</i>
(i 태그보다 먼저 닫히는 b태그)와 같이 잘못된 마크업이더라도 Hi! <b>I'm <i>Chrome</i></b><i>!</i>
이렇게 올바르게 작성된 것으로 처리됩니다.
이는 HTML규격에 이러한 오류를 정상적으로 처리하도록 설계되어있기 때문입니다.
이러한 것들이 어떻게 수행되는지 궁근하다면 An introduction to error handling and strange cases in the parser이 글의 HTML spec섹션을 읽어보세요.
웹페이지는 일반적으로 images,CSS,JavaScript와 같은 외부 리소스를 사용합니다.
이런 파일들은 네트워크나 캐시를 통해 로드해야합니다.
main스레드는 파싱하여 DOM을 만드는 동안 그것들을 하나하나 찾아내고 요청할 수도 있지만,속도 향상을 위해 "preload scanner"라는 것이 동시에 실행됩니다.
<img>
혹은 <link>
와 같은 태그가 있다면,
preload scanner는 HTML파서가 만들어낸 토큰을 보고 browser프로세스 내부의 network스레드에 요청을 전달합니다.
HTML파서가 <script>
태그를 발견하면, HTML문서의 파싱을 중단하고 스크립트를 로드하고, 파싱하고 JavaScript코드를 실행해야합니다.
왜 그래야 할까요?
왜냐하면 JavaScript는 document.write()
와 같은 전체 DOM구조를 변경시킬 수 있는 것들을 통해 document의 모습을 변경시킬 수 있기 때문입니다.
이것이 HTML파서가 JavaScript가 동작하는 동안 대기해야하는 이유입니다.
JavaScript실행 시 어떤 일이 발생하는 지 궁금하다면,
the V8 team has talks and blog posts 을 참고하세요.
리소스를 훌륭하게 로드할 수 있도록 브라우저에게 힌트를 전달할 방법은 다양합니다.
만약 당신의 JavaScript가 document.write()
을 사용하지 않는다면,
async
혹은 defer
속성을 <script>
태그에 추가할 수 있습니다.
이렇게 하면, browser는 JavaScript코드를 비동기적으로 로드하고 실행시킵니다. 파싱을 중단시키지도 않으면서요!
또한, 적합하다면 JavaScript module을 사용할 수도 있습니다.
<link rel="preload">
는 브라우저에게 해당 리소스는 반드시 필요하며 가능한 빨리 다운로드하고싶다고 알려 줄 수 있는 방법입니다.
Resource Prioritization – Getting the Browser to Help You.이 글도 참고해보세요.
DOM을 갖는 것 만으로는 페이지가 어떻게 생겼는지 알기엔 충분하지 않습니다.
CSS에서 페이지의 요소를 스타일링 할 수 있기때문입니다.
main스레드는 CSS를 파싱하고 각각 DOM node에 대한 계산된 스타일을 결정짓습니다.
CSS셀렉터를 베이스로 각각의 요소에 어떤 스타일이 적용되는지에 대한 정보가 존재하며 이런 정보는 개발자도구의 computed
섹션에서 확인할 수 있습니다.
어떤 CSS를 제공하지 않더라도, 각각의 DOM은 저마다 계산된 스타일을 갖고 있습니다.
<h1>
태그는 <h2>
태그보다 크게 표시 되며 각 요소에 대해 여백이 정의됩니다.
브라우저는 기본적인 스타일시트를 갖고있기 때문입니다.
Chrome의 기본 CSS가 어떤 것인지 알고 싶다면 여기에서 소스 코드를 볼 수 있습니다 .
이제 renderer프로세스는 문서의 구조와 각 노드의 스타일을 알게 되었지만 페이지를 렌더링하기엔 충분하지 않습니다.
전화로 친구에게 그림을 설명하려 한다고 상상해보세요.
"커다란 빨간색 원과 작은 파란색 사각형이 있어."라고 친구에게 말하는건 정확히 어떤 모습의 그림인지 알기에 충분치 못한 정보입니다.
layout은 요소의 기하학적 구조를 알아내기위한 과정을 말합니다.
main스레드는 DOM과 계산된 스타일을 살펴보고 x,y좌표와 상자 크기와 같은 정보가 들어있는 layout트리를 만들어냅니다.
layout트리는 DOM트리의 구조와 유사할 수 있지만 페이지에서 보여지는 것과 연관된 정보만을 담고있습니다.
만약 display:none
이 적용된 경우, 해당 요소는 layout트리에 속하지 않습니다.
(그러나, visibility:hidden
이 적용된 요소는 layout트리에 속함.)
유사하게, 만약 p::before{content:"Hi!"}
와 같은 콘텐츠를 갖는 허위의 클래스가 적용되면 DOMM에는 없을지라도 layout트리에는 포함됩니다.
페이지의 layout을 결정짓는 것은 매우 고된 작업입니다.
심지어 block으로 위에서 아래로 흐르는 것같은 심플한 페이지 layout일지라도 글꼴의 크기와 줄바꿈 위치를 고려해야 합니다. 이러한 요소들이
paragraph의 크기와 모양에 영향을 미치기 때문입니다.
CSS는 요소를 한쪽으로 float하게 만들수도 있고,
overflow item을 mask할 수도 있으며 쓰기 방향을 변경할 수도 있습니다.
이러한 layout단계에서 굉장히 까다로운 작업들이 거친다고 상상되지 않나요?
Chrome에서는 전체 엔지니어 팀이 layout을 위해 일합니다.
더 디테일한 업무를 보고싶으면 few talks from BlinkOn Conference을 확인해보세요.
DOM과 스타일,layout을 가져도 페이지를 렌더링하기엔 아직도 충분하지 않습니다.
그림을 재현해보려 한다고 가정해봅시다.
요소의 크기와 모양 그리고 위치를 알고있지만 여전히 어떤 순서로 그것들을 그려야할지 판단해야 합니다.
예를 들어,
z-index
가 특정 요소에 설정되어 있는데, HTML에 작성된 순서로 요소를 그리는 것은 잘못된 렌더링 결과를 초래할 수 있습니다.
지금 Paint단계에서, main스레드는 layout트리를 둘러보며 paint레코드를 생성합니다.
paint레코드는 "background먼저 그리고 text다음 직사각형"과 같은 페인팅과정에 대한 note를 의미합니다.
만약 JavaScript를 이용해 canvas
요소에 그림을 그려봤다면, 이 프로세스가 익숙할 수 있습니다.
rendering파이프라인에서 이해해야할 가장 중요한 것은 각 단계에서 이전작업의 결과물이 새 데이터를 만들어내는 데 사용된다는 것입니다.
예를 들어, 만약 layout트리에서 무언가 변경이 생기면,문서에서 영향받는 부분들에 한해 Paint순서가 다시 만들어져야 합니다.
요소에 애니메이션을 적용하는 경우,
브라우저는 이러한 작업을 매 frame 사이사이 실행시켜야합니다.
대부분의 디스플레이는 1초에 60번(60fps) 화면을 새로고침합니다;
매 프레임마다 스크린을 가로지르는 것들을 움직여야 애니메이션이 사람의 눈에 부드럽게 보여지게 됩니다.
그러나, 애니메이션이 그 사이의 프레임을 놓치면 페이지가 "버벅거려"보이게 됩니다.
rendering작업이 화면새로고침을 따라가고 있더라도 이런 계산은 main스레드에서 실행되므로 어플리케이션이 JavaScript를 실행시키면 중단될 수도 있습니다.
당신은 JavaScript의 작업을 작은 덩어리로 나누고 requestAnimationFrame()
을 사용해서 매 프레임마다 실행되게 스케줄을 조정할 수도 있습니다.
이 주제에 대한 자세한 내용은 Optimize JavaScript Execution을 참고해보세요.
main스레드가 blocking되는걸 방지하기 위해 JavaScript in Web Workers을 실행할 수도 있습니다.
이제 browser는 문서의 구조, 각 요소의 스타일, 페이지의 기하학적 구조, 그리고 페인트 순서를 알게되었습니다, 이제 어떻게 페이지를 그리게 될까요?
이러한 정보를 화면의 pixel로 바꾸는 것을 "rasterizing"이라고 부릅니다.
아마 이러한 것들을 다루는 순수한 방식은 viewport내부의 일부를 raster하는 것입니다.
사용자가 페이지를 스크롤하면, raster된 프레임을 이동하고 더 rastering해서 빈 부분을 채워줍니다.
이런 방식이 Chrome이 처음 출시되었을 때 rasterizing을 처리한 방법입니다.
그러나, 최신 모던 브라우저에서는 더 섬세한 적업인 "compositing"이라는 과정을 실행합니다.
compositing이란 페이지의 일부를 layer로 분리하고, 그것들을 개별적으로 rasterize하며 compositor스레드라 불리는 별도의 스레드에서 페이지로 composite(합성)하는 기술입니다.
만약 스크롤이 발생하면 layer가 이미 rasterized되어있으므로,새 프레임을 composite(합성)하기만 하면 됩니다.
애니메이션도 같은방식으로 layer를 이동시키고 새프레임을 composite(합성)하여 얻을 수 있습니다.
레이어 패널 을 사용하여 DevTools에서 웹사이트가 레이어로 어떻게 분할되는지 확인할 수 있습니다.
어떤 요소가 어떤 layer에 있어야 하는지 알아내기 위해 main스레드는 layout트리를 통해 layer트리를 생성합니다.
(이 부분은 DevTools 성능 패널에서 "Update Layer Tree"라고 함).
만약 slide되는 사이드 메뉴와 같이 별도의 레이어여야 하는 페이지의 특정 부분에 레이어가 표시되지 않는 경우 will-change
라는 CSS의 속성을 사용하여 브라우저에 힌트를 줄 수 있습니다.
모든 요소에 레이어를 제공하고 싶을 수도 있지만 너무 많은 레이어에 걸쳐 합성하면 매 프레임 페이지의 작은 부분을 래스터화하는 것보다 작업 속도가 느려질 수 있으므로 애플리케이션의 렌더링 성능을 측정하는 것이 중요합니다.
더 자세한 내용은 Stick to Compositor-Only Properties and Manage Layer Count.를 참조하십시오 .
layer트리가 생성되고 paint순서가 결정되면 main스레드는 해당 정보를 compositor스레드에 커밋합니다.
그런 뒤 compositor스레드는 각각의 layer를 rasterize합니다.
페이지의 전체 길이 같은 layer는 클 수 있어므로
compositor스레드는 이것들을 타일로 나누고 각 타일을 raster스레드로 보냅니다.
raster스레드는 각각의 타일을 rasterize하고 GPU메모리에 저장합니다.
compositor스레드는 다른 raster스레드의 우선 순위를 지정하여
뷰포트 내(또는 근처)가 먼저 래스터화될 수 있도록 합니다.
또한 layer에는 줌인같은 작업을 처리하기 위해 다양한 해상도로 여러 타일링을 갖습니다.
한번 타일이 rasterize되면 compositor스레드는 draw quads라 불리는 타일 정보를 모아서 compositor프레임을 만들어냅니다.
그런 다음 compositor프레임 IPC를 통해 브라우저 프로세스에 제출됩니다.
이 시점에서 브라우저 UI 변경을 위해 UI스레드에서 또는 확장을 위해 다른 renderer프로세스에서 다른 compositor프레임을 추가할 수 있습니다.
이러한 compositor프레임은 GPU로 전송되어 화면에 표시됩니다.
스크롤 이벤트가 발생하면 compositor스레드는 GPU로 보낼 또 다른 compositor프레임을 생성합니다.
compositing의 이점은 main스레드의 관여없이도 수행된다는 것입니다.
compositor스레드는 스레드 계산이나 JavaScript실행 기다릴 필요가 없습니다.
그래서 애니메이션만 compositing하는 것이 부드러운 성능을 위한 최선의 방법으로 여겨지는 이유입니다.
만약 layout이나 paint를 다시 계산해야 하는 경우엔 main스레드가 관여되어집니다.