mdn을 뒤적거리다 우연찮게 https://developer.mozilla.org/ko/docs/Web/Performance/How_browsers_work를 읽게 되었다. 브라우저의 동작원리를 공부하기에 좋은 시작점이 될 수 있는 자료인것 같다.
목차
펼치기
웹페이지 로딩
처음 우리가 알고 있는건 웹페이지의 URL이다. URL을 가지고 페이지를 인터넷에서 로딩하기까지는 어떤 과정을 거치게 될까?
DNS 조회
URL은 사실 실제 서버의 주소가 아니다 정확하게는 IP주소를 알아야 해당 자원까지 도달할 수 있게 된다. URL과 매핑된 실제 IP주소를 알아내는 과정을 DNS 조회라고 한다. 간단하게는 로컬컴퓨터에 해당 매핑을 저장해 둘 수 있겠지만, 대부분의 경우는 DNS 서버에 요청을 보내서 해당 IP주소를 알아내게 된다. DNS는 계층적으로 구성되어 있으며, Root name 서버, 최상위 도메인(TLD) 서버, 권한 있는 네임서버, 로컬 DNS 서버, 퍼블릭 DNS 서버로 구성된다.
URL을 조회할때마다 이 모든 과정을 거치는 것은 비효율적이기 때문에, 보통 로컬이나 퍼블릭 DNS서버에 캐싱된 정보를 사용하게 된다. 만약 없다면, Root -> TLD - > 권한 있는 네임서버 순으로 조회를 하게 된다.
TCP 연결
이렇게 IP주소를 알아냈다면, 이제 해당 서버와 통신을 하기 위해 TCP 연결을 맺어야 한다. TCP 연결은 3-way handshake로 이루어져 있다. handshake는 서로가 서로 통신할 준비가 되었는지 확인하는 과정이다.
- 클라이언트가 서버에 SYN 패킷을 전송한다.
- 서버는 클라이언트의 SYN 패킷을 받고, SYN-ACK 패킷을 전송한다.
- 클라이언트는 서버의 SYN-ACK 패킷을 받고, ACK 패킷을 전송한다.
TLS 협상
보안성있는 연결을 위해서 TLS 협상을 진행한다. TLS는 암호화된 연결을 제공해준다. TLS는 클라이언트와 서버가 서로 지원하는 암호화 알고리즘을 협상하고, 세션 키(대칭키)를 교환하는 과정을 거친다. 이 과정은 클라이언트와 서버가 서로 신뢰할 수 있는지 확인하는 과정이기도 하다.
우선 서버는 CA에게 검증된(비밀키로 암호화된) 인증서(해시처리된 서버 공개키 포함)를 가지고 있어야 한다.
clientHello
메시지를 서버에 전송한다.- 서버는
serverHello
메시지와serverCertificate
,serverHelloDone
메시지를 전송한다. - 클라이언트는 인증서를 검증하고, 서버의 공개키로 암호화된
pre-master secret
을 전송한다. 클라이언트는 이 과정에서 가지고 있는 CA의 공개키로 인증서를 검증한다. 그리고 서버의 공개키로pre-master secret
를 암호화해 전송한다. - 서버는 클라이언트의 공개키로 암호화된 pre-master secret를 복호화한다. 그리고 서버는
changeCipherSpec
메시지와serverFinished
메시지를 전송한다. - 클라이언트는
changeCipherSpec
메시지와clientFinished
메시지를 전송한다.
첫 요청
이렇게 연결이 완료되면, 클라이언트는 서버에 첫 요청을 보낸다. 대게 HTML 문서를 요청하게 된다. 이 과정에서도 한번에 데이터가 들어오는게 아니라 청크로 나눠서 들어오게 된다. 첫 번째 컨텐츠 청크는 일반적으로 14kb크기이고 여기까지 받는데 걸린시간을 Time to First Byte(TTFB)
라고 한다. 이후의 청크는 TCP 슬로우 스타트 알고리즘에 따라 전송되는 데이터의 양이 늘어나게 된다. (이거 보면 좋음)
(그래서 첫 페이지의 크기를 14kb 이하로 유지하는게 좋다고 들은것 같다)
파싱
브라우저가 첫 번째 데이터 청크를 받으면 구분 분석을 하기 시작하고, 이 정보를 토대로 DOM 과 CSSOM을 생성하고 이를 토대로 랜더링을 한다. 심지어 요청된 HTML의 크기가 14kb보다 크더라도, 브라우저는 먼저 받은 데이터를 파싱하고 렌더링을 시작한다.
DOM (Document Object Model)
DOM은 HTML 문서의 구조화된 표현이다. DOM은 트리구조
로 이루어져 있으며, 각 노드는 객체로 표현된다. 브라우저는 HTML 문서를 파싱하면서 DOM을 생성한다. DOM은 브라우저의 자바스크립트 엔진에 의해 조작될 수 있다.
DOM 노드의 개수가 많아질수록, 트리를 만드는데 더 오랜 시간이 걸린다. 이러한 구문분석은 CSS파일같은 논 블로킹 자원을 만났을때는 구문 분석이 중단되지 않지만 따로 defer
나 async
속성이 없는 script 태그를 만나면 구문 분석이 중단되기 때문에 과도한 스크립트는 주요한 병목이 될 수 있다.
프리로드 스캐너
메인스레드의 DOM 구문분석외로 프리로드 스캐너에 의해 외부자원에 대한 요청을 미리 할 수 있다. 그렇기 때문에 해당 라인에 구문분석기가 다다를때 쯤엔 이미 자원을 전송받고 있거나 전송이 완료되었을 수 있다. 이런 최적화는 구문분석의 블록킹을 줄여준다.
다만, CSS를 다운로드 받는 동안에는 HTML 파싱이나 다운로드를 블락하지 않는다고 한다. 다만, Javascript는 실행되지 않는다고 한다. (나는 최대한 HTML이나 CSS를 사용할 수 있는 부분에서는 그렇게 하는게 접근성과 성능, 유지보수에 유리하다고 하는데, 여기서 또 깨달아 버렸다.)
CSSOM (CSS Object Model)
CSSOM은 CSS 문서의 구조화된 표현이다. 브라우저는 CSS 파일을 파싱하면서 CSSOM을 생성한다. CSSOM은 DOM과 유사하게 트리구조로 이루어져 있고 서로 독립적인 자료구조
다. 현재 브라우저는 CSSOM을 생성하는데 매우 매우 빨라서 성능 최적화의 관점에서 중요한 부분은 아니라고 한다.
Javascript 컴파일, Accessibility tree 구축
그 외로 다운로드된 Javascript파일을 구분분석해 추상 구문 트리를 만들고 인터프리터에게 넘긴다. DOM을 이용해 AOT(Accessibility Object Tree)를 만든다. AOT는 DOM의 시맨틱 버전이라고 할 수 있다. 때문에 DOM이 업데이트 되면 같이 업데이트 된다. AOT는 스크린리더나 브라우저의 접근성 기능을 위해 사용된다.
렌더
랜더링 과정에서는 스타일, 레이아웃, 페인트 그리고 합성이 포함된다. 경우에 따라 CPU 혹은 GPU를 사용해 렌더링을 한다.
렌더 트리(계산된 스타일 트리)
DOM과 CSSOM을 합쳐 렌더 트리를 만든다. 랜더 트리는 루트노드에서 부터 눈에 보이는
노드를 순회하며 만들어진다. 랜더트리는 모든 노드의 내용과 해당 노드에 대한 스타일 정보를 가지고 있다.
레이아웃
레이아웃은 랜더 트리의 각 노드의 크기와 위치를 결정하고, 페이지에서 각 객체의 크기와 위치를 계산한다. 후에 다시 페이지의 부분이나 전체에 대한 크기나 위치의 변경을 리플로우
라고 한다. 리플로우는 브라우저의 성능에 영향을 미치는 중요한 요소이다.
때문에 랜더트리가 만들어지고 난 후에 레이아웃 과정에서 뷰포트의 크기에 대한 정보가 필요하다.
페인트
레이아웃 단계에서 계산된 각 박스를 실제 화면의 픽셀로 변환하는 과정이다. 첫 페인팅 작업을 First Meaningful Paint(FMP)
라고 한다.(Lighthouse에서 더 이상 지원되지 않는 지표라는걸 보면, LCP나 FCP를 고려하는게 더 나을듯 하다) 이런 일련의 랜더링 과정은 60fps를 유지하기 위해 약 16ms안에 완료되어야 한다. 매 16ms 마다 몇백만개의 픽셀을 새로 그린다는건 상당히 큰 작업이다.
때문에 페인팅 작업은 일반적으로 몇 개의 레이어로 구분되고 합성된다. 이런 레이어 구분은 특정 태그
를 사용하거나 특정 속성
에 의해 결정된다. 하지만 레이어는 성능을 향상시키긴 하지만 메모리 관리 측면에서는 비싼작업이 될 수 있다.
합성
문서의 각 섹션이 다른 레이어에서 그려질때, 겹처진 섹션을 올바른 순서로 화면에 그리는것을 보장하기 위해 합성이 필요하다. 이렇게 레이어가 어떻게 되어 있는지 개발자 도구에서도 확인할 수 있다.
(그러다가 요소들 간에 z-index
가 안 먹혔던게 생각이 나서, 혹시 그러면 랜더링 레이어와 연관이 있는건가 싶어서 찾아봤다. 스태킹 컨텍스트
를 만들어 내는 조건과 랜더링 레이어
를 만들어 내는 조건이 겹치는 부분이 있어서 헷갈리긴 했지만, 결국은 서로 다른 개념이었다. 나중에 블로그에 또 쓰겠다.)
상호작용
이렇게 메인 스레드가 페이지를 그리는 것을 완료하면 사용자와의 상호작용이 가능해진다. 하지만 지연된 Javascript를 다운하거나, onload
이벤트가 발생할 때 코드가 실핸되면 여전히 다른 상호작용이 불가능 할 수 있다.
DNS 조회와 SSL 연결이 이루어지는 첫 요청부터 페이지가 상호작용할 준비가 될 때까지 걸니는 시간을 TTI(Time to Interactive)
라고 한다. 대게 FCP(First Contentful Paint)
이후 사용자의 상호작용에 50ms이내로 반응할때를 상호작용이 가능한 시간으로 본다. 때문에 FCP
이후에 메인스레드를 점유하는 작업은 피하는게 좋겠다.