Published on

[성능 최적화] 브라우저 렌더링 과정을 제대로 이해하기

Authors
  • avatar
    Name
    이주영
    Twitter

들어가기 앞서

해당 블로그는 돈워리 프로젝트의 LCP를 단축하며 궁금했던 브라우저 렌더링에 대해 정리하였습니다. 성능을 최적화하려고 하는데 HTML이 브라우저에 그려지는 과정을 모르고 진행하긴 어렵다고 판단하여 자세하게 정리해보았습니다.

"Critical Rendering Path에 대해 설명해 주세요"라는 질문을 받는다면 어떻게 답하실 건가요?!

"DOM이 만들어지고... CSSOM이 만들어진 후, 기반으로 Render Tree가 만들어집니다. 이후 Paint 과정이 일어나서 브라우저에 표시됩니다."

깊이 있는 설명을 할 수 있으면 좋겠다는 생각으로 이번 포스트를 통해 "Critical Rendering Path" (이하 CRP라고 작성) 대해 더욱 상세하게 정리해보려고 합니다.

본론

CRP의 최상위 스코프인 CS... (사전 지식)

크롬 브라우저를 살펴보려고 합니다. html 문서가 화면이 보이기까지는 여러 단계가 존재합니다 시작하기 앞서 기본적인 CS 지식을 정리하고 크롬 브라우저 아키텍처를 구성하는 프로세스를 정리한 후 CRP에 대해 깊게 정리해보려고 합니다.

CS 기본을 간단하게 알아보자 (번외 학습)

기본적으로 하드웨어와 소프트웨어로 나누어 생각해볼 수 있습니다. 하나씩 살펴보시죠.

하드 웨어 구성요소

  • CPU (Central Processing Unit)
    • Core : 각종 연산을 수행하는 핵심 요소, Thread 단위로 Core 단위에 맵핑
    • Cache memory
    • Controller
  • Memory
    • ROM : Read Only Memory
    • RAM : Random Access Memory
  • Contorl Unit
  • GPU

소프트 웨어

  • Program : 실행가능한 파일

    • 기본 프로그램
      • OS : 운영체제 (HardWare 를 제어하고 응용 프로그램을 실행하는 기본 프로그램)
    • 응용 프로그램
      • 브라우저
  • Process : Program이 OS에 의해 memory 영역을 할당받고 실행중인 상태인 것 - Code : PC(다음번에 실행될 명령어의 주소를 갖고 있는 레지스터, 코드 저장) - Data : 전역 변수와 정적 변수 저장 - Heap : 메모리 관리 그리고 동적 메모리 할당 Stack(Thread) : Process가 할당된 자원을 이용하는 실행의 단위, 임시 데이터 (로컬 변수, 및 return address) 저장

짧게 정리해보면

하드웨어에는 CPU, 메모리, 연산장치, 컨트롤 유닛과 GPU가 있습니다. CPU와 GPU는 특성이 조금 다릅니다. CPU는 복잡한 작업을 중앙에서 처리하는 유닛이며 GPU는 상대적으로 단순한 작업을 여러 유닛에서 병렬적으로 처리할 수 있도록 설계된 프로세싱 유닛입니다. 이미지를 살펴보시죠

위의 하드웨어 기반으로 OS가 실행합니다. OS는 기본 프로그램이며 여기서 프로그램이란 실행 가능한 파일을 말합니다. 프로그램에는 기본 프로그램과 응용 프로그램이 존재합니다. 만약 프로그램이 실행되면 OS가 함께 실행이 되는데 OS는 하드웨어를 제어하고 응용 프로그램을 실행할 수 있습니다. 그럼 응용 프로그램은 무엇인가요? 예를 들면 브라우저입니다. 그럼 프로그램이 실행되면 무엇이라고 부를까요?! 바로 프로세스라고 합니다. 프로세스는 OS가 프로그램을 실행한 결과물이기에 OS 안에서 생성이 됩니다. 프로세스 안에는 Code, Data, Stack, Heap이 존재합니다. 각 구성 요소들에 특징이 존재하지만 Stack을 살펴보면, Stack은 우리가 작성한 코드들이 실행되는 흐름입니다. 그 흐름을 스레드라고 부를 수 있습니다.

프로세스와 쓰레드는 다른 블로그로 자세히 찾아뵙겠습니다.

이렇게 프로세스는 OS에 의해 메모리를 할당 받은 실행중인 프로그램이라는 것과 스레드는 프로세스 안에서의 실행 흐름이라는 것을 알게 됐습니다.

초간단 브라우저 아키택쳐

위에서 아주 기본인 CS 지식을 살펴보았고 기본적으로 크롬 브라우저 아키텍쳐를 구성하는 프로세스는 다음과 같습니다.

브라우저주소 표시줄, 북마크, 뒤로 및 앞으로 버튼을 포함하여 애플리케이션의 'chrome' 부분을 제어합니다. 네트워크 요청, 파일 액세스 등 웹브라우저에서 보이지 않고 권한이 있는 부분도 처리합니다.
렌더러웹사이트가 표시되는 탭 내부의 모든 항목을 제어합니다.
플러그인웹사이트에서 사용하는 플러그인(예: 플래시)을 제어합니다.
GPUGPU 작업을 다른 프로세스와 분리하여 처리합니다. GPU가 여러 앱의 요청을 처리하고 동일한 노출 영역에 그리므로 다른 프로세스로 분리됩니다.

브라우저 프로세스, 렌더러 프로세스, 플러그인 프로세스, GPU 프로세스 총 4가지의 프로세스가 존재합니다. 프로세스 간에 통신은 IPC 방식으로 이루어진다고 하는데 이 부분은 해당 블로그에서 조금 벗어난 개념이라 다음에 다루겠습니다.

오늘 알아볼 CRP와 관련한 프로세스는 두가지 입니다.

첫번째, 브라우저 프로세스!
두번째, 렌더러 프로세스!

CRP와 관련있는 프로세스는 렌더러 프로세스이며 핵심 역할은 HTML,CSS,JS를 사용자와 상호작용할 수 있는 웹페이지로 보여주는 것입니다.

각 프로세스 안에서 여러 스레드, 즉 실행 흐름이 존재합니다. 브라우저 프로세스에서는 UI, Network, Storage 스레드가 있고 렌더러 프로세스에 4가지 스레드가 존재합니다. 메인 스레드, 워커 스레드, 컴포지터 스레드, 레스터 스레드. 브라우저 프로세스와 렌더러 프로세스가 합력하여 서버로부터 받은 HTML을 화면에 렌더링 하는 과정이 일어나는 것입니다. 이 과정을 살펴보시죠

Critical Rendering Path에 대해

정의 : CRP(Critical Rendering Path)는 HTML,CSS,JS를 브라우저 화면에 픽셀로 변화하는 일련의 단계를 말합니다.

스텝 별로 하나씩 알아봅시다.

  1. 파싱
    • DOM 생성
    • 하위 리소스 로드 (CSS, Image, font etc)
      • link 태그의 rel 속성들로 preload-scanner 활성화하여 최적화?!
    • JS 다운로드
      • 비동기 다운로드
  2. 스타일 계산
  3. 레이 아웃
  4. 페인트
  5. 합성
  6. 상호 작용

6가지의 단계로 구성할 수 있을 것 같습니다. DOM -> CSSOM -> Layout -> Paint 보다는 조금 더 자세히 알 수 있게 됐죠. 하나씩 살펴봅시다.

1. DOM (파싱 모델)

DOM은 뚝딱 만들어지는 것은 아닙니다. HTML을 파싱하여 결과적으로 DOM이 만들어지는데 HTML 파싱 프로세스를 살펴봐야합니다. 참고 : https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model

HTML 파싱 프로세스는 코드가 스트림 방식으로 내려받아지도록 설계되어 있습니다. 즉 토큰화 단계를 거쳐 트리 구성 단계를 거치고 최종 Document 객체로 출력됩니다.

스트림 방식이란?? 네트워크를 통해 받은 리소스를 작은 조각으로 나누어, Bit 단위로 처리하는 것을 말합니다. MDN 공식문서를 통해 확인해 보니 브라우저가 수신한 자원을 웹 페이지에 표현할 때 주로 사용하는 방법이라고 합니다. 참고 : https://developer.mozilla.org/ko/docs/Web/API/Streams_API

이미지로 보면 더욱 이해가 쉽습니다.

네트워크를 통해 HTML 문서를 받아오고 일련의 과정들이 진행되어 DOM으로 보여집니다. 받은 HTML 문자열을 일련의 과정을 통해 토큰화 되고 트리 구조체가 만들어지고 여기서 DOM이 됩니다. 이 과정은 우선 이미지로 익히고 넘어가겠습니다. 링크 블로그를 통해 자세히 살펴보실 수 있습니다.

웹사이트는 기본적으로 골격인 HTML로만 구성되어 있지 않습니다. 그렇기에 외부 리소스인 CSS와 JS를 사용합니다. 두가지의 파일은 주로 네트워크 혹은 캐시에서 로드하는데. 여기서 중요한 부분이 있습니다.

메인 스레드는 DOM(위의 이미지 과정)을 빌드하기 위해 파싱하는 동안 외부 리소스를 동기적으로 요청할 수 있지만 속도를 높이기 위해 **'미리 로드 스캐너'**가 동시에 실행된다고 합니다.

정리해보면 img 태그 혹은 link 태그는 미리 로드 스캐너가 HTML 파서에서 생성된 토큰을 살펴보고 브라우저 프로세스의 네트워크 스레드로 요청을 전송한다고 합니다. 아래에서 미리 로드 스캐너에 대해 알아봅시다.

script 태그를 만나면?

HTML 파서가 script 태그를 만나면 파싱을 차단할 수 있습니다. 하지만 리소스 로드 방식을 브라우저에게 잘 알려준다면 html 파싱 차단을 최소화할 수 있습니다. document.write() API를 사용하지 않으면 async 혹은 defer 속성을 script 태그에 추가하면 됩니다. HTML 파싱을 중단하지 않고 script를 통해 외부 리소스를 다운로드를 비동기적으로 실행할 수 있도록 합니다. async 속성 같은 경우 HTML 파싱과 병렬적으로 다운로드하고 완료된 JS부터 실행합니다. defer 속성은 마찬가지로 병렬적으로 다운로드하지만 HTML 파싱이 완료한 후 다운로드 순으로 실행하게 됩니다. 그러니 기존 DOM 노드에 의존성이 없는 script는 async 속성으로 의존성이 있고 순서가 중요한 script는 defer 속성으로 추가하는 것이 좋겠죠!!! 아하!

위에서 잠깐 본 프리 로드 스캐너는 무엇인가?!

위에서 살펴봤듯, 프리 로드 스캐너 CSS는 스타일링이 적용되지 않는 콘텐츠가 잠시 뜨는 현상을 방지하기 위해 렌더링이 차단됩니다. 그리고 자바스크립트의 경우는 script 태그의 속성 중 asyncdefer가 없을 경우 파서가 차단됩니다.

예를 들어 설명해 보면, 현재 렌더러 프로세스 내에서 매인 스레드에서는 DOM 트리를 만들고 있습니다. 그렇다면 CSS와 JS 그리고 폰트와 같이 우선순위가 높은 자원은 어떻게 처리되는 걸까요?! 바로 프리로드 스캐너 덕분에 외부 자원에 대한 참조를 DOM 트리와 별개로 요청할 수 있는 것입니다. 즉 해당 데이터를 뒤에서 미리 요청해 놓고 메인 스레드에서 진행하고 있는 HTML 파싱이 해당 자원을 담당하는 코드를 만나게 될 때 이미 가지고 온 응답을 즉시 활용하여 블로킹을 줄여주는 역할을 합니다.

참고 : MDN 공식 문서

스타일 계산

DOM을 기반으로 레이아웃 트리 생성 합니다.

스타일 계산은 만들어진 DOM과 CSSOM을 기반으로 레이 아웃 트리를 만드는 방식으로 이루어집니다. 레이 아웃 트리는 DOM 트리의 루트부터 시작해서 "화면에 존재하는 노드"를 순회하며 만들어지기 떄문에 레이 아웃 트리에 포함되지 않는 것은 웹 페이지 내에 자리를 차지하고 있지 않는 요소입니다. 하지만 visibility : hidden과 같은 속성은 자리는 차지하고 있지만 스타일적으로 보이지만 않는 것이기에 레이아웃 트리에 추가됩니다.

레이아웃

위에서 만들어진 레이아웃 트리를 기반으로 각 노드의 너비, 높이, 위치를 결정하는 프로세스입니다.

페인팅

지금까지 과정을 통해 HTML을 DOM으로 만들었고 스타일과 레이아웃 모두를 포함한 레이아웃 트리를 만들었지만 추가적으로 페인트 프로세스가 필요합니다. 그냥 페인트 하면 될 것 같지만 여기에도 보다 정확히 페인트 하기 위해 크롬 브라우저에선 하나의 페이지를 여러 층의 레이어로 분리하고 순서를 결정합니다.

웹 페이지에 레이어가 있다는 것을 우리는 이미 알고 있습니다. 기본적으로 z-index를 생각해 보면 stacking context라는 개념이 있습니다.

Stacking context is a three-dimensional conceptualization of HTML elements along an imaginary z-axis relative to the user, who is assumed to be facing the viewport or the webpage. HTML elements occupy this space in priority order based on element attributes.

가상의 Z축을 사용하여 HTML 요소의 3차원 개념화입니다. Z축은 사용자 기준이며, 사용자는 뷰포트 혹은 웹 페이지를 바라보고 있을 것으로 가정합니다.

즉 브라우저 상하좌우에서 앞과 뒤까지 제어할 수 있는데, Stacking Context의 개념을 통해 여러 레이어가 존재한다는 것을 알 수 있죠. 다시 돌아가서

페인트 과정에서는 텍스트, 색깔, 그림자, 밑줄등 시각적인 부분을 화면에 그리는 작업을 합니다. 이 작업은 빠르게 진행해야 합니다. 애니메이션등이 있을 때 자칫 버벅거리게 하는 병목이 될 수 있기 때문이죠.

MDN 공식문서를 통해 알 수 있는 것은 부드러운 스크롤과 애니메이션을 위해선, 레이아웃 계산, 리플로우, 패인팅과 같이 메인 스레드를 점유하는 작업은 16.67ms 미만을 차지해야만 한다고 합니다.

성능을 확보하기 위해 합성이 필요한 것이죠. 즉 페인팅 작업은 렌더러 프로세스의 메인 스레드에서 수행하지 않고 GPU 레이어로 올려서 진행하여 페인트와 리페인트의 성능을 높인다고 합니다. 와... 구체적으로 어떤 요소가 있는지 잠시 살펴보시죠.

  • 엘리먼트
    • <video>
    • <canvas>
  • 속성
    • opacity
    • 3D transform
    • will-change

레이어를 분리하는 것으로 애니메이션의 성능을 최적화하는 것이 었습니다. 그렇게 레어이를 분리하면 이제 합성을 해야죠.

합성

레이아웃 트리를 기반으로 레이어 트리를 만든다고 합니다. 합성은 페이지의 일부를 레이어로 분리하고 개별적으로 래스터화한 다음 컴포지터 스레드라는 별도의 스레드에 페이지로 합성하는 기술이라고 합니다. 조금 더 살펴봐야겠죠.

합성의 이점

메인 스레드를 포함하지 않고 수행한다는 것입니다. 즉 컴포지터 스레드는 스타일 계산이나 자바스크립트 실행을 기다릴 필요가 없습니다.

상호작용

위의 과정이 완료하면 화면에 화려하게 모든 요소들이 보여지긴 합니다. 하지만 상호작용은 불가할 수 있습니다. 만약 JS의 다운로드가 지연된다면?! 이럴 경우에는 유저에게 보여지긴 했지만 유저가 사용하지 못하는 시점이라고 생각할 수 있습니다.

TTI : Time to Interactive(TTI) 는 JS 로딩이 완료되고 메인 스레드가 비활성화된 시점으로 정의된 비표준화된 웹 성능 '진행' 측정항목입니다.

즉 여러가지 방법으로 TTFB이 일어난 이후 50ms 이내로 상호작용이 가능하도록 구현해야합니다. JS 번들을 쪼갠다던가 병렬적으로 받아오도록 최적화할 수 있겠죠!!

마치며

위의 글을 정리하고자 합니다.

서버로부터 HTML 문서를 받게 되면 메인 스레드에서 HTML 문서를 파싱 하여 일련의 과정을 통해 DOM으로 만듭니다. HTML 문서를 파싱 하면서 외부 리소스인 CSS를 만나면 파서는 중단되지 않지만 렌더링은 중단됩니다. 그 이유는 CSS가 적용되지 않은 상태로 보이는 현상을 미연에 방지하기 위함입니다. 파싱 중 속성이 부여되지 않은 스크립트 태그를 만나게 되면 파싱이 중단됩니다. 일반적으로 html의 바디 태그 최 하단에 위치하여 콘텐츠에 접근할 수 있도록 합니다. 그러나 async나 defer 속성을 script에 추가한다면 비동기적으로 자바스크립트 파일은 다운로드할 수 있습니다. 하지만 async와 defer 속성 간에 실행되는 시점은 차이가 있습니다. 그렇게 DOM의 CSSOM이 만들어지면 스타일을 계산하기 위해 레이아웃 트리가 만들어집니다. 레이아웃 트리에 포함되는 노드는 웹 페이지에서 자리를 차지하는 요소들이 포함됩니다. 예를 들어 display:none, <head>, <meta>등 속성과 태그는 포함이 되지 않습니다. 이제 레이아웃 단계입니다. 위에서 만들어진 레이아웃 트리를 기반으로 각 노드의 너비, 높이, 위치를 결정합니다. 이후 페인팅 작업이 이루어집니다. 페인팅 작업은 합성 방식으로 이루어집니다. 만들어진 레이아웃 트리를 기반으로 레이어 트리를 만들어 별도의 스레드로 위임하여 페인트를 완료합니다. 이런 과정을 통해서 서버로부터 HTML을 받은 후, 사용자가 눈으로 확인하고 사용할 수 있는 페이지가 렌더링 되기까지의 과정을 살펴보았습니다.

참고

  1. https://developer.chrome.com/blog/inside-browser-part1?hl=ko
  2. https://developer.chrome.com/blog/inside-browser-part2?hl=ko
  3. https://developer.chrome.com/blog/inside-browser-part3?hl=ko
  4. https://developer.chrome.com/blog/inside-browser-part4?hl=ko