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

Mariko Kosaka

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

이 내용은 브라우저의 작동 방식을 살펴보는 블로그 시리즈 4부 중 3부입니다. 앞서 멀티 프로세스 아키텍처탐색 흐름을 다루었습니다. 이 게시물에서는 렌더기 프로세스 내부에서 어떤 일이 일어나는지 살펴보겠습니다

렌더기 프로세스는 웹 성능의 여러 측면에 영향을 미칩니다. 렌더기 프로세스 내에서 많은 작업이 진행되므로 이 게시물은 일반적인 개요일 뿐입니다. 더 자세한 내용은 웹 기초의 성능 섹션을 참조하세요.

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

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

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

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

파싱

DOM 생성

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

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

HTML 문서를 DOM으로 파싱하는 작업은 HTML 표준에 의해 정의됩니다. 브라우저에 HTML을 입력하면 오류가 발생하지 않습니다. 예를 들어 닫는 </p> 태그가 누락되면 유효한 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는 전체 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 Conference의 대담을 녹화해 시청해 보세요.

페인트

그리기 게임
그림 7: 캔버스 앞에 페인트 붓을 들고 먼저 원을 그려야 할지 정사각형 먼저 그려야 할지 고민하는 사람

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

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

Z-색인 실패
그림 8: HTML 마크업 순서대로 나타나는 페이지 요소로, Z-색인이 고려되지 않아 이미지가 잘못 렌더링됨

이 페인트 단계에서 기본 스레드는 레이아웃 트리를 걸어서 페인트 레코드를 만듭니다. 페인트 레코드는 '배경이 먼저 그 다음 텍스트, 직사각형 다음에 오는'와 같은 페인팅 프로세스의 메모입니다. JavaScript를 사용하여 <canvas> 요소를 그린 경우 이 프로세스가 익숙할 것입니다.

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

렌더링 파이프라인 업데이트에는 비용이 많이 듦

그림 10: 생성된 DOM+스타일, 레이아웃, 페인트 트리

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

요소에 애니메이션을 적용하는 경우 브라우저는 모든 프레임 간에 이러한 작업을 실행해야 합니다. 대부분의 디스플레이는 1초에 60회 (60fps)로 화면을 새로 고치며, 모든 프레임에서 화면에서 여러 항목을 이동할 때 사람의 눈에는 애니메이션이 부드럽게 표시됩니다. 하지만 애니메이션에서 프레임이 누락되면 페이지가 '버벅거림'처럼 보입니다.

누락된 프레임으로 인한 jage 버벅거림
그림 11: 타임라인의 애니메이션 프레임

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

JavaScript의 jage 버벅거림
그림 12: 타임라인의 애니메이션 프레임이지만 한 프레임이 JavaScript에 의해 차단됨

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

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

합성

페이지를 어떻게 그려야 할까요?

그림 14: 기본 래스터링 프로세스의 애니메이션

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

표시 영역 내부의 래스터 부분을 사용하면 간단하게 처리할 수 있습니다. 사용자가 페이지를 스크롤하면 래스터로 적용된 프레임을 이동하고 더 래스터화하여 누락된 부분을 채웁니다. Chrome이 처음 출시될 때 이렇게 래스터화를 처리했습니다. 그러나 최신 브라우저는 합성이라는 더 정교한 프로세스를 실행합니다.

합성이란?

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

컴포지션(compositing)은 페이지의 여러 부분을 레이어로 분리하고, 개별적으로 래스터화하고, 컴포지터 스레드라는 별도의 스레드에서 페이지로 합성하는 기법입니다. 레이어가 이미 래스터화되었기 때문에 스크롤이 발생하면 새 프레임을 합성하기만 하면 됩니다. 레이어를 이동하고 새 프레임을 합성하면 동일한 방식으로 애니메이션을 구현할 수 있습니다.

Layers(레이어) 패널을 사용하여 DevTools에서 웹사이트가 어떻게 레이어로 나뉘는지 확인할 수 있습니다.

레이어로 분할

어떤 요소가 어떤 레이어에 있어야 하는지 알아내기 위해 기본 스레드는 레이아웃 트리를 살펴보고 레이어 트리를 만듭니다 (이 부분을 DevTools 성능 패널에서 'Update Layer Tree'라고 함). 슬라이드 인 사이드 메뉴와 같이 페이지에서 별도의 레이어여야 하는 특정 부분이 표시되지 않는 경우 CSS에서 will-change 속성을 사용하여 브라우저에 힌트를 줄 수 있습니다.

레이어 트리
그림 16: 레이어 트리를 생성하는 레이아웃 트리를 살펴보는 기본 스레드

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

기본 스레드 외부의 래스터 및 합성

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

래스터
그림 17: 타일 비트맵을 만들고 GPU로 전송하는 래스터 스레드

컴포지터 스레드는 표시 영역 또는 근처의 항목이 먼저 래스터화될 수 있도록 여러 래스터 스레드의 우선순위를 지정할 수 있습니다. 또한 레이어에는 확대 작업 등을 처리하기 위해 다양한 해상도의 여러 타일이 있습니다.

타일이 래스터화되면 컴포지터 스레드는 그리기 쿼드라고 하는 타일 정보를 수집하여 컴포지터 프레임을 생성합니다.

대각선 그리기 메모리 내 타일 위치, 페이지 합성을 고려하여 카드를 그리는 페이지 위치와 같은 정보를 포함합니다.
컴포지터 프레임 페이지의 프레임을 나타내는 그리기 쿼드 모음입니다.

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

합성
그림 18: 합성 프레임을 생성하는 컴포지터 스레드 프레임이 브라우저 프로세스로 전송된 다음 GPU로 전송됩니다.

합성의 이점은 기본 스레드를 포함하지 않고 실행된다는 것입니다. 컴포지터 스레드는 스타일 계산이나 JavaScript 실행을 기다릴 필요가 없습니다. 이러한 이유로 원활한 성능을 위해서는 애니메이션만 합성하는 것이 가장 좋습니다. 레이아웃이나 페인트를 다시 계산해야 한다면 기본 스레드가 관련되어야 합니다.

요약

이 게시물에서는 파싱에서 합성에 이르는 렌더링 파이프라인을 살펴보았습니다. 이제 웹사이트의 성능 최적화에 관해 자세히 알아볼 수 있습니다.

이 시리즈의 다음 및 마지막 게시물에서는 컴포지터 스레드를 자세히 살펴보고 mouse moveclick와 같은 사용자 입력이 수신되면 어떻게 되는지 살펴보겠습니다.

게시물이 마음에 드셨나요? 향후 게시물에 관한 질문이나 제안이 있으면 아래 댓글 섹션을 통해 알려주시거나 트위터의 @kosamari를 통해 알려주시기 바랍니다.

다음: 입력이 컴포지터로 전송됩니다.