최신 웹브라우저 들여다보기 (3부)

Mariko Kosaka

렌더러 프로세스의 내부 작동

이 도움말은 브라우저의 작동 방식을 살펴보는 4부로 구성된 블로그 시리즈 중 3부입니다. 이전에는 멀티프로세스 아키텍처탐색 흐름을 다뤘습니다. 이 게시물에서는 렌더러 프로세스 내에서 어떤 일이 일어나는지 살펴봅니다.

렌더러 프로세스는 웹 성능의 여러 측면을 다룹니다. 렌더러 프로세스 내에서 많은 작업이 이루어지므로 이 게시물은 일반적인 개요만 제공합니다. 자세히 알아보려면 웹 기초의 성능 섹션에서 더 많은 리소스를 확인하세요.

렌더러 프로세스가 웹 콘텐츠 처리

렌더러 프로세스는 탭 내에서 발생하는 모든 작업을 담당합니다. 렌더러 프로세스에서 기본 스레드는 사용자에게 전송하는 대부분의 코드를 처리합니다. 웹 워커 또는 서비스 워커를 사용하는 경우 JavaScript의 일부가 작업자 스레드에서 처리되는 경우가 있습니다. 컴포저와 래스터 스레드도 렌더러 프로세스 내에서 실행되어 페이지를 효율적이고 원활하게 렌더링합니다.

렌더러 프로세스의 핵심 작업은 HTML, CSS, JavaScript를 사용자가 상호작용할 수 있는 웹페이지로 변환하는 것입니다.

렌더러 프로세스
그림 1: 내부에 기본 스레드, 작업자 스레드, 컴포저 스레드, 래스터 스레드가 있는 렌더러 프로세스

파싱

DOM 구성

렌더러 프로세스가 탐색에 관한 커밋 메시지를 수신하고 HTML 데이터를 수신하기 시작하면 기본 스레드가 텍스트 문자열 (HTML)을 파싱하여 DocumentObjectModel (DOM)로 변환하기 시작합니다.

DOM은 페이지의 브라우저 내부 표현이며 웹 개발자가 JavaScript를 통해 상호작용할 수 있는 데이터 구조 및 API입니다.

HTML 문서를 DOM으로 파싱하는 것은 HTML 표준에 의해 정의됩니다. 브라우저에 HTML을 제공해도 오류가 발생하지 않는 것을 확인했을 수 있습니다. 예를 들어 닫는 </p> 태그가 누락된 HTML은 유효한 HTML입니다. Hi! <b>I'm <i>Chrome</b>!</i>와 같은 잘못된 마크업 (b 태그가 i 태그 전에 닫힘)은 Hi! <b>I'm <i>Chrome</i></b><i>!</i>를 작성한 것처럼 취급됩니다. 이는 HTML 사양이 이러한 오류를 적절하게 처리하도록 설계되었기 때문입니다. 이러한 작업이 어떻게 실행되는지 궁금하다면 HTML 사양의 '파서에서의 오류 처리 및 이상한 사례 소개' 섹션을 참고하세요.

하위 리소스 로드

웹사이트는 일반적으로 이미지, CSS, JavaScript와 같은 외부 리소스를 사용합니다. 이러한 파일은 네트워크 또는 캐시에서 로드해야 합니다. 기본 스레드는 DOM을 빌드하기 위해 파싱하는 동안 찾을 때마다 하나씩 요청할 있지만 속도를 높이기 위해 '미리 로드 스캐너'가 동시에 실행됩니다. HTML 문서에 <img> 또는 <link>와 같은 항목이 있으면 미리 로드 스캐너가 HTML 파서에서 생성된 토큰을 살펴보고 브라우저 프로세스의 네트워크 스레드에 요청을 전송합니다.

DOM
그림 2: HTML을 파싱하고 DOM 트리를 빌드하는 기본 스레드

JavaScript가 파싱을 차단할 수 있음

HTML 파서가 <script> 태그를 발견하면 HTML 문서의 파싱을 일시중지하고 JavaScript 코드를 로드, 파싱, 실행해야 합니다. 이유는 JavaScript가 전체 DOM 구조를 변경하는 document.write()와 같은 것을 사용하여 문서의 모양을 변경할 수 있기 때문입니다 (HTML 사양의 파싱 모델 개요에 멋진 다이어그램이 있음). 이 때문에 HTML 파서는 HTML 문서 파싱을 재개하기 전에 JavaScript가 실행될 때까지 기다려야 합니다. JavaScript 실행 시 어떤 일이 일어나는지 궁금하다면 V8팀의 강연 및 블로그 게시물을 참고하세요.

브라우저에 리소스를 로드하는 방법에 관한 힌트 제공

웹 개발자가 리소스를 원활하게 로드하기 위해 브라우저에 힌트를 전송하는 방법에는 여러 가지가 있습니다. JavaScript에서 document.write()를 사용하지 않는 경우 <script> 태그에 async 또는 defer 속성을 추가할 수 있습니다. 그러면 브라우저가 JavaScript 코드를 비동기식으로 로드하고 실행하며 파싱을 차단하지 않습니다. 적절한 경우 JavaScript 모듈을 사용할 수도 있습니다. <link rel="preload">는 현재 탐색에 리소스가 꼭 필요하며 최대한 빨리 다운로드하고 싶다고 브라우저에 알리는 방법입니다. 리소스 우선순위 지정 – 브라우저의 도움 받기에서 자세히 알아보세요.

스타일 계산

CSS에서 페이지 요소의 스타일을 지정할 수 있으므로 DOM만으로는 페이지가 어떻게 표시될지 알 수 없습니다. 기본 스레드는 CSS를 파싱하고 각 DOM 노드의 계산된 스타일을 결정합니다. CSS 선택자를 기반으로 각 요소에 적용되는 스타일의 종류에 관한 정보입니다. 이 정보는 DevTools의 computed 섹션에서 확인할 수 있습니다.

계산된 스타일
그림 3: CSS를 파싱하여 계산된 스타일을 추가하는 기본 스레드

CSS를 제공하지 않더라도 각 DOM 노드에는 계산된 스타일이 있습니다. <h1> 태그가 <h2> 태그보다 더 크게 표시되고 각 요소에 여백이 정의됩니다. 이는 브라우저에 기본 스타일 시트가 있기 때문입니다. Chrome의 기본 CSS가 어떤지 알아보려면 여기에서 소스 코드를 확인하세요.

레이아웃

이제 렌더러 프로세스는 문서의 구조와 각 노드의 스타일을 알고 있지만 페이지를 렌더링하기에는 충분하지 않습니다. 전화로 친구에게 그림을 설명하려고 한다고 가정해 보겠습니다. '큰 빨간색 원과 작은 파란색 정사각형이 있습니다'라는 정보는 친구가 그림이 정확히 어떤 모습인지 알기에는 충분하지 않습니다.

인간 팩스 게임
그림 4: 그림 앞에 서 있는 사람, 다른 사람과 연결된 전화선

레이아웃은 요소의 도형을 찾는 프로세스입니다. 기본 스레드는 DOM과 계산된 스타일을 살펴보고 x, y 좌표 및 경계 상자 크기와 같은 정보가 포함된 레이아웃 트리를 만듭니다. 레이아웃 트리는 DOM 트리와 유사한 구조일 수 있지만 페이지에 표시되는 항목과 관련된 정보만 포함합니다. display: none가 적용된 요소는 레이아웃 트리의 일부가 아닙니다. 하지만 visibility: hidden가 있는 요소는 레이아웃 트리에 있습니다. 마찬가지로 p::before{content:"Hi!"}와 같은 콘텐츠가 포함된 가상 요소가 적용되면 DOM에 없더라도 레이아웃 트리에 포함됩니다.

레이아웃
그림 5: 계산된 스타일로 DOM 트리를 살펴보고 레이아웃 트리를 생성하는 기본 스레드
그림 6: 줄바꿈 변경으로 인해 이동하는 단락의 상자 레이아웃

페이지의 레이아웃을 결정하는 것은 어려운 작업입니다. 위에서 아래로의 블록 흐름과 같은 가장 간단한 페이지 레이아웃조차도 글꼴 크기와 줄바꿈 위치를 고려해야 합니다. 글꼴 크기와 줄바꿈 위치는 단락의 크기와 모양에 영향을 미치고, 이는 다음 단락의 위치에 영향을 미치기 때문입니다.

CSS를 사용하면 요소를 한쪽으로 플로팅하고, 오버플로 항목을 마스크하고, 쓰기 방향을 변경할 수 있습니다. 이 레이아웃 단계에는 큰 작업이 있습니다. Chrome에서는 전체 엔지니어팀이 레이아웃을 담당합니다. 작업에 대한 세부정보를 확인하려면 BlinkOn 컨퍼런스의 몇 가지 강연을 시청해 보세요. 녹화된 강연이 있으며 매우 흥미롭습니다.

페인트

그리기 게임
그림 7: 캔버스 앞에서 붓을 들고 원을 먼저 그릴지 정사각형을 먼저 그릴지 고민하는 사람

DOM, 스타일, 레이아웃이 있어도 페이지를 렌더링하기에 충분하지 않습니다. 그림을 재현하려고 한다고 가정해 보겠습니다. 요소의 크기, 모양, 위치를 알고 있지만 어떤 순서로 페인트할지는 판단해야 합니다.

예를 들어 특정 요소에 z-index가 설정될 수 있습니다. 이 경우 HTML에 작성된 요소의 순서대로 페인팅하면 잘못된 렌더링이 발생합니다.

z-index 실패
그림 8: 페이지 요소가 HTML 마크업의 순서대로 표시되어 z-index가 고려되지 않아 잘못 렌더링된 이미지가 표시됨

이 페인트 단계에서 기본 스레드는 레이아웃 트리를 탐색하여 페인트 레코드를 만듭니다. 페인트 레코드는 '배경부터 먼저 그리고 텍스트, 직사각형 순으로'와 같은 페인팅 프로세스에 관한 메모입니다. JavaScript를 사용하여 <canvas> 요소에 그린 적이 있다면 이 프로세스가 익숙할 수 있습니다.

페인트 레코드
그림 9: 레이아웃 트리를 탐색하고 페인트 레코드를 생성하는 기본 스레드

렌더링 파이프라인을 업데이트하는 데 비용이 많이 듭니다.

그림 10: 생성 순서에 따른 DOM+스타일, 레이아웃, 페인트 트리

렌더링 파이프라인에서 가장 중요한 점은 각 단계에서 이전 작업의 결과가 새 데이터를 만드는 데 사용된다는 것입니다. 예를 들어 레이아웃 트리에서 변경사항이 발생하면 영향을 받는 문서 부분의 페인트 순서를 다시 생성해야 합니다.

요소에 애니메이션을 적용하는 경우 브라우저는 모든 프레임 간에 이러한 작업을 실행해야 합니다. 대부분의 디스플레이는 초당 60회 (60fps)로 화면을 새로고침합니다. 프레임마다 화면에서 항목을 이동하면 애니메이션이 인간의 눈에 부드럽게 보입니다. 그러나 애니메이션이 중간의 프레임을 놓치면 페이지가 '잡음이 있는' 것처럼 보입니다.

프레임 누락으로 인한 버벅거림
Figure 11: Timeline의 애니메이션 프레임

렌더링 작업이 화면 새로고침을 따라가더라도 이러한 계산은 기본 스레드에서 실행되므로 애플리케이션에서 JavaScript를 실행할 때 차단될 수 있습니다.

JavaScript의 자그 끊김
그림 12: 타임라인의 애니메이션 프레임 중 하나가 JavaScript에 의해 차단됨

JavaScript 작업을 작은 청크로 나누고 requestAnimationFrame()를 사용하여 모든 프레임에서 실행되도록 예약할 수 있습니다. 이 주제에 관한 자세한 내용은 JavaScript 실행 최적화를 참고하세요. 기본 스레드가 차단되지 않도록 웹 워커에서 JavaScript를 실행할 수도 있습니다.

애니메이션 프레임 요청
그림 13: 애니메이션 프레임이 있는 타임라인에서 실행되는 소규모 JavaScript 청크

합성

페이지를 어떻게 그리시겠어요?

그림 14: 순진한 래스터링 프로세스의 애니메이션

이제 브라우저가 문서의 구조, 각 요소의 스타일, 페이지의 도형, 페인트 순서를 알게 되었으므로 페이지를 어떻게 그릴까요? 이 정보를 화면의 픽셀로 변환하는 것을 래스터화라고 합니다.

이 문제를 처리하는 원시적인 방법은 표시 영역 내의 부분을 래스터화하는 것입니다. 사용자가 페이지를 스크롤하면 래스터링된 프레임을 이동하고 더 래스터링하여 누락된 부분을 채웁니다. Chrome이 처음 출시되었을 때 래스터 처리를 처리한 방식입니다. 하지만 최신 브라우저는 컴포지팅이라는 더 정교한 프로세스를 실행합니다.

합성이란 무엇인가요?

그림 15: 합성 프로세스의 애니메이션

합성은 페이지의 일부를 레이어로 분리하고 별도로 래스터화한 후 컴포저러 스레드라는 별도의 스레드에서 페이지로 합성하는 기법입니다. 스크롤이 발생하면 레이어가 이미 래스터화되었으므로 새 프레임을 합성하기만 하면 됩니다. 애니메이션은 레이어를 이동하고 새 프레임을 합성하는 것과 동일한 방식으로 실행할 수 있습니다.

DevTools에서 레이어 패널을 사용하여 웹사이트가 레이어로 분할되는 방식을 확인할 수 있습니다.

레이어로 분할

어떤 요소가 어떤 레이어에 있어야 하는지 확인하기 위해 기본 스레드는 레이아웃 트리를 탐색하여 레이어 트리를 만듭니다 (이 부분은 DevTools 성능 패널에서 '레이어 트리 업데이트'라고 함). 별도의 레이어여야 하는 페이지의 특정 부분 (예: 슬라이드 인 측면 메뉴)이 레이어를 가져오지 않는 경우 CSS에서 will-change 속성을 사용하여 브라우저에 힌트를 줄 수 있습니다.

레이어 트리
레이어 트리를 생성하는 레이아웃 트리를 탐색하는 기본 스레드 그림 16

모든 요소에 레이어를 적용하고 싶을 수 있지만 과도한 수의 레이어에서 합성하면 프레임마다 페이지의 작은 부분을 래스터화하는 것보다 느린 작업이 발생할 수 있으므로 애플리케이션의 렌더링 성능을 측정하는 것이 중요합니다. 주제에 관한 자세한 내용은 컴포저 전용 속성 고수 및 레이어 개수 관리를 참고하세요.

기본 스레드 외부에서 래스터링 및 합성

레이어 트리가 생성되고 페인트 순서가 결정되면 기본 스레드는 이 정보를 컴포저 스레드에 커밋합니다. 그런 다음 컴포저 스레드가 각 레이어를 래스터라이즈합니다. 레이어는 페이지의 전체 길이처럼 클 수 있으므로 컴포저 스레드는 레이어를 타일로 나누고 각 타일을 래스터 스레드로 전송합니다. 래스터 스레드는 각 타일을 래스터화하여 GPU 메모리에 저장합니다.

래스터
그림 17: 래스터 스레드가 타일의 비트맵을 만들고 GPU로 전송합니다.

컴포저 스레드는 뷰포인트 내의 항목 또는 근처 항목을 먼저 래스터링할 수 있도록 다양한 래스터 스레드의 우선순위를 지정할 수 있습니다. 레이어에는 확대 작업과 같은 작업을 처리하기 위해 다양한 해상도에 맞는 여러 타일링도 있습니다.

타일이 래스터링되면 컴포저 스레드는 드로우 쿼드라는 타일 정보를 수집하여 컴포저 프레임을 만듭니다.

쿼드 그리기 메모리 내 카드의 위치, 페이지 합성을 고려하여 카드를 그릴 페이지의 위치 등의 정보를 포함합니다.
컴포저이터 프레임 페이지의 프레임을 나타내는 그리기 사각형 모음입니다.

그런 다음 컴포저 프레임이 IPC를 통해 브라우저 프로세스에 제출됩니다. 이 시점에서 브라우저 UI 변경을 위해 UI 스레드에서 또는 확장 프로그램의 다른 렌더러 프로세스에서 다른 컴포저 프레임을 추가할 수 있습니다. 이러한 컴포저 프레임은 GPU로 전송되어 화면에 표시됩니다. 스크롤 이벤트가 들어오면 컴포저 스레드가 GPU로 전송할 다른 컴포저 프레임을 만듭니다.

composit
그림 18: 컴포지션 프레임을 만드는 컴포저러 스레드 프레임이 브라우저 프로세스로 전송된 후 GPU로 전송됩니다.

합성의 이점은 기본 스레드의 개입 없이 실행된다는 것입니다. 컴포저 스레드는 스타일 계산이나 JavaScript 실행을 기다릴 필요가 없습니다. 따라서 애니메이션만 합성하는 것이 원활한 성능을 위해 가장 좋은 방법으로 간주됩니다. 레이아웃이나 페인트를 다시 계산해야 하는 경우 기본 스레드가 관여해야 합니다.

마무리

이 게시물에서는 파싱에서 합성까지 렌더링 파이프라인을 살펴봤습니다. 이제 웹사이트의 성능 최적화에 대해 자세히 알아보시기 바랍니다.

이 시리즈의 다음 게시물인 마지막 게시물에서는 컴포저러 스레드를 자세히 살펴보고 mouse moveclick와 같은 사용자 입력이 들어오면 어떻게 되는지 알아봅니다.

게시물이 마음에 드셨나요? 향후 게시물에 관해 궁금한 점이 있거나 제안하고 싶은 점이 있으면 아래 댓글 섹션이나 트위터의 @kosamari를 통해 알려주세요.

다음: 컴포저이터에 입력이 들어옴