[웹] 크롬 브라우저 톺아보기

moonee·2021년 9월 10일
0

목록 보기
2/4

해당 포스팅은 Mariko Kosaka 의 Inside look at modern web browser 시리즈 를 정리 하였음을 밝힙니다. 사용된 사진의 출처도 해당 포스팅입니다.
중간에 컴포지터 관련 내용에서 컴포지터(compositor) 전용 속성 고수 및 레이어 수 관리 포스팅도 참고하였습니다.

📍 CPU와 GPU

CPU (Central Processing Unit)

컴퓨터에서 CPU 는 컴퓨터 시스템을 통제하고, 프로그램 연산을 실행하고 처리하는 가장 핵심적인 컴퓨터의 제어 장치이다.
CPU 내부에서 CPU 코어가 핵심적인 작업 처리를 담당하며, 최신 하드웨어는 여러개의 CPU 코어 (멀티코어) 를 사용하여 실행 성능을 높인다.

GPU (Grapics Processing Unit)

컴퓨터에서 GPU는 그래픽 연산 처리를 담당하며, 결과값을 모니터에 출력하는 역할을 한다. CPU와 달리 GPU는 간단한 작업에만 특화 되어있지만, GPU 코어가 동시에 여러 작업을 수행 할 수 있다.

컴퓨터 아키텍쳐 레이어

💡 컴퓨터나 스마트폰에서 사용자가 어플리케이션을 시작하면 CPU와 GPU가 어플리케이션을 실행한다.
이 때 하단부 레이어에 존재하는 CPU와 GPU는 중앙부의 운영체제의 명령에 따라서 동작한다.



📍프로세스와 쓰레드의 실행

  • 프로세스 : 어플리케이션의 실행 프로그램
  • 쓰레드 : 프로세스 내부에서 프로세스의 프로그램을 실행하는 주체

어플리케이션 시작 시

  1. 프로세스 생성
  2. (Optional) 프로그램은 작업을 위해 쓰레드 생성
  3. 운영체제는 프로세스에게 메모리 한 조각을 주어, 어플리케이션의 모든 상태정보를 고유 메모리 공간에 저장할 수 있도록 한다.

    어플리케이션 종료 시, 프로세스도 사라지고 운영체제도 프로세스에게 부여했던 메모리 공간 해제


프로세스는 필요 시, 또 다른 프로세스를 생성하여 작업을 수행하도록 운영체제에 요청 할 수도 있다. 이 때 만약 서로 다른 프로세스 간 통신이 필요하다면 IPC를 이용하게 된다.

많은 어플리케이션이 이러한 방식을 채택하여, 특정 워커 프로세스가 무응답 상태에 빠져도, 다른 프로세스들을 모두 종료시킬 필요 없이 해당 프로세스만 종료 시킬 수 있도록 구현되었다.

크롬과 IE의 차이점

  • 크롬은 멀티 프로세스 브라우저이다. 따라서 하나의 탭 = 하나의 프로세스이며, 탭 하나가 무응답 상태에 빠져도 다른 탭을 종료 할 필요가 없다.

  • 인터넷 익스플로어는 멀티 쓰레드 브라우저로서 하나의 프로세스 내부에서 여러개의 쓰레드를 생성하는 방식이다. 따라서, IE 사용 시 하나의 탭이 무응답 상태에 빠지면 실행 중인 모든 탭이 무응답 상태에 빠지게 된다.



📍 크롬 브라우저 아키텍쳐

1. 브라우저 프로세스

  • 최상위의 브라우저 프로세스는 어플리케이션의 다른 부분을 담당하는 프로세스들을 조율하는 역할
  • 주소 창 / 뒤로가기 - 앞으로가기 이동 버튼을 포함한 어플리케이션의 "크롬" 부분을 제어
  • 네트워크 요청 / 파일 액세스와 같은 웹 브라우저의 권한이 부여된 보이지 않는 부분을 제어

2. 렌더 프로세스

  • 다수의 프로세스가 생성되어 각 탭마다 할당
  • 웹사이트가 디스플레이 될 때 탭 안의 모든 것을 담당

3. 플러그인

  • 플래시와 같은 웹사이트가 사용하는 모든 플러그인 담당

4. GPU

  • 다른 프로세스와 분리된 GPU 작업 제어
  • GPU는 여러 앱의 요청을 제어하고 동일한 표면에 표시하기 때문에 다른 프로세스로 분리

멀티 프로세스 장점

크롬은 각 탭별로 다른 렌더러 프로세스를 사용한다. 이는 아래와 같은 장점을 가진다.
1. 한 탭이 무응답 상태가 되어도 다른 탭에 영향을 주지 않는다.
2. 보안 및 샌드박싱

  • 크롬 브라우저는 렌더러 프로세스와 같은 임의의 사용자 입력을 처리하는 프로세스에 대한 임의의 파일 액세스를 제한한다.

메모리 문제 해결 방법

하나의 프로세스는 개별 메모리 공간을 소유한다. 따라서, 각 프로세스들은 서로의 메모리 공간을 공유할 수 없으므로 '공통 인프라스트럭쳐'라는 보통 복사본을 가지고 있다.
이 때, 프로세스가 많아질 수록 메모리 사용량이 급격히 상승하게 되므로 크롬 브라우저는 생성 가능한 프로세스 개수에 제한을 두었고, 만약 프로세스의 갯수가 한계치를 넘게 되면, 크롬은 한 프로세스에서 동일한 사이트를 오픈하는 여러 탭들을 실행하기 시작한다.



📍 탐색 시 일어나는 일

탐색(Navigation) 이란 사용자의 요청을 받아 브라우저가 페이지를 렌더링하는 과정

브라우저 프로세스 내부 스레드

  • UI 스레드 : 버튼 / 입력창을 그리는 역할
  • 네트워크 스레드 : 인터넷에서 데이터를 수신하기 위해 통신 스택을 건드리는 역할
  • 스토리지 스레드 : 파일 같은 것들에 접근하는 역할

1. 입력 처리

  • 사용자가 주소창에 입력 시 UI스레드는 우선 검색어인지 URL인지 판단
  • 검색어일 경우, 입력 문구를 파싱해서 검색엔진에 송신
  • URL일 경우 요청한 페이지로 연결

2. 탐색 시작

  • 사용자가 엔터를 치면 UI 스레드가 사이트 컨텐츠를 받기 위해 네트워크 요청 초기화

3. 응답 읽기

  • 응답 바디 (payload)가 들어올 때, 필요시 네트워크 스레드가 스트림의 첫 몇 바이트를 확인
  • 응답의 Content-Type 헤더가 데이터 타입을 알려주지만, 만약의 상황을 대비하여 MIME Type 스니핑 수행
  • 응답이 HTML 파일 -> 다음 단계로 렌더러 프로세스에 데이터 전달
  • 응답이 zip 혹은 다른 형식의 파일 -> 다운로드 메니저에게 데이터 전달

  • 안전 브라우징 체크 수행 -> 도메인과 응답 데이터가 이미 알려진 악성 사이트와 일치한다면 네트워크 스레드는 warning 페이지를 보여주어 경고
  • Cross Origin Read Blocking 체크 -> 민감한 cross-site 데이터가 렌더러 프로세스에 도달하지 못하게 함

4. 렌더러 프로세스 찾기

  • 모든 확인 작업이 끝나고, 네트워크 스레드가 브라우저가 요청된 사이트로 이동해야함을 확신하면 네트워크 스레드는 UI스레드에게 데이터가 준비되었다고 알려줌
  • UI 스레드는 웹 페이지를 렌더링을 담당 할 렌더러 프로세스를 찾음

    네트워크 요청 - 응답 과정 최적화를 위해 렌더러 프로세스는 이미 데이터 수신 시 대기하고 있는 상태이다. 만약 탐색 도중 cross-site로 리다이렉트한다면 준비된 프로세스는 사용되지 않게 된다.

5. 탐색 수행


데이터와 렌더러프로세스 준비 완료

  • 브라우저 프로세스에서 렌더러 프로세스로 IPC 전송 (탐색을 커밋하기 위해서 )
  • 브라우저 프로세스에서 렌더러 프로세스로 데이터 스트림 전송 -> 렌더러 프로세스가 HTML 데이터를 계속받을 수 있도록 해줌
  • 렌더러 프로세스에서 커밋이 확인되면 브라우저 프로세스는 탐색을 완료하고, 문서 로딩 단계 시작

    이 시점에서 주소창 갱신 / 보안 알리미와 사이트 설정 UI 가 새 페이지 사이트 정보를 반영한다.
    따라서 탭의 세션 이력이 갱신되어 뒤로/앞으로 가기 버튼에 방금 방문한 사이트가 추가되고 탭/세션 복구 기능을 위해 탭이나 윈도우를 닫을 때 세션 이력이 저장된다.

6. 초기 로딩 완료

  • 탐색이 커밋완료 후, 렌더러 프로세스가 리소스 로딩과 페이지 렌더를 지속
  • 렌더러 프로세스가 렌더링을 끝내면 브라우저 프로세스에 IPC를 반환 (onload 이벤트가 페이지의 모든 프레임에서 발생하고, 실행까지 완료된 후 ) -> UI 스레드는 탭의 로딩 스피너 정지

다른 사이트 탐색 시 - beforeunload 이벤트 확인

beforeunload 이벤트란?
문서와 리소스가 언로드 되기 직전에 window에서 발생하여, 이를 통해 사용자가 페이지를 떠날 때, 정말로 떠날 것인지 묻는 대화 상자를 표시 할 수 있다.
사용자가 확인을 누를 경우 브라우저는 새로운 페이지를 탐색하고, 취소할 경우 탐색을 취소한다.

  • 사용자가 다른 URL을 다시 입력하면, 다른 사이트 탐색 전에 현재 렌더링된 사이트가 beforeunload 이벤트를 처리하는지 확인한다.
  • 렌더러 프로세스가 탐색 과정을 초기화하면 렌더러 프로세스는 우선 beforeunload 핸들러를 체크한다.

  • 브라우저 프로세스는 새 렌더러 프로세스에게 페이지 렌더링을 요청하고, 이전 렌더러 프로세스에게는 페이지를 unload 하도록 요청한다.

서비스 워커 (Service Worker)

서비스 워커란 ?
앱 코드에 네트워크 프록시를 작성 할 수 있는 수단으로, 웹 개발자로 하여금 로컬에 캐시할 데이터와 네트워크로부터 받아올 데이터를 컨트롤 할 권한을 가지게 해준다.
서비스 워커가 페이지를 캐시에서 로드하도록 세팅된다면, 네트워크에서 데이터를 받아올 필요가 없다.

  • 서비스 워커는 렌더러 프로세스에서 돌아가는 자바스크립트 코드이다.
  • 서비스 워커가 등록되면, 서비스 워커 스코프가 레퍼런스로 취급된다.
  • 탐색 시작 시, 네트워크 스레드는 등록된 서비스 워커 스코프와 도메인을 비교하여 동일한 URL에 서비스 워커가 등록되어 있는지 확인한다.

  • 서비스 워커가 등록되어 있다면, UI스레드가 해당 서비스 워커 코드를 실행하기 위해 렌더러 프로세스를 찾는다.
  • 이 후, 렌더러 프로세스의 워커 스레드가 네트워크 스레드에게 데이터를 요청한다.

선제 탐색 (Navigation Preload)

  • 서비스 워커의 시작과 동시에 리소스들을 병행 로딩하여 브라우저 프로세스와 렌더러 프로세스 간 네트워크 데이터 요청 과정의 딜레이를 줄여주는 메커니즘
  • 이런 요청들에 헤더를 표기하여 서버가 다른 콘텐츠를 보낼 지 결정하게 한다.


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

  • 브라우저 탭 안에서 일어나는 모든 일 담당
  • 메인 스레드가 대부분의 코드 처리 (웹 워커 혹은 서비스 워커 사용 시, 워커 스레드가 자바스크립트 코드 일부분 처리)
  • 컴포지터와 레지스터 스레드는 렌더러 프로세스 내부에서 페이지를 효율적이고 매끄럽게 렌더하기 위해 실행

1. 파싱

DOM 생성

  • 렌더러 프로세스가 탐색을 위한 커밋 메시지를 받고 HTML 데이터를 받기 시작 할 때, 메인 스레드는 텍스트 문자열 (HTML) 을 파싱하기 시작하고 이를 DOM (Document Object Model)로 변환

    DOM 이란?
    페이지에 대한 브라우저의 내부 표현 / 웹 개발자가 자바스크립트를 통해 웹 페이지와 상호작용 할 수 있는 데이터 구조 및 API

추가 리소스 로딩

  • 이미지, CSS, 자바스크립트와 같은 외부 리소스를 네트워크 혹은 캐쉬로부터 로드
  • 사전 로드 스캐너 실행 : 메인 스레드는 DOM을 구성하기위한 파싱 도중에 리소스들을 찾을 때마다 요청 할 수 있지만, 속도를 높이기 위해 사전 로드 스캐너가 동시에 실행된다. HTML 문서에 <img> 혹은 <link>가 있을 경우 사전 로드 스캐너는 HTML 파서에 의해 생성된 토큰들을 보고 프로세스에 있는 네트워크 스레드에게 요청 전송

HTML 파서가 script 태그 발견 시, 문서 파싱 과정을 중단하는 이유
자바스크립트는 전체 DOM 를 바꾸는 연산을 할 수 있기 때문이다.

브라우저에 어떻게 리소스를 로드할지 알려주는 법
자바스크립트 코드가 전체 DOM 구조를 바꿀 일이 없다면 async 나 defer attribute를 script 태그에 추가하여, 브라우저가 자바스크립트 코드를 비동기적으로 로드 / 실행하도록 할 수 있다.

스타일 계산

  • 메인스레드는 CSS를 파싱하여 각 DOM 노드에 대한 계산된 스타일 결정

2.레이아웃

  • 요소들의 기하학적 구조를 찾는 과정
  • 메인 스레드는 DOM과 계산된 스타일을 통해 x,y좌표 및 bounding box의 크기와 같은 정보를 갖는 레이아웃 트리 생성

    요소에 display:none이 적용되면 해당 요소는 레이아웃 트리에 포함되지 않는다.
    반면 visibility:hidden이나, 유사 클래스가 있는 컨텐츠는 DOM에 없음에도 레이아웃 트리에 포함된다.

3. 페인트

  • 메인 스레드는 레이아웃 트리를 따라가며 페인트 기록을 생성한다. (HTML 마크업 순서로 페인팅 할 경우 제대로 렌더되지 못하기 때문)

🔴 렌더링 파이프라인 업데이트

  • DOM + 스타일 -> 레이아웃 -> 페인트
  • 렌더링 파이프라인 생성은 각 단계에서 그 전 단계 실행 결과물이 새로운 데이터를 생성하는데 쓰인다. 따라서 만약 레이아웃 트리에서 무언가 변하면 문서에 영향 받은 부분에 대하여 페인트하는 순서가 갱신된다.

애니메이션

  • 요소 애니메이션 경우 브라우저는 렌더링 파이프라인 생성을 매 프레임마다 실행해야 한다. 대부분 디스플레이는 매 초당 60번 (60fps)스크린을 새로고침한다. (그래야 애니메이션이 사람 눈에 매끄러워 보인다)

  • 만약 애니메이션이 중간 프레임을 손실하는 경우, 페이지가 버벅이게 된다.

  • 렌더링 연산은 메인 스레드에서 실행되기 때문에 이는 어플리케이션이 자바스크립트 실행을 방해 할 수 있다.

  • 이를 방지하기 위해서 자바스크립트 동작을 작은 단위로 나눌 수 있고, requestAnimationFrame()을 이용하여 매 프레임마다 실행하는 것을 미리 설정 할 수 있다.

4. 컴포지팅

레스터라이징

레스터라이징이란 , 문서의 구조, 각 요소의 스타일, 페이지의 기하학 구조, 페인트 순서와 같은 정보를 스크린의 픽셀로 바꾸는 것이다.
따라서 이전에는 화면에 보이는 영역을 레스터하여 사용자가 페이지를 스크롤하면 레스터된 프레임을 움직이고, 더 레스터링을 하여 부족한 부분을 메꾸는 방식을 사용했다. 하지만 현재 크롬은 '컴포지팅' 기법을 사용한다.

  • 컴포지팅 : 한 페이지의 부분들을 여러 레이어로 나누고, 그 것들을 각각 레스터하며, 컴포지터 스레드에서 페이지를 합성하는 기술
  • 스크롤 발생 시, 레이어들이 이미 레스터 되었기 때문 새로운 프레임을 합성해주는 작업만 하면 된다.
  • 컴포지터 스레드는 메인 스레드의 레이어 트리를 복사하여 별도로 갖고 있다.

🔵 컴포지션을 통해 성능 최적화 하기

  • 레이아웃과 페인트를 피하고 컴포지팅 (합성) 변경만 요구하는 픽셀 파이프라인 버전이 당연히 성능적으로 좋다.
  • 이를 위해 컴포지터가 단독으로 처리 할 수 있는 변경 속성을 사용해야한다.
  • 주의점은 이러한 속성을 변경하는 요소가 자체 컴포지터 레이어에 있어야한다는 것이다. 요소 자체를 레이어로 승격하는 방법은 아래와 같다.
.element {
	will-change : transform;
}

// or...

.element {
	transform: translateZ(0);
}

그렇다고해서 모든 요소를 레이어로 승격하면 메모리가 초과될 수 있으므로, 꼭 필요할 때만(애니메이션이 큰 비용을 차지할 때) 승격해야한다.



📍 입력 이벤트

브라우저 관점에서 입력 이벤트

  • 입력 이벤트 (input event)란 사용자의 모든 제스쳐를 말한다. 스크롤부터 화면 터치, 마우스 포인터를 화면위에 올리는 것 까지 입력 이벤트이다.
  1. 입력 이벤트가 발생 했을 때 가장 먼저 수신하는 것은 브라우저 프로세스이다. 브라우저 프로세스는 제스쳐가 어디에서 발생했는지만 알고 있고, 탭 내부의 콘텐츠는 렌더러 프로세스에서 처리한다.
  2. 브라우저 프로세스는 이벤트 유형( 예) click , scroll ) 과 이벤트가 발생한 조표를 렌더러 프로세스에 전송한다.
  3. 렌더러 프로세스는 이벤트 대상을 찾고, 해당 대상과 연결된 이벤트 리스너를 실행하여 이벤트를 적절히 처리한다.

컴포지터와 입력이벤트

  • 웹 페이지에 이벤트 리스너가 연결되어 있지 않다면, 컴포지터 스레드는 메인 스레드와 상고나 없이 새로운 합성 프레임을 만들 수 있다. 하지만 이벤트 리 스너가 웹 페이지 연결되어있다면 어떨까? 이벤트를 처리해야 하는지 컴포지터 스레드는 '고속 스크롤 불가 영역'을 통해 알 수 있다.

고속 스크롤 불가 영역

  • 자바스크립트 코드 실행은 메인 스레드의 작업이므로 웹페이지가 합성 될 때 컴포지터 스레드는 이벤트 핸들러가 연결된 영역을 고속 스크롤 불가 영역(non-fast scrollable region)으로 표시한다.
  • 웹페이지는 이 영역에서 이벤트가 발생 했을 때, 컴포지터 스레드가 입력 이벤트를 메인 스레드로 보내야하는지 이 정보로 확인 할 수 있다.
  • 입력 이벤트가 고속 스크롤 불가 영역 밖에서 발생했다면 컴포지터 스레더는 메인 스레드를 기다리지 않고 새 프레임을 합성한다.

🔴 이벤트 핸들러 작성시 주의

document.body.addEventListener('touchstart',event=>{
  	if(event.target=== area){
    	event.preventDefault();
    }
});
  • 위와 같은 이벤트 위임 패턴은 모든 요소에 대해 이벤트 핸들러를 하나만 작성하면 되므로, 편해보인다. 하지만 위와 같은 코드의 문제점은 웹 페이지의 모든 영역이 고속 스크롤 불가영역으로 표시된다는 것이다.
  • 즉, 어플리케이션이 신경쓰지 않는 부분에 입력이 들어와도 컴포지터 스레드는 입력 이벤트가 들어올 때 마다 메인스레드와 통신해야하고, 메인 스레드가 일을 끝내기를 기다려야한다.
document.body.addEventListener('touchstart',event=>{
  	if(event.target=== area){
    	event.preventDefault();
    }
}, {passive:true});
  • 이러한 문제를 방지하기 위해 이벤트 리스너에 passive: true 옵션을 전달 할 수 있다.
  • 이는 메인 스레드에서 이벤트를 받지만 컴포지터가 메인 스레드의 처리를 기다리지 않고 새 프레임을 만들어도 된다는 힌트를 브라우저에 준다.

이벤트 대상 찾기

  • 컴포지터 스레드가 입력 이벤트를 메인 스레드로 보낼 때, 가장 먼저 하는 일은 이벤트 대상을 찾는 hit test 이다.
  • 이벤트가 발생한 좌표에 무엇이 있는지 확인하기 위해 hit test는 렌더링 프로세스에서 생성된 페인트 기록의 데이터를 사용한다.

hit test 에서 레이아웃 트리가 아닌 페인트 트리를 사용하는 것은 clip, opacity, transform등의 속성을 반영해야 이벤트 대상을 정확히 알 수 있기 때문

메인 스레드로 이벤트 전송 최소화

  • 화면 갱신은 초당 60회이고, 일반적 터치크스킬ㄴ 장치는 터치 이벤트를 초당 60~120회 전달한다.
  • touchmouse 이벤트 처럼 연속적인 이벤트가 초당 120회씩 메인 스레드로 보내지면, 화면이 갱신되는 정도보다 더 많이 hit test를 하거나 자바스크립트를 실행하게 된다. 즉, 입력 이벤트의 전달 주기가 화면 갱신 주기보다 짧아지게 된다.

  • 메인 스레드의 호출이 과도해지는 것을 막기위해 크롬은 연속적인 이벤트를 합쳐서 바로 다음번 requestAnimationFrame()메서드 실행 직전까지 전송하지 않고 기다린다.
  • 비연속적인 이벤트는 즉시 전달된다.
profile
기록

0개의 댓글