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

코사카 마리코

렌더러 프로세스 내부 작동

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

렌더기 프로세스는 웹 성능의 여러 측면에 영향을 미칩니다. 렌더기 프로세스 내에서 많은 일이 일어나기 때문에 이 게시물은 일반적인 개요에 불과합니다. 더 자세히 알아보려면 웹 기초의 성능 섹션을 참조하세요.

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

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

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

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

파싱

DOM 생성

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

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

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

하위 리소스 로드 중

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

DOM
그림 2: HTML 파싱 및 DOM 트리 빌드

자바스크립트가 파싱을 차단할 수 있음

HTML 파서는 <script> 태그를 찾으면 HTML 문서의 파싱을 일시중지하고 자바스크립트 코드를 로드, 파싱, 실행해야 합니다. 그 이유는 무엇인가요? JavaScript는 전체 DOM 구조를 변경하는 document.write()와 같은 요소를 사용하여 문서의 모양을 변경할 수 있기 때문입니다 (HTML 사양의 파싱 모델 개요에서 다이어그램을 볼 수 있음). 따라서 HTML 파서가 자바스크립트가 실행될 때까지 기다려야 HTML 문서의 파싱을 재개할 수 있습니다. JavaScript 실행에서 발생하는 상황이 궁금하다면 V8팀에서 이에 관한 강연과 블로그 게시물을 확인하세요.

리소스 로드 방식을 브라우저에 알립니다.

리소스를 원활하게 로드하기 위해 웹 개발자가 브라우저에 힌트를 보낼 수 있는 방법은 다양합니다. JavaScript가 document.write()를 사용하지 않는 경우 async 또는 defer 속성을 <script> 태그에 추가할 수 있습니다. 그러면 브라우저가 JavaScript 코드를 비동기식으로 로드하고 실행하며 파싱을 차단하지 않습니다. 해당하는 경우 JavaScript 모듈을 사용할 수도 있습니다. <link rel="preload">는 현재 탐색에 반드시 이 리소스가 필요하고 최대한 빨리 다운로드하고 싶다고 브라우저에 알리는 방법입니다. 자세한 내용은 리소스 우선순위 지정 - 브라우저의 문제 해결을 참고하세요.

스타일 계산

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

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

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

레이아웃

이제 렌더기 프로세스는 문서 구조와 각 노드의 스타일을 알지만 페이지를 렌더링하기에는 충분하지 않습니다. 휴대전화로 친구에게 그림을 묘사한다고 상상해 보세요. '커다란 빨간색 원과 작은 파란색 정사각형'으로는 그림이 정확히 어떤 모양인지 친구가 알 수 있는 정보가 아닙니다.

인간 팩스 게임
그림 4: 그림 앞에 서서 상대방에게 전화가 연결된 사람

레이아웃은 요소의 도형을 찾는 프로세스입니다. 기본 스레드는 DOM 및 계산된 스타일을 탐색하고 xy 좌표 및 경계 상자 크기와 같은 정보가 포함된 레이아웃 트리를 만듭니다. 레이아웃 트리는 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+스타일, 레이아웃, 페인트 트리

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

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

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

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

JavaScript의 jage 버벅거림
그림 12: 타임라인에 있는 애니메이션 프레임이지만 하나의 프레임이 자바스크립트에 의해 차단됨

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

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

합성

페이지는 어떻게 그리면 좋을까요?

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

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

이를 처리하는 단순한 방법은 표시 영역 내부의 부분을 래스터하는 것입니다. 사용자가 페이지를 스크롤하면 래스터화된 프레임을 이동하고 더 많이 래스터링하여 누락된 부분을 채웁니다. 이것이 처음 출시되었을 때 Chrome에서 래스터화 작업을 처리한 방법입니다. 그러나 최신 브라우저는 합성이라는 보다 정교한 프로세스를 실행합니다.

합성이란 무엇인가요?

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

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

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

레이어로 나누기

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

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

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

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

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

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

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

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

사각형 그리기 메모리 내 카드의 위치, 페이지 구성을 고려하여 페이지에서 타일을 그릴 위치와 같은 정보가 포함됩니다.
합성 프레임 페이지의 프레임을 나타내는 그리기 사각형 모음입니다.

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

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

합성의 이점은 기본 스레드를 포함하지 않고 수행된다는 것입니다. 컴포지터 스레드는 스타일 계산이나 자바스크립트 실행을 기다릴 필요가 없습니다. 따라서 애니메이션만 합성하는 것이 원활한 성능을 위해 가장 좋은 것으로 간주됩니다. 레이아웃 또는 페인트를 다시 계산해야 하는 경우 기본 스레드를 포함해야 합니다.

결론

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

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

게시물이 도움이 되었나요? 향후 게시물과 관련하여 질문이나 제안사항이 있으면 아래의 의견 섹션이나 트위터의 @kosamari를 통해 연락해 주시기 바랍니다.

다음: 컴포지터에 입력