사용자가 URL을 입력하고 브라우저에 화면이 띄워지기까지의 과정과 어떤 항목을 최적화해야 하는지에 대해 작성한 글입니다. 브라우저 렌더링 과정은 파일 다운로드에 초점을 맞추어 작성하였습니다.
틀린 부분이 있다면 댓글로 알려주시면 정말 감사하겠습니다!
클라이언트측에서 URL을 통해 서버에서 요청을 보냄
서버는 클라이언트가 요청한 URL에 따라 결과물을 만들어서 응답
HTML 파일일 수도 있고, JSON일 수도 있고, 이미지 등의 파일일 수도 있다.
이 경우에는 HTML 파일을 반환하며, 브라우저가 가장 첫번째로 다운로드 받는 것이 HTML
<html>
<head>
<link href="www.lawandgood.com/static/entry.css"></link>
<script src="www.lawandgood.com/static/entry.js"></script>
</head>
<body>
.
.
</body>
</html>
HTML 파일을 파싱하는 과정에서 만나는 CSS, JS 등의 모듈들을 다운로드 한다.
브라우저마다 한번에 다운로드할 수 있는 모듈의 개수가 정해져 있다. 위 표는 Max Connection per Domain으로 하나의 도메인으로부터 다운로드 받을 수 있는 모듈의 개수이다.
브라우저는 보통 한번에 6개의 모듈을 하나의 도메인으로부터 다운로드 받을 수 있다. 이는 HTTP 1.1에 해당하며 HTTP2는 제한 사항이 다르며 6개 이상도 동시 요청할 수 있다. 아래에 조금 더 상세히 설명한다.
여러 도메인으로부터 다운로드 받으면 6개 이상의 모듈을 동시에 다운로드 받을 수 있고, 몇백~몇천개의 모듈을 동시에 다운로드 받을 수 있어서 제한이 거의 없다고 봐도 된다. 따라서 동시 연결 제한을 우회하는 방법으로 여러 서브 도메인으로부터 모듈을 다운로드 받는 방법이 있다. (도메인 샤딩)
왜 TCP 연결 개수에 제한이 있었을까?
서버 과부하를 방지하기 위함!
순차적 커넥션
HTTP 1.0에서 사용하던 방식으로 하나의 요청이 응답을 받고 나서야 다음 요청을 할 수 있는 방법.
하나의 커넥션은 하나의 요청 후 서버와 연결이 끊어지기 때문에 매 요청마다 서버와 핸드쉐이크 단계부터 다시 진행한다.
지속 커넥션
웹 클라이언트는 보통 같은 서버에 여러개의 리소스를 요청한다. 이러한 특징을 Site Locality라고 한다.
같은 서버에 대한 요청이 여러개일 때, 커넥션을 한번만 사용하고 버리기 아깝다. 따라서 한번 서버와 연결된 커넥션을 끊지 않고 유지하여 커넥션을 연결하는데 쓰이는 오버헤드를 줄인다.
HTTP 1.1 부터 적용된 방식이다.
파이프라이닝 커넥션
하나의 요청이 끝나기를 기다리지 않고 병렬적으로 요청을 전송한다. 동시에 요청을 처리하기 때문에 마지막 요청이 끝나기까지의 시간을 대폭 줄일 수 있다. 그러나 HOL Blocking 문제로 인해 브라우저는 기본적으로 파이프라이닝 커넥션을 사용하지 않는다.
따라서, 동시에 여러개의 요청을 처리하는 방법은 여러개의 지속 커넥션을 연결하는 방법 밖에 없다.
여러개의 커넥션을 유지하고 있는 것은 클라이언트와 서버 모두 부담이 된다. 특히 서버는 하나의 클라이언트만을 상대하는 것이 아니므로, 100개의 클라이언트가 각각 100개의 커넥션을 가지면 서버는 동시에 1만개의 커넥션을 유지해야할 수 있다. 따라서, 커넥션 정도의 절충안으로 6개가 설정된 것이다.
HTTP 2 커넥션
HTTP 2에서는 하나의 커넥션 내에서 스트림을 달리하여 여러개의 요청을 동시에 처리할 수 있다.
프레임을 여러개 나누어도 연결은 하나이므로 동시 요청 수는 사실상 무제한이다. 따라서 위에서 설명한 커넥션 제한이 HTTP2에서는 적용되지 않으며, HTTP2의 SETTINGS_MAX_CONCURRENT_STREAMS 설정에 따라 최대로 동시 요청 수가 정해진다.
SETTINGS_MAX_CONCURRENT_STREAMS ****설정은 서버와 클라이언트 측 양쪽에서 설정할 수 있고, 크롬에서는 1000이 기본값이라고 한다.
The Right Way to Bundle Your Assets for Faster Sites over HTTP/2
위 아티클에 따르면 HTTP2에서는 1개를 요청하던, 1000개를 동시에 요청하던 큰 차이를 보이지 않는다고 한다. 단 HTTP2를 미지원하는 브라우저와 서버를 위해 동시 요청 수를 조절하라고 하는데, 2016년 아티클이고 현재 브라우저는 HTTP2를 대부분 지원한다, 따라서 서버가 HTTP2를 지원한다면 동시 요청 수를 고려하지 않아도 된다.
HTML, CSS 파일을 다운로드한 뒤 구문 분석과정을 거쳐 DOM 트리와 CSSOM 트리를 만든다.
DOM 트리와 CSSOM 트리를 기반으로 실제로 화면에 표시하는 객체들로 구성된 Render 트리를 만든다.
Render 트리의 각 노드는 DOM 객체에 스타일이 붙어있는 형태이며, display: none
스타일을 갖는 DOM 객체는 Render 트리에서 탈락한다.
Render 트리를 기반으로 DOM 객체의 위치를 잡는 레이아웃 과정을 진행한다. 브라우저 화면에서 어디에 위치하며, 크기는 얼마로 해야하는지 계산하는 단계
레이아웃 과정 후에 실제로 요소들을 그리는 과정
자바스크립트 파일도 다운로드 후 자바스크립트 엔진에 의해 실행된다. 반복되는 코드는 JIT 컴파일러에 의해 컴파일 된다. JIT 컴파일러의 동작과정은 아래 링크에서 확인할 수 있다.
자바스크립트까지 실행되면 비로소 개발자가 의도한 화면이 브라우저에 표시된다.
브라우저 최적화를 진행하는 이유는 사용자 경험이다. 웹 애플리케이션에도 첫인상이라는게 존재한다. 웹의 첫인상은 사이트의 디자인, 적재적소의 UI 애니메이션, 헤드 카피 등의 시각적인 요소일 수도 있다.
시각적인 측면도 웹의 첫인상일 수 있지만, 응답성 측면의 첫인상도 있다. 페이지 로딩이 길어서 사용자가 3초 이상 흰 화면만 보게 된다면, 좋지 않은 첫인상을 갖게 된다.
Lighthouse | Tools for Web Developers | Google Developers
구글에서 제공하는 페이지 속도 측정 사이트에서 원하는 페이지의 최적화 점수를 알아낼 수 있다. 크롬 확장 프로그램인 Light House에서도 최적화 점수 측정이 가능하다. (구글 스피드 인사이트가 Light House를 내부적으로 사용)
웹 폰트와 FCP
보통 렌더링되는 첫 요소는 텍스트일 확률이 크다. 이미지나 비디오는 다운로드 시간이 별도로 존재하기 때문에 텍스트가 먼저 보이게 되는데, 폰트 설정을 어떻게 하냐에 따라 FCP 시간에 영향이 갈 수 있다.
각각의 폰트 역시 하나의 모듈이므로 다운로드 시간을 거친다. 만약 폰트 파일의 파일 용량이 크면 다운로드 시간이 길어질 것이고 사용자가 폰트가 적용된 텍스트를 보기까지의 시간이 더 걸리게 된다.
또 다른 최적화 방법으로는 폰트 파일이 다운로드되기 전에 사용자에게 텍스트를 보여주는 방법이다. 브라우저가 가지고 있는 기본 폰트를 사용하여 우선적으로 화면에 텍스트를 띄우고, 폰트 파일이 다운로드 완료되면 그때서야 지정된 폰트를 적용한다.
@font-face {
font-family: NanumSquareRoundR;
src: local("NanumSquareRoundR"),
local("NanumSquareRoundR"),
url(NanumSquareRoundR.woff2),
url(NanumSquareRoundR.woff),
url(NanumSquareRoundR.eot),
url(NanumSquareRoundR.ttf);
font-weight: bold;
}
웹 폰트를 적용해본 경험이 있다면 `@font-face`를 사용해본 경험도 있을 것이다. 웹 폰트는 대표적으로 woff2, woff, eot, ttf 등의 형식이 있는데 압축 방식이 달라 파일 용량이 다르다.
대부분의 브라우저는 압축율이 가장 좋은 woff2 형식을 지원한다. IE에서는 다음으로 압축율이 좋은 woff 형식을 지원한다. 따라서 woff2와 woff 형식의 폰트 파일만으로도 충분히 많은 브라우저를 커버할 수 있다.
브라우저는 생각보다 똑똑해서 브라우저가 지원해주는 폰트만 다운로드한다. 여러개의 폰트 파일을 열거해놓으면 폴백 형식으로 다음 폰트 파일로 넘어간다. woff2를 지원하지 않은 IE는 폴백 1회 후 woff 파일을 다운로드 한다.
FOUT 방식으로 폰트 렌더링
웹 폰트의 문제는 다운로드 전에 텍스트를 표시하지 않으면 사용자에게 잘못된 정보를 전달할 수 있다는 것이다.
CSSOM 트리를 만드는 과정에서 css 파일에 포함된 웹 폰트 모듈을 다운로드 받기 시작하는데, Paint 단계에서 웹 폰트 모듈이 다운로드되지 않은 경우 화면에 그리는 것을 차단한다. 따라서 웹 폰트가 적용된 텍스트가 사용자에게 안 보이는 순간이 존재하게 되는 것이다.
FOIT(Flash Of Invisible Text) 방식은 폰트파일이 다운로드되기 전에 텍스트가 보이지 않고, 다운로드 후에 번쩍이듯이 텍스트가 등장한다.
FOUT(Flash Of Unstyled Text) 방식은 폰트파일이 다운로드되기 전에는 기본 폰트로 텍스트가 표시되고, 다운로드 후에 해당 폰트로 변경되면서 번쩍임이 발생한다.
크롬은 기본적으로 FOIT 방식인데, 텍스트가 보이지 않다가 번쩍이며 등장하는 문제가 있기 때문에 사용자 경험 관점에서 좋지 않다. 따라서 Light House에서는 FOUT 방식을 권장한다.
@font-face {
.
.
font-display: swap;
font-weight: bold;
}
font-display: swap
속성을 적용하면 FOUT 방식으로 텍스트를 렌더링할 수 있다. 기본 텍스트를 빠르게 띄워서 FCP 시간을 단축하기 위해 적용할 수 있다.
TTI 와의 관계
왜 LCP를 측정할까?
0.75 * 0.25 = 0.1875
의 점수가 누적된다.화면을 띄우기까지 걸리는 시간 = 필요한 파일 다운로드 시간 + 브라우저에 화면을 그리는 시간
파일 다운로드에 초점을 맞춰서 브라우저 최적화를 진행하려면 어떻게 해야할까?
<img loading="lazy" src="..." />
<img src="some-image.webp" onerror="this.src='some-image.png'" />
const Home = () => import('./Home.vue')
const About = () => import('./About.vue')
const Contact = () => import('./Contact.vue')
const routes = [
{ path: '/', name: 'home', component: Home },
{ path: '/about', name: 'about', component: About },
{ path: '/contact', name: 'contact', component: Contact }
]
브라우저 렌더링 과정 - Reflow Repaint, 그리고 성능 최적화
Browser connection limitations
Max parallel HTTP connections in a browser?
Network features reference - Chrome Developers
Why does your browser limit the number of concurrent network calls?
Speed index - MDN Web Docs Glossary: Definitions of Web-related terms | MDN
Largest Contentful Paint (LCP)
WebP는 정말 JPG보다 뛰어날까? 최신 이미지 파일 3종 비교 - 테크잇
https://www.linkedin.com/pulse/why-does-your-browser-limit-number-concurrent-ishwar-rimal/
[기타] HOL 블로킹(Head-Of-Line Blocking)
HTTP/2 소개 | Web Fundamentals | Google Developers
HTTP: HTTP/1.X - High Performance Browser Networking (O'Reilly)
The Right Way to Bundle Your Assets for Faster Sites over HTTP/2
Is the per-host connection limit raised with HTTP/2?
크롬 및 엣지의 자바스크립트 엔진은 V8이고
FF는 스파이더 몽키인 거 같습니다
그리고, 글 잘 보고 갑니다. 명쾌한 설명이네요...