CS/브라우저

브라우저 살펴보기 - 렌더러 프로세스의 내부동작(2)

H.E 2024. 6. 8. 20:53

기술글을 보고 정리하였음(https://d2.naver.com/helloworld/5237120)

이 글은 렌더러 프로세스가 HTML 문서를 받았을 때 어떤 절차를 거쳐 화면을 구성하는지 설명하고 있음

기술글이 2019년도에 작성된 글이기 때문에 현재와는 많이 다른점이 있을것이기에 이런 흐름으로 만들어졌다고 이해하고 현재와 다른점이 있다는것을 생각하면서 봐야함(크롬에 많은 버전 업데이트가 있었기 때문)

 

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

렌더러 프로세스는 여러 측면에서 웹 페이지의 성능에 영향을 끼침

 

▶ 렌더러 프로세스는 웹 콘텐츠를 처리

메인 스레드와 워커 스레드, 컴포지터 스레드, 래스터 스레드가 있는 렌더러 프로세스

렌더러 프로세스 탭 내부에서 발생하는 모든 작업을 담당

렌더러 프로세스의 메인 스레드 브라우저로 전송된 대부분의 코드를 처리

간혹 웹 워커나 서비스 워커를 사용하는 경우에는 워커 스레드 JavaScript 코드의 일부를 처리

웹 페이지를 효율적이고 부드럽게 렌더링하기 위해 별도의 컴포지터 스레드(웹 페이지의 시각적 요소들을 결합하여 최종 화면에 렌더링하는 작업 담당)와 래스터 스레드(웹 페이지의 각 레이어를 실제로 래스터화하는 작엄 담당, 벡터 그래픽을 픽셀 단위의 비트맵 이미지로 변화하는 과정을 처리)가 렌더러 프로세스에서 실행됨

렌더러 프로세스의 주요 역할은 HTML과 CSS, JavaScript를 사용자와 상호작용을 할 수 있는 웹 페이지로 변환하는 것

▶ 파싱

DOM 구축

페이지를 이동하는 내비게이션 실행 메시지를 렌더러 프로세스가 받고 HTML 데이터를 수신하기 시작하면 렌더러 프로세스의 메인 스레드 문자열(HTML)을 파싱해서 DOM(document object model)으로 변환하기 시작

DOM은 브라우저가 내부적으로 웹 페이지를 표현하는 방법일 뿐만 아니라 웹 개발자가 JavaScript를 통해 상호작용을 할 수 있는 데이터 구조이자 API

HTML 문서를 DOM으로 파싱하는 방법은 HTML 표준에 정의되어 있음

브라우저에서 HTML 문서를 열었을 때 오류를 받지 않는데 이유는 오류를 우아하게 처리하도록 HTML 명세가 설계됐기 때문(ex. 닫는 태그 누락, 먼저 닫힌 잘못된 마크업)

하위 리소스(subresource) 로딩

HTML을 파싱하고 DOM 트리를 만드는 메인 스레드

 

웹 사이트는 일반적으로 이미지, CSS, JavaScript와 같은 외부 리소스를 사용

이러한 파일은 네트워크나 캐시에서 로딩해야 함

DOM을 구축하기 위해 파싱하는 동안 이런 리소스를 만날 때마다 메인 스레드가 하나하나 요청할 수도 있을 것임

하지만 속도를 높이기 위해 '프리로드(Preload) 스캐너' 동시에 실행

HTML 문서에 <img> 또는 <link> 와 같은 태그가 있으면 프리로드 스캐너는 HTML 파서가 생성한 토큰을 확인하고 브라우저 프로세스의 네트워크 스레드에 요청을 보냄

자바 스크립트가 파싱을 막을 수 있다

<script> 태그를 만나면 HTML 파서 HTML 문서의 파싱을 일시 중지한 다음 JavaScript 코드를 로딩하고 파싱해 실행해야 함 DOM 구조 전체를 바꿀 수 있는 document.write() 메서드와 같은 것을 사용해 문서의 모양을 변경할 수 있기 때문 HTML 파싱을 재개하기 전에 HTML 파서 JavaScript의 실행이 끝나기를 기다려야 함

 

리소스를 어떻게 로딩하길 원하는지 브라우저에 힌트를 주는 방법

JavaScript에서 document.write() 메서드를 사용하지 않는다면 <script> 태그에 async 속성이나 defer 속성 추가할 수 있음. 이 속성이 있으면 브라우저가 JavaScript 코드를 비동기적으로 로딩하고 실행하면서 HTML 파싱을 막지 않음

 

또한 JavaScript 모듈 사용할 수도 있음

<link rel="preload">는 현재 내비게이션을 실행하기 위해 리소스가 반드시 필요하다는 것을 브라우저에 알려서 리소스를 가능한 한 빨리 다운로드하려는 경우에 사용할 수 있음


▶ 스타일 계산

계산된 스타일을 추가하기 위해 CSS를 파싱하는 메인 스레드

 

DOM만으로는 웹 페이지의 모양을 알 수 없음. CSS 웹 페이지 요소의 모양을 결정할 수 있기 때문

메인 스레드 CSS를 파싱하고 각 DOM 노드에 해당되는 계산된 스타일(computed style)을 확정

계산된 스타일은 CSS 선택자(selector)로 구분되는 요소에 적용될 스타일에 관한 정보

CSS 전혀 적용하지 않아도 DOM 노드에는 계산된 스타일이 적용되어 있음

<h1> 태그는 <h2> 태그보다 크게 표시되며 바깥 여백(margin)이 모든 요소에 적용됨

브라우저에 기본 스타일 시트가 있기 때문(html.css 파일을 보면 Chrome의 기본 CSS를 알수 있음)

▶ 레이아웃

원과 사각형만을 듣는것만으로는 그림이 전체적으로 어떻게 생겼는지 알수 없음

 

레이아웃은 요소의 기하학적 속성(geometry)를 찾는 과정

메인 스레드는 DOM과 계산된 스타일을 훑어가며 레이아웃 트리를 만듬

레이아웃 트리는 x, y 좌표, 박스 영역(bounding box) 크기와 같은 정보를 가지고 있음

레이아웃 트리는 DOM 트리와 비슷한 구조일 수 있지만 웹 페이지에 보이는 요소에 관련된 정보만 가지고 있음

 

- display: none 속성이 적용된 요소는 레이아웃 트리에 포함되지 않음(visibility: hidden 속성이 적용된 요소는 포함)

이와 비슷하게 p::before{content:"Hi!} 속성과 같은 의사 클래스(pseudo class = 가상 클래스)의 콘텐츠는 DOM에는 포함되지 않지만 레이아웃 트리에는 포함

계산된 스타일이 있는 DOM 트리를 돌며 레이아웃 트리를 생성하는 메인 스레드

 

웹 페이지의 레이아웃을 결정하는 것은 어려운 작업

 

줄 바꿈이 변해 이동하는 단락의 박스 레이아웃

 

가장 단순하게 위에서 아래로 펼쳐지는 블록 영역 하나만 있는 웹 페이지의 레이아웃을 결정할 때에도 폰트의 크기가 얼마이고 줄 바꿈을 어디서 해야 하는지 고려해야 함(단락의 크기와 모양이 바뀔 수 있고, 단락의 위치에 영향이 있기 때문)

CSS는 요소를 한쪽으로 흐르게(float) 하거나, 크기를 벗어난 부분을 보이지 않게 하거나, 글이 쓰이는 방향을 변경할 수 있음 

 

▶ 페인트

어느 도형을 먼저 그려야할지 알수 없음

 

DOM, 스타일, 레이아웃을 가지고도 여전히 페이지를 렌더링할 수 없음

요소의 크기, 모양, 위치를 알더라도 어떤 순서로 그려야 할지 판단해야 함

z-index 속성이 적용되었다면 HTML에 작성된 순서로 요소를 그리면 잘못 렌더링된 화면이 나옴
즉, DOM에 선언된 노드 순서와 페인트 순서는 많이 다를 수 있음

 

레이아웃 트리를 순회하며 페인트 기록을 생성하는 메인 스레드

 


페인트 단계에서 메인 스레드 페인트 기록(paint record)을 생성하기 위해 레이아웃 트리를 순회

페인트 기록은 '배경 먼저, 다음은 텍스트, 그리고 직사각형'과 같이 페인팅 과정을 기록한 것


렌더링 파이프라인을 갱신하는 데는 많은 비용이 든다

DOM 트리 및 스타일, 레이아웃 트리, 페인트 트리의 순서로 생성

 

렌더링 파이프라인에서 파악해야 할 가장 중요한 점은 각 단계에서 이전 작업의 결과가 새 데이터를 만드는 데 사용되는 것

예를 들어 레이아웃 트리에서 변경이 생겨 문서의 일부가 영향을 받으면 페인팅 순서도 새로 생성해야 함

 

애니메이션 타임라인에서 애니메이션 프레임과 JavaScript 때문에 렌더링이 막힌 프레임


요소에 애니메이션을 적용하면 브라우저는 모든 프레임 사이에서 이러한 작업을 해야 함

대부분의 디스플레이 장치는 화면을 초당 60번 새로 고침(60fps)

요소의 움직임이 모든 프레임에 반영되어야 사람이 볼 때 부드럽게 느껴짐

애니메이션에서 프레임이 누락되면 웹 페이지가 '버벅대는(janky)' 것처럼 보임

애니메이션 타임라인에서 애니메이션 프레임과 JavaScript 때문에 렌더링이 막힌 프레임

 

화면 주사율에 맞추어 렌더링 작업이 이루어져도 이 작업은 메인 스레드에서 실행되기 때문에 애플리케이션이 JavaScript를 실행하는 동안 렌더링이 막힐 수 있음

 

애니메이션 프레임에 맞춰 애니메이션 타임라인에서 실행되는 더 작은 JavaScript 덩어리


JavaScript 작업을 작은 덩어리로 나누고 requestAnimationFrame() 메서드를 사용해 프레임마다 실행하도록 스케줄을 관리할 수 있음

 

Reflow와 Repaint 최소화하여 브라우저 렌더링 최적화하기

렌더링이 반복되는 경우

  1. 자바스크립트에 의한 노드 추가 또는 삭제
  2. 브라우저 창의 리사이징에 의한 뷰포트 크기 변경
  3. HTML 요소의 레이아웃에 변경을 발생시키는 스타일 변경

Layout 계산과 Paint 과정이 재차 실행되어 리렌더링이 발생

Layout 계산을 다시 수행하는 것을 Reflow, 픽셀을 렌더링하는 Paint 작업을 다시 하는 것을 Repaint라고 함

여기서 주의해야할 점은 Reflow가 발생하면 Repaint까지 발생하게 되므로 큰 비용이 발생한다는 점

만약 발생시킨다면 paint부터 시작될 수 있도록 Repaint만 발생하게 되는것이 훨씬 비용적 이점이 있음

 

Reflow와 Repaint 최소화

  • 영향받는 노드 최소화하기 (position fixed, absolute)
    • 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 주게 됨
    • 따라서 다른 엘리먼트 레이아웃에 영향을 주지 않는 fixed와 absolute 속성을 사용하면 비용을 줄일 수 있음
  • 숨겨진 엘리먼트 수정
    • display: none의 경우 Reflow와 Repaint가 발생하지 않음
    • 많은 수의 엘리먼트를 변경해야 할 경우, 숨겨진 상태에서 변경하고 다시 보이도록 display: none 속성을 설정하여 레이아웃 발생을 최대한 줄일 수 있음
    • 주의: visibility: hidden의 경우 보이지 않고 존재는 하기 때문에 layout 발생
  • transform, opacity 속성 사용
    • 두 속성은 reflow와 repaint가 일어나지 않는 속성
    • left, right 대신 transform 속성을 사용
    • visibility, display 대신 opacity 속성을 사용하면 reflow, repaint 발생을 최소화 할 수 있음
  • 애니메이션 최적화
    • 한 프레임의 처리는 16ms(60fps) 내로 완료되어야 끊기는 현상 없이 자연스럽게 처리
    • 자바스크립트에서 setTimeout을 이용하여 애니메이션을 구현한다면 이벤트 루프에 의해 딜레이가 생길 수 있고, 16ms 안에 실행하지 못 한다면 해당 프레임은 유실됨
    • 이를 도와주는 것이 requestAnimationFrame() 메서드(리페인트 바로 전에 브라우저가 애니메이션을 업데이트 할 지정된 함수를 호출하도록 요청함 즉, 리페인트 이전에 호출할 인수로 콜백을 받)
      • 장점: 지연 및 블로킹 없이 일정한 간격으로 애니메이션을 수행할 수 있음
      • 브라우저 프레임 속도(60fps)에 맞춰 애니메이션을 실행할 수 있도록 함
      • 페이지가 비활성 상태일 때 렌더링을 중지(CPU의 리소스와 배터리 수명을 낭비하지 않아도 됨)
        • 이와 달리 setTimeout은 백그라운드 상태일 때도 계속 실행됨

▶ 합성

페이지는 어떻게 그려질까

가장 단순한 래스터화 과정

 

정보(브라우저는 문서의 구조와 각 요소의 스타일, 요소의 기하학적 속성, 페인트 순서)를 화면의 픽셀로 변환하는 작업을 래스터화(rasterizing)라고 함

가장 단순한 래스터화는 아마 뷰포트 안쪽을 래스터하는 것일 것

사용자가 웹 페이지를 스크롤하면 이미 래스터화한 프레임을 움직이고 나머지 빈 부분을 추가로 래스터화

이 방식은 Chrome이 처음 출시되었을 때 래스터화한 방식이지만 현재는 브라우저는 합성(compositing)이라는 보다 정교한 과정을 거침(렌더링 파이프라인에서는 이단계부터 GPU가 많이 사용됨)

합성이란 무엇인가

합성과정


합성은 웹 페이지의 각 부분을 레이어로 분리해 별도로 래스터화하고 컴포지터 스레드(compositor thread)라고 하는 별도의 스레드에서 웹 페이지로 합성하는 기술

스크롤되었을 때 레이어는 이미 래스터화되어 있으므로 새 프레임을 합성하기만 하면 됨

애니메이션 역시 레이어를 움직이고 합성하는 방식으로 만들 수 있음

여러 레이어로 나누기

레이어 트리를 만들면서 레이아웃 트리를 순회하는 메인 스레드

 

어떤 요소가 어떤 레이어에 있어야 하는지 확인하기 위해 메인 스레드 레이아웃 트리를 순회하며 레이어 트리를 만듬

뷰포트로 미끄러져 들어오는 들어오는 슬라이드인 메뉴처럼 별도의 레이어여야 하는 웹 페이지의 어떤 부분이 별도의 레이어가 아니라면 CSS의 will-change 속성을 사용해 브라우저가 레이어를 생성하게 힌트를 줄 수 있음

모든 요소에 레이어를 할당하면 좋을 것 같지만 수많은 레이어를 합성하는 작업은 웹 페이지의 작은 부분을 매 프레임마다 새로 래스터화하는 작업보다 더 오래 걸릴 수 있음(확인하기 위해 애플리케이션의 렌더링 성능은 직접 측정해 봐야 함)

레이어가 많으면 합성 비용이 높을 뿐만 아니라 레이어를 메모리에 가지고 있어야 하는 부담도 있음(Chrome은 레이어가 과도하게 많아지는 것(layer explosion)을 막기 위해 특정한 경우에는 레이어를 생성하지 않거나 합치기도 함)

메인 스레드 이후 래스터화와 합성

타일 단위로 비트맵을 만들고 GPU로 보내는 래스터 스레드

 

레이어 트리가 생성되고 페인트 순서가 결정되면 메인 스레드가 해당 정보를 컴포지터 스레드에 넘김(commit)

그러면 컴포지터 스레드는 각 레이어를 래스터화

어떤 레이어는 페이지의 전체 길이만큼 클 수 있음

그래서 컴포지터 스레드 레이어를 타일(tile) 형태로 나눠 각 타일을 래스터 스레드로 보냄

래스터 스레드 각 타일을 래스터화해 GPU 메모리에 저장

컴포지터 스레드 래스터 스레드간의 우선순위를 지정할 수 있어서 뷰포트 안이나 근처의 것들이 먼저 래스터화될 수  있음 또한 레이어는 줌인 같은 동작을 처리하기 위해 여러 해상도별로 타일 세트를 여러 벌 가지고 있음(타일 세트의 모든 타일이 래스터화되어 있지는 않기 때문에 최대한 구멍을 메울 수 있는 방식으로 여러 타일 세트의 타일을 조합해서 사용. 래스터화하지 못한 영역은 체스판의 하얀색 부분처럼 빈곳으로 둠 → 이 단계의 큰 목표는 이 하얀 빈곳을 줄이는 것)

타일이 래스터화되면 컴포지터 스레드 '합성 프레임'을 생성하기 위해 타일의 정보를 모음

이 타일의 정보를 '드로 쿼드(draw quads)'라고 부름

  • 드로 쿼드: 메모리 타일의 위치와 웹 페이지 합성을 고려해 타일을 웹 페이지의 어디에 그려야 하는지에 관한 정보를 가지고 있음
  • 합성 프레임: 웹 페이지의 프레임을 나타내는 드로 쿼드의 모음


이후에 합성 프레임이 IPC를 통해 브라우저 프로세스로 전송

이 시점에 브라우저 UI의 변경을 반영하려는 UI 스레드나 확장 앱을 위한 다른 렌더러 프로세스에 의해 합성 프레임이 더 추가될 수 있음 이러한 합성 프레임은 GPU로 전송되어 화면에 표시

스크롤 이벤트가 발생하면 컴포지터 스레드 GPU로 보낼 다른 합성 프레임을 만듬

- 기술글(2019년 작성)에서는 ' 합성 프레임이 브라우저 프로세스를 거치지 않고 GPU 프로세스로 바로 보내지는 형태로 변경될 예정'이라고 되어있었는데 검색으로 해보니 현재 크롬의 합성 아키텍처는 성능과 효율성을 개선하기 위해 많은 변화가 있었다고 함 주요 목표는 중간 단계를 거치지 않고 직접 GPU 프로세스로 합성하는 것 따라서 현재는 합성 단계가 많이 다를꺼라고 생각함 찾아보니 이 2023년 작성된 블로그글을 확인해보니 '프로세스를 가속화하기 위해 래스터 스레드는 IPC를 통해 타일을 GPU 프로세스로 보낸다'고 되어있음 좀더 현재에 가까운 동작방법을 알기 위해서 이 글을 참조하며 이해하는것이 훨씬 좋아 보임

합성의 이점 메인 스레드와 별개로 작동할 수 있다는 점

컴포지터 스레드 JavaScript 실행이나 스타일 계산을 기다리지 않아도 됨

이것이 합성만 하는 애니메이션이 성능상 가장 부드럽다고 보는 이유

레이아웃이나 페인트를 다시 계산해야 할 경우에는 메인 스레드가 관여해야 함