Critical Rendering Path란? [Rendering Engine]

Dengo·2023년 4월 5일
3

WEB

목록 보기
3/3
post-thumbnail

줄여서 CRP라고 부르겠습니다 🙂

CRP란?

MDN의 설명에 따르면 HTML, CSS, JavaScript를 화면에 픽셀로 변화하여 나타내는 일련의 과정이라고 소개하고 있습니다.
즉, 웹 브라우저의 렌더링 엔진이 HTML, CSS, JavaScript 파일을 읽고 렌더링을 하는 과정을 의미합니다.

흔히 웹 프론트엔드 개발자에게 있어서 렌더링 성능을 향상 시키는 것은 꽤나 중요한 숙제로 꼽혀지곤 합니다.
사용자로 하여금 더 나은 경험을 하게 해야하기 때문이죠.

여기서 CRP는 단순히 웹 브라우저의 렌더링 과정을 넘어서 렌더링 성능 향상의 척도로 여겨지기도 합니다.
CRP 최적화 => 렌더링 성능 향상 으로 직결되는 것 입니다.

그럼 CRP가 정확히 어떤 과정을 의미하는지를 알아봐야겠습니다.

브라우저의 기본 구조


잠시 브라우저 구조를 간략하게 소개하면 아래와 같습니다.

1. 사용자 인터페이스 - 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분에 해당한다
   (주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등)
2. 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
3. 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함
4. 통신 - HTTP 요청과 같은 네트워크 호출
5. UI 백엔드 - 콤보 박스와 창 같은 기본적인 장치를 그림
6. 자바스크립트 해석기 - JavaScript 엔진
7. 자료 저장소 - 로컬, 세션 스토리지

지금부터 알아볼 CRP가 일어나는 부분은 바로 렌더링 엔진입니다.
Chrome브라우저 기준 Webkit엔진에서 파생된 Blink엔진을 사용하고 있습니다.

자바스크립트 코드 실행과정 글에서와 같이 렌더링 과정 또한 각 브라우저의 렌더링 엔진마다 그 과정이 상이합니다.
이번 글에서는 Blink의 모체인 Webkit의 렌더링 과정에 대해 알아보도록 하겠습니다.

전체적인 렌더링 동작 과정


1. 파싱 (DOM트리, CSSOM트리 생성)
2. 렌더트리 생성
3. 레이아웃(배치)
4. 그리기
이렇게 크게는 네번의 단계를 거친다고 볼 수 있습니다.

여기서 오해하기 쉬운 부분은 위에 적은 1번 ~ 4번의 과정은

1번 시작
1번 모두 완료
2번 시작
2번 모두 완료
...

이런식으로 순차적으로 동작하는게 아닌 점 입니다.

위의 과정은 병렬적으로 진행이 됩니다.
예를 들면 HTML, CSS, JavaScript 리소스를 다운로드하고 파싱하는 동안 동시에 파싱된 데이터를 가지고 DOM트리, CSSOM트리를 생성하고 또 만들어진 렌더 트리에 대해서 레이아웃과 페인트 과정 또한 병렬적으로 처리가 됩니다.

렌더링 과정이 이와 같이 동작하기 때문에 웹 페이지에 표시하기 위해 필요한 최소한의 자원을 우선적으로 로드하고 처리하여 사용자 경험을 향상시킬 수 있습니다.

HTML 파싱

위의 그림에서 살펴본 웹 브라우저의 '통신' 부분을 통해 가져온 HTML을 렌더링 엔진이 파싱하는 것에서 부터 렌더링 과정은 시작이 됩니다.

파싱이란 사람이 이해할 수 있도록 작성된 HTML '문서'를 브라우저가 이해하고 사용할 수 있는 구조로 변환하는 것을 의미합니다.

이 과정을 통해서 웹 브라우저는 HTML을 DOM트리로 만들게 됩니다.


그림과 같은 과정을 거쳐서 DOM 트리가 만들어집니다.

문서 내의 태그를 순서대로 읽어가면서 위와 같은 DOM트리를 만드는 것인데,
<link>태그나<img> 태그와 같이 외부 자원을 필요로 한다면 네트워크 요청을 보냅니다.
특히 <link>태그의 경우 CSS파일을 불러오고 앞서 언급한 바와 같이 CSS파싱을 HTML파싱과 함께 병렬적으로 수행합니다.

<script>태그를 만나면 마찬가지로 네트워크 요청을 보내어 자바스크립트 파일을 가져오는데요,
이 경우에는 자바스크립트 파일을 실행하기 위해서 웹 브라우저의 제어 권한이 렌더링 엔진에서 자바스크립트 엔진으로 넘어가게 됩니다. 즉, HTML파싱이 중단됩니다.

이러한 문제를 해결하기 위해 <script>태그에서는 defer속성과 async속성이 지원되는데 해당 글에서는 자세하게 다루진 않겠습니다.

CSS 파싱


로드된 스타일 시트 또한 파싱됩니다.
이 과정을 통해서 스타일 시트는 CSSOM트리가 됩니다.

좀 더 정확히는 가져온 CSS를 파싱하여 스타일 규칙 (Selector, Property, Value)으로 분해합니다.
분해된 스타일 규칙을 파싱하여 해당 규칙이 적용될 요소와 함께 스타일 규칙 객체를 생성합니다.
이렇게 만들어진 스타일 규칙 객체를 결합해서 CSSOM 트리가 만들어지게 됩니다.

이 과정에서 만일 복잡한 선택자에 대해 스타일을 적용하는 내용이 있다고 가정해보겠습니다.
가령 div div div div { ... } 와 같은 상황이라면
브라우저는 이 복잡한 선택자에 대해 해당하는 요소를 찾기위해 더 많은 작업을 수행하게 될 것 입니다.
이는 렌더링 성능 저하로 직결되기 때문에 선택자는 가급적 간단하게 하는 것이 좋습니다.

Render Tree 생성 (Style)


이제 이렇게 만들어진 DOM 트리와 CSSOM 트리를 결합하여 렌더트리를 생성합니다.
위 그림에서 다시 한번 오해하면 안되는 부분은
DOM트리, CSSOM트리 전부 완성 -> 렌더트리 완성으로 진행되는 것이 아니라 준비가 된 부분에 대해서는 먼저 결합을 진행한다는 부분을 기억하셔야 합니다. (준비가 된 부분들을 빨리 빨리 먼저 보여주려고)

렌더트리의 각 노드를 렌더러(혹은 렌더 객체)라고 부르고 아래와 같은 데이터를 담습니다.

// Webkit 렌더러
class RenderObject { virtual 
	void layout(); virtual 
	void paint(PaintInfo); virtual 
	void rect repaintRect(); 
	Node * node; //the DOM node 
	RenderStyle * style; // the computed style 
	RenderLayer * containgLayer; //the containing z-index layer 
}

렌더러에 대해서

렌더러는 자신과 자식 요소를 어떻게 배치하고 그려내야 하는지 알고 있습니다.
즉, 너비, 높이, 위치와 같은 기하학적인 정보들을 가지고 있습니다.

예를 들어 어떤 요소가 width : 50%같은 정보를 가지고 있고, 이 요소의 부모요소가 1000px이라면 해당 요소의 너비는 500px로 계산되어 나타내질 것 입니다.

이 계산 과정을 Layout과정에서 진행하는데 이건 아래에서 설명하도록 하겠습니다.

여기서 중요한 점은 렌더러는 width : 50%정보와 계산된 500px 둘 다 가지고 있게됩니다.
만약 웹 브라우저의 창의 크기가 달라져서 부모 요소의 크기가 달라진다면 width : 50%를 다시 계산해야할 것 이기 때문입니다.

렌더러는 오직 보여지는 콘텐츠에 대해서만 만들어지게 됩니다.
즉, display:none인 요소가 있다거나 비시각적 요소(예를 들면<head>태그)는 렌더러 자체가 만들어지지 않습니다.

렌더러는 DOM과 1:1관계를 갖는 것은 아닙니다.
위의 비시각적 요소에 대한 이야기도 포함됩니다만, 다른 예시로 <select>태그를 예시로 들 수 있겠습니다.


기본적으로 UI백엔드에서 가져오는 <select>요소의 모습은 위와 같습니다.
크게 세 부분으로 나눌 수 있는데 펼치기 버튼, 드롭다운 목록, 선택한 값 보여주는 영역 이렇게 나눌 수 있고 이 세가지 부분은 각각 렌더러로 만들어집니다.
<select>요소에 대해서는 3개의 렌더러가 만들어지는 것 입니다.

뷰포트에 대해서

뷰포트(viewport)란 웹페이지가 표시되는 영역입니다.
다시 말해 사용자가 볼 수 있는 영역을 뜻하고 이 영역 내에서 웹페이지를 렌더링 합니다.
그렇기 때문에 브라우저는 최초의 렌더링을 위해 뷰포트를 생성할 것 이고, 뷰포트가 최초의 렌더러가 되는 것 입니다.

일반적으로 뷰포트는 웹 브라우저 창의 크기와 같습니다.
하지만 모바일 기기의 경우엔 뷰포트의 크기가 작아져서 일부 콘텐츠가 화면에 보이지 않을 수도 있는데 이것을 <meta>태그를 이용해서 해결할 수 있습니다

<meta name="viewport" content="width=device-width, initial-scale=1.0">

간략하게 설명을 해보자면 <meta>태그의 viewport속성을 사용하여 뷰포트의 크기를 지정한 것 입니다.
width=device-width를 통해서 뷰포트의 너비를 디바이스의 가로 크기와 같게 지정할 수 있습니다.
initial-scale=1.0은 뷰포트의 초기 확대/축소 비율을 정하는 부분입니다.
initial-scale=1.0은 명시하지 않아도 동일한 동작은 하지만 모바일 기기 성능상 권장되는 부분이라고 합니다.

그래서 렌더러는 무엇을 할까요?

// Webkit 렌더러
class RenderObject { virtual 
	void layout(); virtual 
	void paint(PaintInfo); virtual 
	void rect repaintRect(); 
	Node * node; //the DOM node 
	RenderStyle * style; // the computed style 
	RenderLayer * containgLayer; //the containing z-index layer 
}

렌더러를 통해서 기하학적 정보를 계산하고 저장한다고 했습니다.
그리고 이 정보를 가지고 마침내 웹 브라우저에 그리는 것 입니다.

RenderObject를 다시 가져와봤는데 다시 보시면 여기에 layoutpaint라는 메서드가 있습니다.
layout : 해당 객체와 자식 요소들의 위치와 크기를 계산함
paint : 이 정보를 가지고 그림
대략 이런 기능을 하는 메서드를 렌더러가 실행하게 됩니다.

예상할 수 있다시피 해당 부분은 렌더링 과정에서 가장 핵심적인 부분이고 렌더링 성능과 가장 직결되는 부분이라 볼 수 있습니다.

위 두 과정을 하나씩 자세히 살펴보겠습니다

Layout (배치 과정)


레이아웃 과정은 렌더 트리의 렌더러를 화면에 어떻게 배치해야할 것인지를 계산하는 과정입니다.

웹 브라우저에서는 최종적으로 paint되기 위해서 모든 수치를 px로 가지고 있어야 합니다.
예를 들어 사진과 같이 노드의 크기가 상대적인 값 50%로 잡혀있을 때 정확한 px값을 구하는 과정이 Layout 과정입니다.

즉 웹 브라우저 창의 크기를 사용자가 조절하거나, 디바이스의 세로 모드를 가로 모드로 변환하는 window resizing 상황에서 Layout과정이 다시 일어날 수 있습니다.

이러한 Layout과정은 렌더링 과정을 통틀어서 가장 복잡하고 계산 비용이 많이 드는 과정입니다. 따라서 웹 브라우저의 레이아웃 구조를 복잡하게 하지 않고 최대한 단순히 하는게 렌더링 성능 향상에 도움이 될 것 입니다.

이번 과정 또한 앞서 설명했듯이, Layout이 진행된 렌더러에 대해서 Paint가 바로 진행됩니다. 따라서 웹 브라우저는 전체의 웹 페이지에 대해서 Layout과 Paint를 동시에 수행하는 모습이 됩니다.

Paint (그리기 과정)

Layout과정을 통해서 계산된 위치와 스타일을 통해서 그릴 준비를 해야합니다.
이것을 위해 Layer라는 것을 만들고 여기에 배경, 텍스트와 같은 내용들을 채워 넣고 이것을 그리는 것 입니다.

Layer라는 것이 존재하는 이유는 그릴 요소들에 z-index나 position 속성이 부여되는 경우에 그리는 순서를 보장하기 위해서 입니다.

이렇게 Layer가 여러개가 된다면 Paint과정 이후에 추가적으로 Composite(합성) 과정이 일어나야 합니다.

Paint과정은 Layout에 비해 비교적 시간 소요가 적긴 하지만,
처리해야 하는 스타일이 복잡하다면 오래걸릴 수 있습니다.

예를 들어, 단순히 background-color의 경우 색깔만 설정해주는 것이라 빠른 반면, 그라데이션이나 그림자 효과와 같은 경우엔 비교적 더 오래걸린다고 합니다.

Reflow, Repaint

레이아웃 과정은 렌더링 과정에서 가장 치명적인 과정에 해당됩니다.
레이아웃이 다시 진행되는 과정을 Reflow라고 하고
이것이 발생하는 경우는 다음과 같습니다.

  • 윈도우 리사이징 시 (Viewport 크기 변경 시)
  • 노드 추가 또는 제거
  • 요소의 위치, 크기 변경 (left, top, margin, padding, border, width, height, 등 ...)
  • 폰트 변경(텍스트 내용)과 이미지 크기 변경(크기가 다른 이미지로 변경 시)
  • 스크롤이 일어났을 때

그리고 Reflow가 일어나게 된다면 Repaint또한 다시 일어나게 됩니다.

Repaint는 말그대로 다시 Paint과정을 수행하는 것 인데 Reflow없이 Repaint만 일어나는 경우는 레이아웃 변경 없이 스타일 속성 변화만 일어나는 경우 입니다.
가장 대표적으로 색상 변화만 일어나는 경우가 이에 해당될 것 입니다.

추가적으로 Reflow와 Repaint는 자바스크립트 코드에 의해 변경될 수 도 있을 것 입니다.
예를 들어 버튼 클릭시 요소의 크기가 증가하고 색상이 바뀌게 할 수 있겠죠.

따라서 이러한 상호작용과 관련해서는
microtask queue가 전부 비워졌을 때 Reflow, Repaint가 일어날 수 있다라고 간략하게 설명드릴 수 있습니다.

Reflow, Repaint에 대한 자세한 설명은 다른 글에서 자세히 설명해보겠습니다.

참고

https://velog.io/@mu1616/Critical-Rendering-Path
https://developer.mozilla.org/ko/docs/Web/Performance/Critical_rendering_path
https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Virtual-DOM/#_1-파싱
https://tecoble.techcourse.co.kr/post/2021-10-24-browser-rendering/
https://nohack.tistory.com/36
https://hangem-study.readthedocs.io/en/latest/front_interview/browser-rendering/
https://d2.naver.com/helloworld/59361

profile
Software Engineer (전산쟁이)

1개의 댓글

comment-user-thumbnail
2024년 1월 15일

자세한 설명 감사합니다 !! 많은 도움이 됐어요 🙌🏻

답글 달기