🔖 Before start
웹브라우저란, 웹서버와 통신해 HTML, CSS, JavaScript, 이미지 등 웹 리소스를 받아 사용자에게 보여주는 GUI 기반 소프트웨어다.
웹 브라우저의 종류는 대표적으로 Safari, Firefox, Chrome이 있는데,
서버로 요청한 HTML, CSS, JavaScript 코드가 사용자에게 어떻게 보여지는지 렌더링 과정을 살펴보자.
HTML과 CSS를 파싱해 요청한 웹페이지를 표시하는 역할
모든 브라우저는 기본적으로 HTML와 CSS를 파싱하는 렌더링 엔진이 있다.
Safari는 Webkit, Firefox는 Gecko, Chrome은 Webkit에서 분리된 Blink 렌더링 엔진을 사용하고 있으며,
기본적으로 모두 웹 표준을 준수하지만 동작 방식에는 조금의 차이가 있다.
렌더링 엔진은 비동기 요청으로 인한 데이터 로딩, 사용자 동작(스크롤, 애니메이션 등)으로 인한 업데이트가 발생했을 경우 변경사항을 반영해 다시 화면에 그려내는 역할을 담당한다.
다음은 브라우저 렌더링 과정을 담은 그림이다. 이 그림을 중심으로 렌더링 과정을 알아보자.
브라우저는 서버에 리소스를 요청하고 응답 받은 리소스를 화면에 그려낸다.
www.naver.com
을 검색창에 입력했을 때 네이버 메인 페이지가 브라우저에 그려진다.
명확히 어떤 리소스를 요청한다는 내용은 없지만, 서버는 기본적으로 루트 요청에 대해 index.html을 응답하도록 되어있다.
1️⃣ 렌더링 엔진은 HTML을 파싱할 때 외부 리소스를 로드하는 태그를 만나면 HTML 파일을 일시 중단한다.
CSS 파일을 로드하는 link 태그, 이미지 파일을 로드하는 img 태그, 자바스크립트를 로드하는 script 태그가 있다.
2️⃣ 해당 태그의 리소스를 서버로 요청한다.
HTMl 문서는 문자열로 이루어진 순수 텍스트다. 브라우저에서 시각적인 픽셀로 렌더링하려면 브라우저가 이해할 수 있는 자료구조로 변환해야 한다.
1️⃣ 서버는 브라우저가 요청한 HTML 파일을 메모리에 저장한 다음 메모리에 저장된 바이트(2진수) 형태로 응답한다.
2️⃣ 서버가 응답한 바이트 형태의 HTML은 meta
테그의 인코딩 방식을 기준으로 문자열로 변환된다.
3️⃣ 브라우저는 문자열로 변환된 HTML을 문법적 의미를 갖는 최소 단위인 토큰으로 분해한다.
4️⃣ 각 토큰은 객체로 변환되어 DOM을 구성하는 기본 단위인 노드를 생성한다.
5️⃣ 노드의 중첩 관계를 반영한 트리 자료구조인 DOM 트리가 생성된다.
DOM은 DOM을 조작할 수 있는 인터페이스로 DOM API를 제공한다.
렌더링 엔진은 HTML을 순차적으로 파싱하며 DOM 트리를 생성하는데, CSS를 로드하는 style
이나 link
태그를 만나면 DOM 트리 생성을 일시 중단한다.
1️⃣ 응답 받은 CSS 파일이나 style
태그 내에 작성된 CSS를 HTML과 동일한 파싱 과정을 거친다.
바이트 > 문자열 > 토큰 > 노드 > CSSOM
2️⃣ CSS 상속을 반영해 CSSOM 트리를 생성한다.
3️⃣ CSS 파싱을 완료하면 DOM 트리 생성을 재개한다.
1️⃣ DOM 트리와 CSSOM 트리는 렌더링을 위해 렌더 트리로 결합된다.
렌더트리는 렌더링을 위한 트리 자료구조기 때문에 브라우저 화면에 렌더링 되지 않는 노드는 포함하지 않는다.
2️⃣ meta
태그, script
태그 등과 CSS의 display:none
속성을 가진 노드는 포함시키지 않는다.
3️⃣ 렌더 트리는 각 HTML 요소의 위치와 크기를 계산하는데 사용되고 브라우저 화면에 픽셀을 렌더링하는 페인팅에 사용된다.
브라우저 렌더링은 반복되어 실행되기 때문에 다음과 같은 경우 레이아웃(위치와 크기)계산과 페인팅 과정이 재실행된다.
1. 자바스크립트에 의한 노드 추가 및 삭제
2. 브라우저 창 리사이징에 의한 뷰포트 크기 변경
3. 레이아웃 변경을 발생시키는 CSS 코드(display, position, margin 등)
DOM을 생성해 나가는 렌더링 엔진은 중간에 script
태그를 만나면 역시 DOM 생성을 일시적으로 중단한다.
1️⃣ 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘긴다.
2️⃣ 자바스크립트 엔진은 자바스크립트 코드를 파싱해 AST(Abstract Syntax Tree) 추상적 구문 트리를 생성한다.
3️⃣ AST는 인터프리터가 실행할 수 있는 바이트 코드로 변환되고 인터프리터에 의해 실행된다.
DOM API를 사용하는 경우 DOM 트리가 모두 생성되었을 때 자바스크립트 파일을 실행하는 것이 적합하다.
자바스크립트를 닫는 body
태그 바로 위에 위치시키는 것이 바로 그 이유다.
자바스크립트 파싱에 의해 DOM 생성이 중단되는 경우를 해결하고자 HTML5부터 script
태그에 async
와 defer
속성이 추가되었다.
async와 defer 속성을 사용하면 HTML 파싱과 외부 자바스크립트 로드가 비동기적으로 진행된다.
두 속성은 외부 자바스크립트 파일을 로드하는 경우에만 사용할 수 있다는 것을 명심하자.
async
async 속성은 자바스크립트가 로드된 직후 파싱과 실행이 바로 진행되며, 이 과정에서 HTML 파싱이 중단된다.
여러 script
태그에 async
속성을 지정하면 로드가 완료된 자바스크립트부터 파싱과 실행이 진행되기 때문에 자바스크립트 실행 순서가 보장되지 않는다.
defer
defer 속성은 DOM 트리 생성이 완료된 직후 자바스크립트 파싱과 실행이 진행된다.
DOM 생성이 완료된 후 실행되어야 하는 자바스크립트의 경우 defer
속성을 사용할 수 있겠다.
자바스크립트 코드에서 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우 DOM과 CSSOM은 변경사항을 반영해 업데이트 되며, 렌더 트리 또한 재결합된다.
이 과정에서 레이아웃 - 페인트
가 다시 실행될 수도, 페인트
만 재실행 될 수도 있다.
요소의 크기나 위치가 변경되었을 때, 브라우저 창의 크기가 바뀌었을 때 발생한다.
레이아웃을 다시 계산하기 때문에 뒤따라오는 Paint와 합성(Compostie)도 다시 발생하게 된다. 이 과정이 빈번하게 발생하게 되면 당연히 렌더링 성능이 좋지 않을 것이다.
배경 이미지, 텍스트 색상, 그림자 등 레이아웃의 수치가 변하지 않는 스타일 변경이 일어났을 때 발생한다.
Layout 과정이 다시 일어나지 않기 때문에 성능상으로 이점이라고 볼 수 있다.
Layer의 합성만 다시 발생한다.
Layout, Paint 과정을 수행하지 않기 때문에 성능상으로 가장 큰 이점을 갖는다.
1. 크롬 브라우저의 경우 Layout 과정 이후 브라우저가 레이어를 생성한다.
2. 렌더 트리의 노드 객체는 생성된 레이어에 포함되게 된다.
3. 레이어 역시 트리구조로 생성이 되며, Print 과정에서 각 레이어를 그려준다.
4. 하나의 비트맵으로 합성해 페이지를 완성한다.
* Layer : 페인팅할 영역을 나누어 놓는 것을 의미한다.
* 비트맵 : 디지털 이미지를 저장하는 데 쓰이는 이미지 파일 포맷
사용자 입장에서 똑같이 동작하는 애니메이션일지라도 어떤 속성을 써서 구현하느냐에 따라 Layout, Print, Composite 과정을 각각 다르게 거친다.
따라서 렌더링 성능을 위해서라면 Layout 단계를 최소화하고 Composite만 일어나는 CSS 속성을 사용할 것을 고민해봐야 한다.
CSS Triggers에서 브라우저의 렌더링 엔진별로 각 CSS가 어떤 과정을 거쳐 구현되는지 상세하게 나와있다. 앞으로 렌더링 성능 개선을 고민할 때 사용해봐야겠다.