Critical Rendering Path란?
"어떠한 과정을 통해 사용자에게 웹페이지가 보여지게 되는 것일까?"
개발을 하다보면 내가 개발한 화면을 다른 유저가 보게 되는 일이 많은데, 해당 화면이 어떠한 과정을 통해 사람들에게 보여지는지에 대해서는 별다른 의문을 가져본 적이 없었습니다. 이번 글에서는 내가 만든 화면이 어떻게 사용자에게 보여지는 것인지 알아보겠습니다.
인터넷 브라우저의 구조
설명에 앞서, 먼저 인터넷 브라우저의 구조에 대해 살펴보겠습니다.
인터넷 브라우저란 웹 서버와 통신하여 인터넷 사이트 및 다양한 컨텐츠를 볼 수 있도록 지원해주는 소프트웨어 프로그램으로, Chrome을 포함하여 Safari, Firefox, 오페라 등 여러 종류가 있습니다. 각 브라우저마다 전반적인 구조는 조금씩 다르지만 큰 틀은 아래의 그림으로 동일합니다.

사용자 인터페이스 (User Interface / UI)

위 이미지와 같이, 페이지를 보여주는 창 위에 주소 표시줄, 이전/이후 버튼, 홈버튼, 북마크 버튼 등이 존재하는 영역을 말합니다.
브라우저 엔진
사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어하는 부분입니다. 예를 들어 이전 버튼을 눌렀을 때, 이전 화면을 보여주게 되는데 이전 버튼이 있는 부분은 사용자 인터페이스, 이전 화면을 보여주는 부분은 렌더링 엔진, 그리고 그 사이의 동작을 제어하는 곳이 브라우저 엔진이라고 이해할 수 있습니다.
렌더링 엔진
브라우저 엔진과 밀접히 관련된 엔진으로, 웹 페이지가 표시되는 모든 영역을 제어하는 역할을 합니다. 렌더링 엔진은 요청한 콘텐츠(HTML, CSS 등)를 파싱하고, 화면에 나타내는 일을 수행합니다.
자료 저장소 / 데이터 저장소
localStorage, sessionStorage, 쿠키와 같이 로컬에 저장되어 좀 더 오래 유지되어야 하는 데이터들을 보관할 수 있도록 지원하는 영역입니다.
통신 (Networking)
플랫폼과 독립적인 인터페이스로, 각 플랫폼의 하부에서 실행되며 HTTP/HTTPS 네트워크 처리를 담당합니다.
자바스크립트 해석기 (JS Interpreter)
스크립트(JS 코드)를 파싱할 때 사용하는 JS 엔진입니다. HTML을 파싱 중 script 태그를 만나면 JS 엔진이 제어 권한을 넘겨받습니다. 즉 DOM 트리를 만들다 script 태그를 만나면 DOM 트리를 만드는 과정을 잠시 중단하고, JS 엔진이 작업을 마칠 때까지 기다리게 됩니다.
UI 백엔드 (UI Backend)
기본 위젯을 그릴 때 이용하는 부분으로, OS의 방법을 사용합니다.
주소창에 www.google.com을 치면 일어나는 일
그렇다면 본격적으로 주소창에 www.google.com을 검색했을 때 어떤 일이 일어나는지 알아보겠습니다.
주소창에 www.google.com을 검색했을 때는 아래와 같은 과정이 일어나게 됩니다.
google.com -> 입력한 텍스트 정보 확인 -> 네트워크 호출 -> 렌더링 작업 -> google
1. 입력한 텍스트 정보 확인
브라우저는 사용자가 주소창에 어떤 텍스트를 입력했을 때, 입력한 텍스트가 검색어인지 URL인지 확인합니다.
만일 입력한 텍스트가 검색어이면, 브라우저는 검색 엔진의 URL에 검색어를 포함한 주소로 페이지를 이동시킵니다.
만일 입력한 텍스트가 URL이면, 브라우저 엔진에서 네트워크 호출을 수행합니다.
따라서 www.google.com이라는 텍스트를 입력하고 엔터를 치면, 해당 값은 URL이므로 네트워크 호출을 수행하게 됩니다.
2. 네트워크 호출
다음은 네트워크 호출입니다.
사용자에게 구글 사이트를 화면에 보여주려면 브라우저는 구글의 HTML, CSS, script, 이미지 등의 데이터를 가지고 있어야 합니다. 하지만 해당 데이터들은 구글 서버 컴퓨터에 존재합니다.
따라서 브라우저는 구글 서버와의 네트워크 통신을 통해 이러한 데이터들을 가져와야 하며, 해당 과정은 아래의 두 과정으로 설명할 수 있습니다.
- 구글 서버의 주소를 알기 위해 **도메인 네임 서버(Domain Name Server)**와 통신
- DNS를 통해 알아낸 주소를 바탕으로 구글 서버와 통신하여 필요한 데이터를 수집
구글 서버의 주소를 알기 위해 도메인 네임 서버(DNS)와 통신
클라이언트는 구글 서버의 주소를 알아내기 위해 아래의 과정을 수행합니다.
- host 파일에서 도메인 네임에 대응하는 IP 주소가 있는지 확인합니다.
- 만일 없다면, 도메인 네임 서버(Domain Name Server)에 '구글 IP 주소를 알려주세요'라는 요청을 보냅니다.
여기서 도메인 네임이란 URL www.google.com에서 google.com에 해당하는 부분입니다.
인터넷은 컴퓨터의 주소인 IP 주소를 기반으로 동작합니다. 하지만 우리가 인터넷을 사용할 때는 IP 주소 대신 사용하기 쉽도록 문자로 이루어진 도메인 네임을 사용합니다.
따라서 도메인 네임을 IP 주소로 변환해 주는 환경인 DNS(Domain Name Server)가 반드시 필요한데, 이 DNS를 운영하는 장치를 DNS 서버라고 합니다.
즉, DNS 서버는 도메인 주소에 대응하는 IP 주소를 찾아주는 역할을 수행하는 것입니다.
클라이언트는 일반적으로 DNS 서버의 IP 주소를 이미 가지고 있습니다. 따라서 클라이언트는 DNS 서버와 통신이 가능하고, google.com에 해당하는 IP 주소를 요청 및 응답받을 수 있습니다.
DNS를 통해 알아낸 주소를 바탕으로 구글 서버와 통신하여 필요한 데이터 수집
-
이제 클라이언트는 구글 서버의 IP 주소를 알게 되어 구글 서버와 통신할 수 있게 되었습니다. 클라이언트의 브라우저는 구글 서버에 데이터를 요청하는 HTTP Request를 보냅니다.
-
HTTP Request를 받은 구글 서버는 클라이언트가 요청한 문서를 찾아 읽고, 이를 0과 1로 이루어진 바이트 형태(바이트 스트림)로 변환한 후 클라이언트로 HTTP Response를 보냅니다.
3. 렌더링 작업
위 과정에서 구글 서버와 통신하여 얻은 HTTP Response는 0과 1로 이루어진 바이트 형태로, 브라우저 엔진이 읽을 수 없습니다. 따라서 브라우저 엔진은 렌더링 엔진에게 해당 데이터를 해석하고, 웹 페이지를 화면에 띄울 것을 요청합니다.
요청을 받은 렌더링 엔진은 받은 데이터를 바탕으로 렌더링 프로세스를 수행하고, 이 과정이 끝나면 브라우저 엔진에게 작업 완료를 알리고 최종적으로 화면에 구글 페이지가 보여지게 됩니다.
그렇다면 렌더링 프로세스는 어떠한 과정으로 이루어지는 것일까요?
렌더링 프로세스
렌더링 프로세스는 아래와 같은 과정으로 이루어집니다.
- HTML을 파싱하여 DOM 트리 구축, CSS를 파싱하여 CSSOM 트리 구축 (+ JS 파싱)
- DOM 트리와 CSSOM 트리를 통한 렌더 트리 (Render Tree) 구축
- 렌더 트리 배치 (Layout)
- 렌더 트리 그리기 (Paint)
다음은 웹킷과 Blink의 렌더링 프로세스 과정과, 게코의 렌더링 프로세스 과정입니다.
브라우저별로 사용하는 렌더링 엔진이 다른데, 파이어폭스의 경우 모질라에서 직접 만든 게코(Gecko) 엔진을 사용하고, 사파리는 웹킷(Webkit) 엔진을 사용합니다.
참고로 크롬은 웹킷(Webkit) 엔진을 사용했다가, 웹킷을 Fork하여 자체적으로 구현한 블링크(Blink) 엔진을 현재 사용하고 있습니다.
웹킷과 Blink의 렌더링 프로세스 과정

게코의 렌더링 프로세스 과정

그렇다면 위의 1번부터 4번의 렌더링 프로세스 과정에 대해서 자세하게 알아보겠습니다.
HTML을 파싱하여 DOM 트리 구축, CSS를 파싱하여 CSSOM 트리 구축 (+ JS 파싱)
HTML 파싱
HTML 파서는 HTML 문서를 위에서부터 읽어 내려가며 파싱을 진행하고, 그 결과물로 DOM 트리를 생성합니다. 자세한 과정은 아래와 같습니다.
-
변환 (Bytes -> Characters): 브라우저가 HTML의 원시 바이트를 해당 파일에 대해 지정된 인코딩(예: UTF-8)에 따라 개별 문자로 변환합니다.
-
토큰화 (Character -> Tokens): 브라우저가 문자열을 W3C HTML5 표준에 지정된 고유 토큰으로 변환합니다. (예:
<html>,<body>및 꺽쇠괄호로 묶인 기타 문자열) -
렉싱 (Tokens -> Nodes): 변환된 토큰은 해당 속성 및 규칙을 정의하는 '객체'로 변환됩니다.
-
DOM 트리 생성 (Nodes -> DOM): 마지막으로, HTML 마크업이 여러 태그 간의 관계를 정의하기 때문에 생성된 객체는 트리 데이터 구조 내에 연결됩니다. 이 트리 데이터 구조에는 원래 마크업에 정의된 상위-하위 관계도 포함됩니다.
이때 렌더링 엔진은 사용자의 만족도를 높이기 위해 HTML 문서가 모두 파싱될 때까지 기다리지 않고 파싱 이후의 과정인 배치와 그리기를 미리 진행합니다.
CSS 파싱
CSS 파싱도 HTML 파싱과 비슷한 과정으로 진행됩니다.
HTML 파싱 중 CSS 문서를 가져오는 link 태그를 만나면, DOM 생성이 잠시 중단되고 해당 CSS의 파싱 과정이 시작됩니다. DOM 생성 과정은 CSSOM이 만들어지는 것을 기다립니다. 따라서 CSS 파일이 크다면 CSSOM 트리를 만드는 과정이 오래 걸리며, 이는 DOM 트리 생성에도 영향을 끼치기 때문에 CSS 파일의 크기를 줄이는 방법으로 시간을 줄일 수 있습니다.
CSS도 HTML을 파싱하여 DOM 트리를 만드는 과정과 동일하게 변환, 토큰화, 렉싱 과정을 거치고 CSSOM 트리를 생성합니다.
이렇게 만들어진 CSSOM 트리의 노드는 DOM 트리 요소의 선택자에 맞춰 적용될 CSS 스타일 정보가 포함되어 있습니다.
JS 파싱
HTML 파싱 과정에서 script 태그를 만나면, 렌더링 엔진은 DOM 생성을 잠시 중지하고 서버에서 해당 JavaScript 리소스를 브라우저 엔진으로부터 받아옵니다. 그리고 JavaScript 엔진에게 제어권을 넘겨줍니다.
JavaScript 엔진은 받아온 JS 리소스를 파싱하여 AST(추상 구문 트리)를 생성하고, 이를 바이트코드로 변환해 실행합니다.
JavaScript 파싱이 종료되면 렌더링 엔진은 다시 제어권을 돌려받고 DOM 생성을 이어나갑니다.
만일 script 태그를 body 태그의 중간에 작성할 경우, HTML 파싱이 끝나지 않은 상태에서 JavaScript로 인해 DOM이 조작되어 에러가 발생할 위험이 생깁니다. 따라서 script 태그는 반드시 body 태그 내부의 최하단에 위치해야 합니다.
DOM 트리와 CSSOM 트리를 통한 렌더 트리 (Render Tree) 생성
다음은 렌더 트리 생성입니다.
DOM은 콘텐츠, CSSOM은 스타일 규칙을 설명하는 독립적인 객체입니다. 각각의 객체를 가지고 화면에 픽셀을 찍기 위해서는 두 객체를 합쳐 Render Tree를 만들어야 합니다.
따라서 앞선 과정의 HTML과 CSS의 파싱 과정에서 나온 결과물인 DOM 트리와 CSSOM 트리를 서로 결합하여 **렌더 트리(Render Tree)**를 생성합니다.

렌더 트리 생성과정은 아래와 같습니다.
- DOM 트리의 루트에서 시작하여 표시되는 노드 각각을 순회합니다. 이때,
스크립트 태그,메타 태그,display: none속성 등 화면에 보여지지 않는 노드는 렌더 트리의 구성에서 제외됩니다. - 표시된 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용합니다.
- 표시된 노드를 콘텐츠 및 계산된 스타일과 함께 내보냅니다.
렌더 트리 배치 (Layout)
Render Tree에는 노드와 노드의 스타일만 계산되어 있습니다. 따라서 Layout에서는 화면에 표시될 노드의 정확한 위치 및 크기를 계산하는 과정을 진행합니다.
이때 노드의 위치는 (x, y) 좌표계를 사용하는데, 렌더 트리의 루트부터 아래로 내려가면서 계산을 진행하게 되며 %, rem, vh 등의 상대적인 값들이 절대적인 값인 px로 변하게 됩니다.
렌더 트리 그리기 (Paint)
Layout 과정을 거쳐 화면에 UI를 표현하기 위한 계산이 끝나면 Paint 과정이 진행됩니다.
Layout 과정에서 Render Layer가 2개 이상 생성되면 각각의 Layer를 Painting한 뒤 하나의 이미지로 Composite하는 과정을 추가로 거쳐 브라우저에 표현합니다.
Paint / Rasterize
Paint는 Render Tree를 화면의 픽셀로 변환하는 프로세스입니다. 이는 텍스트, 색, 이미지, 경계 및 그림자 등 요소의 모든 시각적 부분을 그리는 작업을 포함합니다. 이때 픽셀로 변환하는 이 과정을 래스터화(Rasterizing)라고 합니다.
Composite
Composite이란, 사용된 HTML이나 CSS 속성에 따라 여러 개의 Layer가 생성된 경우, 생성된 Layer들을 합성하여 한 장의 bitmap으로 만드는 과정을 말합니다. 각 Layer별로 paint 되기 때문에 불필요한 painting을 줄여 효율적으로 그릴 수 있습니다.
Reflow와 Repaint
여기서 끝이 아닙니다.
브라우저에 특정 변경이 생긴다면 이를 화면에 다시 그려주어야 합니다. 즉 특정 액션이나 이벤트에 따라 HTML 요소의 크기나 위치 등의 레이아웃이 변하게 될 수 있으며, 이 경우 Layout(Reflow) 또는 페인트 과정이 다시 일어나는 것을 리페인트(Repaint)라고 합니다. 기본적으로 리플로우가 발생하면 리페인트도 함께 발생합니다. 또한 화면의 구조가 변하지 않더라도 요소의 색깔이 변한다면 리페인트가 일어납니다.
Reflow
렌더 트리와 각 요소들의 크기와 위치를 다시 계산해주는 과정을 Reflow라고 합니다.
이러한 Reflow는 아래와 같은 경우에 발생합니다.
- DOM 노드를 추가하거나 제거하는 경우
- DOM 노드의 위치가 변경되는 경우
- DOM 노드의 크기가 변경되는 경우 (margin, padding, border 등)
- 폰트가 변경되는 경우 (font-weight, font-size)
- 페이지 초기 렌더링
- 윈도우 리사이징
Repaint
Reflow만 수행되면 실제 화면에는 반영되지 않기 때문에 다시 Painting이 일어나야 합니다. 이 과정을 Repaint라고 합니다.
화면의 구조가 변경되었을 때에는 Reflow 과정을 거쳐 화면 구조를 다시 계산한 후 Repaint 과정을 통해 화면을 다시 그립니다. 즉 화면의 구조가 변경되었을 때에는 Reflow와 Repaint 모두 발생합니다.
화면의 구조가 변경되지 않는 경우에는 Repaint만 발생합니다. 예를 들면 opacity, background-color, visibility, outline 등의 스타일 변경 시에는 Repaint만 동작합니다.
이러한 Reflow와 Repaint는 비용이 높은 연산이므로 이를 최소화하는 것을 통해 성능을 향상시킬 수 있습니다.
Reflow가 일어나는 대표적인 속성
position, width, height, left, top, right, bottom, margin, padding, border, border-width, clear, display, float, font-family, font-size, font-weight, line-height, min-height, overflow, text-align, vertical-align, white-space...
Repaint가 일어나는 대표적인 속성
background, background-image, background-position, background-repeat, background-size, border-radius, border-style, box-shadow, color, line-style, outline, outline-color, outline-style, outline-width, text-decoration, visibility...