출처
https://developer.mozilla.org/ko/docs/Web/Performance/How_browsers_work
https://developer.chrome.com/blog/inside-browser-part3/
https://web.dev/optimize-javascript-execution/
사용자가 URL을 브라우저에 입력하면, 초기에는 이런 과정을 거친다
https://velog.io/@loevray/CS-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC1-Http-IP-DNS
여기부터 쭉따라가면 됨...
웹서버에서 응답이오면, 브라우저는 초기http request(get)을 요청.
보통 14kb로 옴.
=> TCP slow start. 네트워크 대역폭 크기 파악을 위해 14kb부터 2배수씩 증가시킴. 혼잡하면 2배증가 중단.
이후에 브라우저는 화면에 파일을 보여주기위한 작업을 거치는데, 이를 Critical Rendering Path(CRP)라고 지칭한다.
이 녀석은 DOM, CSSOM으로 파싱 => 렌더트리 생성 => 레이아웃 => 페인트 과정으로 진행된다.
CRP의 순서를 따라가보자!
파일의 첫 청크를 받으면, 바로 parsing에 들어감.
브라우저 내부에서 html을 이용하기 쉽게 DOM이나 CSSOM으로 파싱해줌.
js로 조작가능한 API! document.querySelector()
같이 DOM을 조작할수 있는 메소드가 DOM API다.
과정은 크게 토큰 => 노드 => ObjectModel순.
토큰이란 의미있는 단어의 최소 단위임.
html
을 루트로 시작해서 트리가 만들어 진다.
노드가 많을수록 당연히 DOM생성에 시간이 많이 소요된다.
img
태그같은 논 블로킹자원을 만나면 자원을 요청해놓고 계속 파싱한다.
하지만 async나 defer가 사용되지 않은 script태그는 블로킹을 발생.
왜? =>document.write()
처럼 DOM 구조를 변경하는 기능이 있기 때문.
이를 도와주는 프리로드 스캐너라는 기능도 있지만, 병목현상은 막기 어렵다.
신기한 점 : html parser는 잘못된 html태그도 바로잡아준다.
Hi! <b>I'm <i>Chrome</b>!</i>
같이 <i>
보다 <b>
를 먼저 닫았음에도 오류가 나지않고
Hi! <b>I'm <i>Chrome</i></b><i>!</i>
처럼 정상적으로 파싱된다.
이는 파서의 오류 처리 및 이상한 경우 소개에서 확인할 수 있다.
=> DOM과 실제 코드가 다를 수 있음
위에서 소개한 블로킹을 완화시켜주는 기능이다.
브라우저는 멀티쓰레드다. 여러 작업을 동시에 할 수 있지!
그 덕분에 css, js, 웹 폰트같이 우선순위가 높은 외부 자원들을 선점해서 요청을 보내준다.
=> 블로킹을 줄여준다
만약 DOM파싱이 됐다면, CSSOM파싱에 들어감.
DOM만으로는 페이지가 어떻게 보일지 결정할 수 없기 때문.
각 노드마다 어떻게 스타일이 계산되는지 정한다
CSS가 작성되지 않아도, 태그마다 기본 스타일이 있기에 계산해야함.
이렇게 CSSOM도완성됐다면 렌더트리로 DOM과 CSSOM이 합성됨.
여기서 문제!
display: none
visibility: hidden
둘 중 렌더트리에 들어가는 녀석은?
바로 visibility: hidden
.
display: none
은 자리를 차지하지 않기에 렌더트리에 합성될 필요가 없다.
처음 노드의 사이즈와 위치가 결정되는것이 레이아웃
메인 스레드가 계산된 스타일을 사용하여 DOM트리를 살펴보고 레이아웃을 그린다.
그 다음부터는 리플로우
예를들어 이미지를 불러오기전에 렌더트리가형성되고(레이아웃), 이미지가 로드되서 노드들의 크기가 재조정 됨(리플로우)
다 된거 아닌가? 싶지만, 레이아웃만으로는 그림을 그릴 수 없다.
크기,모양,위치 뿐이 아닌 순서도 고려해야하기 때문.
예를들어 z-index
. 만약 html이 작성된 순서대로 배치한다면, z-index
는 무시 될것이다.
메인 스레드가 레이아웃을 보고 페인트 레코드를 작성하는 모습이다.
이렇게 한번에 작성되면 좋겠지만, 현대의 웹페이지는 스타일 변경이 자주 일어난다
이런 업데이트에는 비용이 많이든다.
예를들어 레이아웃 트리에서 변경이 이루어 졌다면, 영향을 받는 노드들은 다시 페인트 순서를 결정해야 함.
특히 애니메이션을 생각해보자.
게이밍 모니터를 제외하면 화면의 주사율은 보통 60hz
따라서 60frame의 애니메이션을 만들어야 버벅임이 없다.
하지만, 스타일 업데이트로 인해 영향을 받는 모든 노드에 업데이트가 일어난다면, 버벅임을 피할순 없다
JANK = 버벅임
메인스레드를 JS가 점유할때도 마찬가지다.
이를 해결하기위해 존재하는 메소드가 바로requestAnimaitonFrame()
이다.
단위 프레임마다 JS점유를 쪼개서 프레임 일부를 점유하는걸 예방해준다.
자세한건 여기 ▼
https://velog.io/@loevray/%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-6-JS%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84
레이아웃 트리에서 레이어를 분리해 그리는 작업도 있음.
canvas, video, opacity, transfrom...
등 . 얘네들은 GPU사용해서 빠르게 그려줌
이제 다 왔다. 화면은 결국 픽셀의 조합으로 이루어져있기에, 정보의 픽셀화가 필요하다.
이를 래스터화라고 한다.
예전에는 보이는 부분만 래스터화했다. 스크롤 하면 그 부분만 또 래스터화 하고.
최신 브라우저는 페이지의 일부를 레이어로 분리하는 방식을 택했다.
이렇게 분리된 레이어는 별도로 마련된 합성스레드에서 레이어들을 합성한다.
스크롤이 발생해도 이미 레이어 => 래스터화 진행되있으니, 합성만 하면된다.
분명 페이지의 일부를 레이어로 분리 한다고 했다.
그렇다면, 어떤 요소가 레이어에 있어야 할까?
레이아웃 트리에서 레이어 트리를 결정한다.
참고로 레이어는 당연히 비용을 많이 발생시킨다. 속도 향상에 도움을 줄 수 있지만, 메모리가 많이듬!
레이어화된 velog 포스팅 화면.
크롬 개발자도구에서 레이어화된 화면을 볼 수 있다.
3d로 나타나며, 페인트 순서를 한눈에 볼 수 있음!
레이어 트리도 생성됐고, 페인트 순서도 결정됐다.
메인 스레드는 해당정보를 합성 스레드에 커밋한다.
합성 스레드는 레이어를 래스터화하고, 타일로 나눈다.
=> 보이는 화면보다 레이어 하나가 더 클수 있다. 따라서 나눠서 저장한다.
각 타일은 래스터 스레드로 넘어가고, GPU에 저장된다.
이제 그릴 차례다.
합성된 프레임을 만들기 위해 합성 스레드에 draw quad를 이용한다.
draw quad는 사각형으로 잘린 타일을 다시 합치는 것.
합성 스레드가 draw quad(타일)을 이용해 합성 프레임을 생성하여 gpu로 다시 보내 렌더링을 마친다!
파싱 => DOM+CSSOM(렌더트리) => 레이아웃 => 페인트 => 래스터화(레이어,타일) => 합성 => 렌더링!의 과정을 거쳤다.
브라우저의 렌더링 동작이 단순하지 않을것 같긴 했지만, 이정도로 복잡할 줄은 몰랐다.
그리고 왜 알아야하는지 알게됐다.
동일한 계산을 반복한다면, 렌더링 과정을 다시 거쳐야하는 수고로움이 발생한다.
이는 생각보다 큰 낭비다.