프론트엔드

『기초부터 완성까지, 프런트엔드』 8. 브라우저 렌더링 과정

오늘의 나1 2022. 1. 18. 18:01

8. 브라우저 렌더링 과정

브라우저의 구현 방법은 표준화되어 있지 않기 때문에 제조사별로 브라우저를 다르게 구현한다. 크롬의 경우 멀티프로세스 아키텍처를 차용하였다.

 

크롬 브라우저 아키텍처

크롬브라우저의 멀티프로세스 아키텍처 https://developers.google.com/web/updates/2018/09/inside-browser-part1

At the top is the browser process coordinating with other processes that take care of different parts of the application. For the renderer process, multiple processes are created and assigned to each tab. Until very recently, Chrome gave each tab a process when it could; now it tries to give each site its own process, including iframes.

https://developers.google.com/web/updates/2018/09/inside-browser-part1#browser-architecture

 

멀티프로세스 아키텍처는 다음과 같은 장점이 있다.
1)프로세스별로 메모리가 관리되어 보안성이 높다.
2)어떤 탭의 렌더링 프로세스가 응답하지 않는 경우에 다른 탭에 영향을 주지 않는다.

 

단점도 존재한다.

1)스레드를 사용할 때보다 메모리가 많이 든다. 스레드를 사용하면 프로세스를 사용할 때에 비해 공통으로 사용할 수 있는 부분이 있기 때문이다.
메모리가 많이 드는 단점을 보완하기 위해 크롬은 디바이스 사양에 따라 최대로 띄울 수 있는 프로세스의 수를 제한하고 있다. 이 수를 초과하면 같은 웹사이트를 방문한 여러 탭을 하나의 프로세스에서 실행하기 시작한다.

 

각 프로세스의 역할

브라우저 프로세스 브라우저 UI(주소창, 북마크, 이전, 이후 버튼 등), 네트워크 요청, 파일 엑세스 권한 처리
렌더러 프로세스 웹사이트가 표시되는 탭 영역의 모든 부분을 제어
플러그인 프로세스 flash 같은 웹사이트 내에서 사용되는 플러그인을 제어
GPU 프로세스 GPU 작업을 수행, 여러 앱의 요청을 처리하여 한 화면에 보여줘야 하기 때문에 독립적인 프로세스로 분리
확장프로그램 프로세스  
유틸리티 프로세스  

 

작업관리자

크롬브라우저의 작업관리자를 통해 크롬브라우저가 실행 중인 프로세스의 종류를 확인할 수 있다.

브라우저 프로세스 1개, GPU 프로세스 1개, 유틸리티 프로세스 3개, 렌더러 프로세스 4개(탭 3개 + 예비 렌더러), 확장 프로그램 프로세스 2개

작업관리자를 열어봐서 프로세스를 확인했다. 새로웠던 점은 예비 렌더러 프로세스가 할당되었다는 것과 유틸리티 프로세스의 종류가 많다는 점이다.

 

8.1 렌더링 과정 살펴보기

브라우저 프로세스 안에 UI 스레드, 네트워크 스레드, 스토리지 스레드 3개가 실행되고 있다고 가정한다.

 

웹사이트로 이동

Step1. 주소창에 텍스트를 입력하면,
UI 스레드는 입력된 텍스트가 일반문자열인지 URL인 지 파악한다. 

URL이면 이동하고, 일반문자열이면 검색엔진에 요청을 보내기 위함이다.
Step2. 주소창에서 엔터를 치면,
UI 스레드는 네트워크 요청을 시작한다. 탭 상단에 로딩바가 표시되고, 네트워크 스레드는 네트워크 요청을 처리한다. 
Step3. 네트워크 스레드는 응답의 종류와 응답이 안전한 지를 확인한다.
Step4. 네트워크 스레드가 모든 검사를 완료하면 UI 스레드에게 응답 데이터가 준비되었다고 알린다. UI 스레드는 렌더러 프로세스를 찾는다.
Step5. 브라우저 프로세스는 IPC를 통해 렌더러 프로세스에게 데이터를 보낸다. 렌더러 프로세스가 응답하면 네비게이션은 종료되고 다큐먼트 로딩 단계가 시작된다.

주소창과 보안 표시가 업데이트된다. 탭에 대한 세션 히스토리가 저장되어 이전, 이후 버튼으로 이동할 수 있다. 
Extra Step. 렌더러 프로세스는 리소스를 로드하고 페이지를 렌더링한다. 렌더러 프로세스가 렌더링을 종료하면, load 이벤트가 실행되고, 이후 브라우저 프로세스에 페이지가 로드되었음을 알린다.
다른 웹사이트로 이동할 때.
다른 웹사이트로 이동하는 경우 브라우저 프로세스 또는 렌더러 프로세스는 beforeunload 이벤트 핸들러를 확인한다. 

새로운 렌더러 프로세스를 띄워 렌더링을 시작하는 데, 기존 렌더러 프로세스는 기존 페이지의 unload 이벤트 등을 확인한다.

 

8.2 렌더러 프로세스의 작업

렌더러 프로세스는 메인스레드, 워커스레드, 컴포지터스레드, 래스터스레드로 구성된다.

각 스레드의 역할은 다음과 같다. 메인스레드는 렌더링 작업을 담당한다. 워커스레드는 웹 워커나 서비스 워커를 실행한다. 컴포지터스레드와 래스터스레드는 페이지를 효율적이고 부드럽게 출력하는 일을 한다. 

Parsing: 도큐먼트 구조 구하기 메인스레드는 HTML을 파싱하여 DOM 트리를 생성한다.

다른 리스소를 다운로드 받을 때 경우에 따라 메인스레드의 DOM 파싱이 중단되는 경우가 있다. 
<script> 태그는 자바스크립트 소스가 DOM을 변경하는 경우가 있기 때문에 DOM 파싱을 중단하고 자바스크립트 소스를 다운로드하고 실행한다. 이후 DOM 파싱을 재개한다.
반면에, <link>, <img> 태그 등의 리소스를 다운로드할 때는 DOM 파싱을 중단하지 않고, 브라우저프로세스의 네트워크스레드로 요청을 보낸다.

다른 리소스의 로딩 방식을 선택할 수도 있다.
<script> 태그의 async, defer 속성을 넣으면 DOM 파싱을 중단하지 않고 비동기로 로드가 된다. 자바스크립트 코드에서 DOM 조작을 하지 않는 경우 async, defer 속성을 넣으면 렌더링 속도 향상에 도움이 된다. 
<link> 태그의 경우 rel="preload" 속성을 지정하여 우선적으로 로드되도록 할 수 있다.
Style Calculation: 각 엘리먼트의 스타일 구하기 메인스레드는 CSS를 파싱하여 DOM 노드에 어떤 스타일을 적용할 지 결정한다.
Layout: 페이지 상의 위치 구하기 메인스레드는 각각의 DOM 노드들의 좌표(x,y)와 박스사이즈 등에 관한 정보를 갖고 있는 레이아웃트리를 생성한다.

레이아웃트리는 실제로 영역을 차지하는 DOM만 포함한다.
예를 들어, display: none이 적용된 노드는 레잉아웃트리에 포함되지 않는다. 반면에, visibility: hidden이나 p::before{content: "Hi!"}가 적용된 노드는 레이아웃트리에 포함된다. 
Paint: 어떤 순서로 배치할 지 구하기 메인스레드는 레이아웃 트리를 통해 페인트 레코드(어떤 노드를 더 앞에 배치할 것인지)를 생성한다.
Composition: 페이지를 레이어로 나누어 그리기 메인스레드는 레이아웃 트리를 통해 페이지를 어떻게 레이어로 쪼갤 지 판단하고 이에 관한 레이어 트리를 생성한다.

레이어 트리가 생성되고 페인팅 순서가 결정되면, 메인스레드는 컴포지터 스레드에게 이 정보를 넘긴다.

컴포지터 스레드는 각각의 레이어를 래스터라이징(화면 속 픽셀로 변환)한다. 레이어의 크기에 대한 제한이 없기 때문에 컴포지터 스레드는 하나의 레이어를 여러 개의 타일로 분할한다. 그 후, 각각의 타일을 래스터스레드에게 보낸다.

래스터 스레드는 각각의 타일을 래스터라이징하고 이를 GPU 메모리에 저장한다.

컴포지터 스레드는 래스팅된 타일을 모아 컴포지터 프레임을 구성한다. 

컴포지터 프레임은 IPC를 통해 브라우저 프로세스에 보내진다. 이 프레임은 UI 스레드를 통해 GPU로 보내진다.

스크롤이 일어나면 해당하는 컴포지터 프레임을 모아 다시 브라우저 프로세스로 보낸다.


렌더링 성능에 관한 이야기

렌더링은 비용이 많이 드는 작업이다. Style ➡️ Layout ➡️ Paint 과정은 이전 과정의 결과를 통해서 얻어지기 때문에 하나가 업데이트되면 그 뒤 과정도 업데이트가 따라온다. 예를 들어, 레이아웃 트리의 일부가 변경되면 변경된 노드에 관한 Paint 과정이 수반된다. 

애니메이션을 렌더링할 때는 자바스크립트를 잘게 쪼개는 것이 좋다. 용량이 큰 자바스크립트 코드를 실행하게 되면 애니메이션을 업데이트할 프레임 간격보다 길게 수행된다. 이 경우, 애니메이션을 업데이트하지 못했기 때문에 애니메이션이 버벅이는 것처럼 보인다.
부득이하게 용량이 큰 자바스크립트 코드를 실행할 경우에는 requestAnimationFrame() API를 사용하여, 자바스크립트 실행 중 애니메이션이 프레임마다 업데이트되도록 설정할 수 있다. 

 

컴포지션에 관한 이야기

초기 크롬에서는 컴포지션 과정이 적용되지 않았다. 뷰포트에 출력되는 영역만 그때 그때 계산해서 래스팅했다. 컴포지션 과정을 적용하면, 페이지 전체를 출력하는 데 필요한 정보를 타일별로 저장해두고, 뷰포트 변경될 때 필요한 타일만 건네주면 된다. 레이아웃 변경이 없는 한, 재계산할 필요가 없다. 

 

8.3 브라우저별 렌더링 엔진

Chrome Blink, Webkit(모바일 iOS Chrome)
Safari Webkit
IE Trident
Edge EdgeHTML ➡️ Blink
Firefox Gecko

 

출처