웹 퍼포먼스의 두 가지 주요 문제
- 지연과 관련된 문제
- 레이턴시는 신속한 페이지 로딩이 가능한 가장 큰 걸림돌이다.
- 개발자의 목표 : 사이트가 최대한 빨리 로딩되도록 하거나 최소한 매우 빠르게 로딩되도록 함으로써 사용자가 요청된 정보를 최대한 빨리 얻을 수 있도록 하는 것.
- 브라우저는 대부분 싱글 스레드
- 다른 작업을 수행하기 전에 처음부터 끝까지 작업을 수행한다.
- 원활한 상호작용을 위해 개발자의 목표는 부드러운 스크롤에서 터치에 대한 반응성까지 사이트 상호작용을 수행하는 것이다. 메인 스레드가 모든 작업을 완료하고 사용자의 조작을 처리할 수 있도록 하는 것이 중요하다.
- 브라우저의 단일 스레드 특성을 이해하고 가능한 경우 메인 스레드의 책임을 최소화하여 렌더링이 원활하고 상호작용에 대한 응답이 즉시 이루어지도록 함으로써 웹 성능을 향상시킬 수 있다.
Navigation
네비게이션은 웹 페이지를 로드하는 첫 번째 단계로, 사용자가 주소 표시줄에 URL을 입력하고 링크를 클릭하며 양식을 제출하거나 다른 작업을 수행하여 페이지를 요청할 때마다 발생한다.
웹 성능의 목표 중 하나는 내비게이션이 완료되는 데 걸리는 시간을 최소화하는 것이다.이상적인 상황에서는 보통 이 작업에 그리 오래 걸리지 않지만 지연과 대역폭 때문에 딜레이가 발생할 수 있다.
DNS 검색(DNS lookup)
웹 페이지로 이동하는 첫 번째 단계는 해당 페이지의 자산이 있는 위치를 찾는 것입니다.https://example.com
에 네비게이트 하는 경우에 HTML 페이지는, IP 주소의 서버에 있습니다.93.184.216.34
이 사이트를 방문한 적이 없는 경우 DNS 조회가 필요하다.
브라우저가 DNS 룩업을 요구하면 최종적으로 네임서버에 의해 필드화 되고 네임서버는 IP 주소로 응답한다.이 초기 요구 후 IP는 일정 시간 동안 캐시되기 때문에 네임서버에 다시 접속하는 대신 캐시에서 IP 주소를 취득함으로써 후속 요구의 속도가 향상된다.
DNS 검색은 보통 페이지 로드에 대해 호스트 이름별로 한 번만 수행하면 된다.
- 단, 요청된 페이지가 참조하는 일의의 호스트명 마다 실시해야 하므로 글꼴, 이미지, 스크립트, 광고 및 메트릭이 모두 다른 호스트 이름을 가진 경우 각각에 대해 DNS 검색을 수행하게 된다.
- 특히 모바일네트워크에서의 퍼포먼스에 문제가 될 수 있다.
- DNS 룩업이 전화기에서 셀타워로 전송되어야 하는데 전화기, 셀타워 및 네임서버 간의 거리에 따라 상당한 지연이 발생할 수 있기 때문이다.
TCP 핸드쉐이크
한번 IP 주소가 알려지면, 브라우저는 TCP 세 개의 단계를 거쳐 서버와의 연결을 설정한다.
- 이 메커니즘은 통신을 시도하는 두 개체(브라우저/웹 서버)가 데이터를 전송하기 전에 네트워크 TCP 소켓 연결의 매개 변수를 협상할 수 있도록 설계되었다.
- 주로 HTTPS를 통해 데이터를 전송하는 경우에 사용됩니다.
TCP의 세 단계 핸드셰이킹 기술은 일반적으로 "SYN-SYN-ACK" 또는 더 정확하게는 "SYN, SYN-ACK, ACK"로 알려져 있다.
- 두 대의 컴퓨터 간에 TCP 세션을 네고하고 시작하기 위해 TCP에 의해 전송되는 세 개의 메시지가 있기 때문이다.
- 이는 각 서버 간에 추가적으로 세 개의 메시지가 오고 가야하며, 아직 요청이 이루어지기 전입니다.
TLS 네고시에이션
HTTPS를 통해 설정된 보안 연결의 경우, 추가적인 핸드셰이크가 필요합니다.
- 더 정확하게는 TLS 네고시에이션은 통신을 암호화할 암호 방식을 결정하고, 서버를 인증하며, 실제 데이터 전송을 시작하기 전에 안전한 연결이 설정되었음을 확인한다.
- 이를 위해 실제 컨텐츠 요청이 전송되기 전에 서버와의 추가적인 세 번의 왕복 통신이 필요하다.
- 안전한 연결은 페이지 로드에 시간을 추가하지만 브라우저와 웹 서버 간에 전송되는 데이터가 제3자에 의해 해독될 수 없기 때문에 사용하는 것이 보안상에 좋다.
Response
웹 서버와의 연결이 확립되면 브라우저는 사용자를 대신해 초기 HTTP GET 요청을 보낸다. 대부분의 경우 웹 사이트에서는 HTML 파일이 요청되고, 서버가 요청을 받으면, 관련된 응답 헤더와 HTML 내용을 응답으로 보낸다.
이 초기 요청에 대한 응답은 수신된 첫 번째 데이터 바이트를 포함한다. Time to First Byte (TTFB)는 사용자가 요청을 한 시점(예: 링크를 클릭함)부터 이 첫 번째 HTML 패킷을 받는 시점까지의 시간을 나타낸다.
- 첫 번째 콘텐츠 청크는 일반적으로 14KB의 데이터.
TCP Slow Start 및 14KB 규칙
첫 번째 응답 패킷은 14KB이다. 이는 네트워크 연결의 속도를 균형잡기 위한 TCP Slow Start 알고리즘의 일부분으로, Slow Start는 네트워크의 최대 대역폭을 결정할 때까지 전송되는 데이터의 양을 점진적으로 증가시킨다.
- TCP Slow Start는 초기 패킷 수신 후, 서버가 다음 패킷의 크기를 약 28KB로 두 배로 증가시킨다. 이후의 패킷은 미리 정해진 임계값에 도달하거나 혼잡이 경험될 때까지 크기가 증가한다.
- TCP Slow Start는 혼잡을 피하기 위해 네트워크의 성능에 적합한 속도로 전송 속도를 점진적으로 높이는 메커니즘이다.
Congestion control
서버가 TCP 패킷으로 데이터를 보낼 때, 사용자의 클라이언트는 수신 확인인 ACK(acknowledgements)를 반환하여 전달을 확인한다.
연결은 하드웨어와 네트워크 조건에 따라 제한된 용량을 갖는데, 서버가 너무 많은 패킷을 너무 빠르게 보내면 드롭될 수 있다(확인 응답이 없게 된다). 서버는 이를 누락된 ACK로 인식하고 기록한다. Congestion control 알고리즘은 보낸 패킷과 ACK의 흐름을 사용하여 전송 속도를 결정한다.
Parsing
브라우저가 첫 번째 데이터 청크를 받으면, 수신한 정보를 파싱하여 DOM과 CSSOM으로 변환하는 작업을 시작할 수 있다.
- 파싱은 네트워크를 통해 수신한 데이터를 브라우저가 사용하는 문서 객체 모델(DOM)과 CSS 객체 모델(CSSOM)으로 변환하는 과정이다(renderer가 화면에 웹 페이지를 그리기 위해 필요한 과정).
DOM은 브라우저의 마크업에 대한 내부 표현이다.
- DOM은 브라우저에서 JavaScript를 통해 다양한 API를 통해 조작할 수 있습니다.
요청한 페이지의 HTML이 초기 14KB 패킷보다 크더라도 브라우저는 수신한 데이터를 기반으로 파싱을 시작하고 화면에 렌더링을 시도한다(이것이 웹 성능 최적화에 중요한 이유).
- 즉, 브라우저가 페이지를 렌더링하기 위해 필요한 모든 것, 또는 페이지의 템플릿인 최초 렌더링에 필요한 CSS와 HTML을 최초 14KB에 포함시키는 것이다.
- 하지만 화면에 무엇이든 그려지기 전에 HTML, CSS, 그리고 JavaScript가 파싱되어야 한다.
DOM 트리 구축 (1단계)
렌더링 경로에서 중요한 다섯 가지 단계가 있다.
1단계) HTML 마크업을 처리하고 DOM 트리를 구축하는 것.
- HTML 파싱은 토큰화(tokenization)와 트리 구축(tree construction)을 포함한다.
- HTML 토큰은 시작 태그와 종료 태그, 속성 이름과 값 등이 포함된다.
- 문서가 유효한 형식이라면, 파싱은 간단하고 빠르게 수행됩니다. 파서는 토큰화된 입력을 문서에 파싱하여 DOM 트리를 구축합니다.
DOM 트리는 문서의 내용을 설명한다.
- html 요소는 문서 트리의 첫 번째 태그이자 루트 노드이다.
- 트리는 다양한 태그 간의 관계와 계층 구조를 반영한다. 다른 태그 내에 중첩된 태그는 자식 노드이고 DOM 노드의 수가 많을수록 DOM 트리를 구축하는 데 더 많은 시간이 소요된다.
파서는 이미지와 같은 비차단(non-blocking) 리소스를 찾으면 브라우저는 해당 리소스를 요청하고 파싱을 계속한다.
- CSS 파일을 만나면 파싱이 계속될 수 있지만
- script 태그는(특히 async나 defer 속성이 없는 경우) 렌더링을 차단하고 HTML 파싱을 일시 중지한다.
- 브라우저의 사전 로드 스캐너(preload scanner)가 이 과정을 가속화하지만, 과도한 스크립트는 여전히 중요한 병목 현상의 원인이 될 수 있다.
Preload scanner
브라우저가 DOM 트리를 빌드하는 동안 이 과정은 메인 스레드를 점유한다.
이 과정에서 Preload scanner는 사용 가능한 콘텐츠를 분석하고 CSS, JavaScript, 웹 폰트와 같은 최우선순위 리소스를 요청한다.
기능
- 외부 리소스에 대한 참조를 파서가 찾을 때까지 요청을 기다릴 필요가 없습니다.
- 리소스를 미리 백그라운드에서 검색하고 메인 HTML 파서가 요청한 에셋에 도달할 때 이미 다운로드가 완료되었거나 이미 전송 중일 수 있습니다. Preload scanner가 제공하는 최적화는 블록을 줄여줍니다.
- 클립보드에 복사 이 예제에서는 메인 스레드가 HTML과 CSS를 파싱하는 동안 Preload scanner가 스크립트와 이미지를 찾고 다운로드를 시작한다.
- 스크립트가 프로세스를 차단하지 않도록 하려면 async 속성을 추가하거나, JavaScript 파싱 및 실행 순서가 중요하다면 defer 속성을 추가하면 된다.
CSS를 얻기 위해 기다리는 것은 HTML 파싱이나 다운로드를 차단하지 않지만, JavaScript는 자주 CSS 속성이 요소에 미치는 영향을 쿼리하는데 사용되기 때문에 JavaScript를 차단한다.
CSSOM 구축 (2단계)
2단계) CSS를 처리하고 CSSOM 트리를 빌드하는 것
DOM과 CSSOM은 모두 트리이고 독립된 데이터 구조이다.
- 브라우저는 CSS 규칙을 이해하고 사용할 수 있는 스타일의 맵으로 변환한다.
- 브라우저는 CSS 내의 각 규칙 세트를 살펴보고 CSS 셀렉터에 따라 부모, 자녀 및 형제 관계를 가진 노드의 트리를 만듭니다.
HTML과 마찬가지로 브라우저는 수신된 CSS 규칙을 사용할 수 있는 것으로 변환해야 합니다.따라서 CSS를 위해 HTML-to-object 프로세스를 반복한다.
CSSOM 트리에는 사용자 에이전트 스타일시트의 스타일이 포함된다.
- 브라우저는 노드에 적용할 수 있는 가장 일반적인 규칙에서 시작하여 보다 구체적인 규칙을 적용하여 계산된 스타일을 반복적으로 조정합니다(속성 값을 CASCADE 한다).
CSSOM의 구축은 매우 고속이며 현재 개발 도구에서 고유한 색상으로 표시되지 않는다.
- 오히려 개발자 도구의 "Recalculate Style"은 CSS 해석, CSSOM 트리 구축 및 계산된 스타일을 재귀적으로 계산하는 데 걸리는 총 시간을 보여준다.웹 퍼포먼스 최적화에서는 일반적으로 CSSOM을 작성하는 데 걸리는 시간이1개의 DNS 룩업에 걸리는 시간보다 짧기 때문에 효과가 낮다.
Another Process
JavaScript 컴파일
CSS가 파싱되고 CSSOM이 생성되는 동안 다른 assets, JavaScript 파일을 포함하여 다운로드 된다(preload scanner).
JavaScript는 해석되고 컴파일되고 파싱되며 실행된다.
- 스크립트는 추상 구문 트리(abstract syntax tree)로 파싱된다.
- 일부 브라우저 엔진은 추상 구문 트리를 인터프리터에 전달하여 메인 스레드에서 실행되는 바이트코드로 출력합니다. 이를 JavaScript 컴파일이라고 합니다.
접근성 트리 구축
브라우저는 보조 기기가 콘텐츠를 파싱하고 해석하는 데 사용하는 접근성 트리(accessibility tree)를 구축한다.
- 접근성 객체 모델(accessibility object model, AOM)은 DOM의 의미론적 버전과 같다.
- 브라우저는 DOM이 업데이트되면 접근성 트리를 업데이트한다.
- 접근성 트리는 보조 기술 자체에서 수정할 수 없다.
AOM이 구축되기 전까지는 콘텐츠가 스크린 리더에게 접근 가능하지 않습니다.
Render
렌더링 단계에는 스타일, 레이아웃, 페인트 및 경우에 따라 컴포지팅이 포함된다.
인터프리트 스텝에서 작성된 CSSOM 트리와 DOM 트리는 렌더 트리로 결합되어 표시 가능한 모든 요소의 레이아웃을 계산하기 위해 사용된다(이 트리는 화면에 그려진다).
- 경우에 따라서는 컨텐츠를 독자적인 레이어로 승격해 컴포지트 할 수 있기 때문에, CPU 대신에 GPU에 화면의 일부를 도장하는 것으로 퍼포먼스를 향상시켜, 메인 스레드를 해방할 수 있습니다.
Style (3단계)
3단계) 작성된 CSSOM 트리와 DOM 트리는 렌더 트리로 만드는 것
- 계산된 스타일 트리(렌더 트리)는 DOM 트리의 루트에서 시작하여 표시되는 각 노드를 통과합니다.
- 표시되지 않는 태그(및 그 자녀, 및 기타 노드 등)
display: none
예를 들어,script { display: none; }
사용자 에이전트 스타일시트에 있는 은 렌더링된 출력에 나타나지 않으므로 렌더링 트리에 포함되지 않는다.
- 다음 노드
visibility: hidden
적용된 파일은 공간을 차지하기 때문에 렌더 트리에 포함한다.
표시되는 각 노드에는 CSSOM 규칙이 적용된다.
- 렌더 트리에는 표시되는 모든 노드가 콘텐츠 및 계산된 스타일로 유지된다.
- DOM 트리의 모든 가시적인 노드에 관련된 모든 스타일을 일치시키고 CSS CASCADE에 따라 각 노드의 계산된 스타일을 결정.
Layout (4단계)
4단계) 렌더 트리에서 레이아웃을 실행하여 각 노드의 형상을 계산하는 것.
- 레이아웃 : 렌더 트리의 모든 노드의 너비, 높이 및 위치, 그리고 페이지의 각 객체의 크기와 위치를 결정하는 과정.
- 리플로우(Reflow) : 페이지의 일부 또는 전체 문서의 이후 크기와 위치 결정을 의미.
렌더 트리가 구축되면 레이아웃이 시작.
- 렌더 트리는 어떤 노드가 표시되는지(보이든 안보이든)와 그들의 계산된 스타일을 식별하지만 각 노드의 크기나 위치를 알지 못한다.
- 브라우저는 렌더 트리의 루트에서 시작하여 노드의 정확한 크기와 위치를 결정하기 위해 렌더 트리를 탐색한다.
웹 페이지에서 거의 모든 것은 박스라 보면 된다.
- 다양한 장치와 데스크톱 환경 설정에 따라 무수히 많은 viewport 크기가 존재.
- 이 단계에서 뷰포트 크기를 고려하여 브라우저는 화면 상에 모든 다양한 박스의 크기를 결정.
- 레이아웃은 일반적으로 뷰포트를 기준으로 시작하여 body를 먼저 배치
- 각 요소의 상자 모델 속성을 사용하여 body의 자식 요소들의 크기를 결정.
- 이미지와 같이 크기를 알 수 없는 대체 요소들을 위한 공간을 제공.
노드의 크기와 위치가 처음으로 결정되는 것을 레이아웃이라 보면 된다.
이후의 노드 크기와 위치의 재계산은 리플로우라고 한다.
- 예를 들어, 우리의 예시에서는 초기 레이아웃이 이미지가 반환되기 전에 발생한다고 가정하면. 이미지의 크기가 알려진 후에 리플로우가 발생한다.
Paint (5단계)
5단계) 개별 노드를 화면에 그리는 것.
이 중 첫 번째로 발생하는 것을 first meaningful paint라 한다.
- 페인팅 또는 래스터화 단계에서 브라우저는 레이아웃 단계에서 계산된 각 상자를 화면에 실제 픽셀로 변환한다. -
- 페인팅은 텍스트, 색상, 테두리, 그림자 및 이미지와 같은 요소의 모든 시각적 부분을 화면에 그리는 작업을 포함.
- 브라우저는 이 작업을 매우 빠르게 처리해야 한다.
- 부드러운 스크롤링과 애니메이션을 보장하기 위해 스타일 계산, 리플로우 및 페인팅을 포함한 메인 스레드에서 처리되는 모든 작업은 브라우저에서 16.67ms보다 더 짧은 시간에 완료되어야 한다.
- 2048 x 1536 해상도의 iPad의 경우, 화면에 그려야 할 픽셀이 3,145,000개가 넘는데 이걸 매우 빠르게 페인팅할 수 있어야 한다. 때문에 화면에 그리는 작업은 일반적으로 여러 개의 레이어로 분할된다. 이러한 경우에 compositing이 필요하다.
- 페인팅은 레이아웃 트리의 요소들을 레이어로 분리할 수 있다.
- 레이어로 내용을 활성화하면(메인 스레드가 아닌 GPU에서), 페인트, 리페인트 성능이 향상됩니다.
Compositing
문서의 일부가 서로 겹쳐진 다른 레이어에서 그려지는 경우, 이들이 올바른 순서로 화면에 그려지고 내용이 올바르게 렌더링되도록 보장하기 위해 합성(compositing)이 필요하다.
페이지가 계속해서 에셋을 로드하면서 리플로우(reflow)가 발생할 수 있다.
이 때 리플로우는 다시 페인트와 리-컴포짓(re-composite)를 일으킨다.
- 이미지의 크기를 정의했다면 리플로우가 필요하지 않았을 것이고, 다시 페인트가 필요한 레이어만 다시 페인트되고 필요한 경우에만 합성된다.
- 이미지 크기를 포함하지 않았다면 이미지가 서버에서 가져와지면 렌더링 과정이 레이아웃 단계로 다시 돌아가고 다시 시작됩니다.
Interactive
주요 스레드가 페이지를 그린 후에는 "모든 설정이 끝났다"고 생각할 수 있겠지만 꼭 그렇진 않다.
만약 로드에 JavaScript가 포함되어 있고, 이것이 onload 이벤트가 발생한 후에만 실행되도록 지연되었다면, 메인 스레드가 바쁘게 돌아가고 스크롤링, 터치 및 기타 상호작용에 대해 사용 가능하지 않을 수 있다.
Time to Interactive (TTI)는 첫 번째 컨텐츠가 그려진 후부터 페이지가 50ms 이내에 사용자 상호작용에 응답하는 지점까지의 시간을 측정하는 것인데, 메인 스레드가 JavaScript를 파싱, 컴파일, 실행하는 동안 사용 가능하지 않아서 50ms 이내에 사용자 상호작용에 응답할 수 없는 경우가 있다.
예를 들어, 어떤 이미지가 빠르게 로드되었지만, 다른 스크립트 파일인 anotherscript.js가 2MB이고 사용자의 네트워크 연결이 느리다면, 사용자는 페이지를 빠르게 볼 수 있겠지만 해당 스크립트가 다운로드되고 파싱되고 실행될 때까지 스크롤을 할 수 없다. 좋지 않은 상황이므로 메인 스레드를 차지하지 않도록 피해야 한다.