Browser 최적화

사용자가 URL을 입력하고 브라우저에 화면이 띄워지기까지의 과정과 어떤 항목을 최적화해야 하는지에 대해 작성한 글입니다. 브라우저 렌더링 과정은 파일 다운로드에 초점을 맞추어 작성하였습니다.

틀린 부분이 있다면 댓글로 알려주시면 정말 감사하겠습니다!



브라우저의 렌더링 과정

1. 주소 입력

클라이언트측에서 URL을 통해 서버에서 요청을 보냄


2. HTML 파일 다운로드

서버는 클라이언트가 요청한 URL에 따라 결과물을 만들어서 응답
HTML 파일일 수도 있고, JSON일 수도 있고, 이미지 등의 파일일 수도 있다.

이 경우에는 HTML 파일을 반환하며, 브라우저가 가장 첫번째로 다운로드 받는 것이 HTML


3. CSS, JS 다운로드

<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 등의 모듈들을 다운로드 한다.


4. 네트워크 연결 제한 (HTTP 1.1)

브라우저마다 한번에 다운로드할 수 있는 모듈의 개수가 정해져 있다. 위 표는 Max Connection per Domain으로 하나의 도메인으로부터 다운로드 받을 수 있는 모듈의 개수이다.

브라우저는 보통 한번에 6개의 모듈을 하나의 도메인으로부터 다운로드 받을 수 있다. 이는 HTTP 1.1에 해당하며 HTTP2는 제한 사항이 다르며 6개 이상도 동시 요청할 수 있다. 아래에 조금 더 상세히 설명한다.

여러 도메인으로부터 다운로드 받으면 6개 이상의 모듈을 동시에 다운로드 받을 수 있고, 몇백~몇천개의 모듈을 동시에 다운로드 받을 수 있어서 제한이 거의 없다고 봐도 된다. 따라서 동시 연결 제한을 우회하는 방법으로 여러 서브 도메인으로부터 모듈을 다운로드 받는 방법이 있다. (도메인 샤딩)


왜 TCP 연결 개수에 제한이 있었을까?

서버 과부하를 방지하기 위함!

  1. 순차적 커넥션

    HTTP 1.0에서 사용하던 방식으로 하나의 요청이 응답을 받고 나서야 다음 요청을 할 수 있는 방법.

    하나의 커넥션은 하나의 요청 후 서버와 연결이 끊어지기 때문에 매 요청마다 서버와 핸드쉐이크 단계부터 다시 진행한다.


  1. 지속 커넥션

    웹 클라이언트는 보통 같은 서버에 여러개의 리소스를 요청한다. 이러한 특징을 Site Locality라고 한다.

    같은 서버에 대한 요청이 여러개일 때, 커넥션을 한번만 사용하고 버리기 아깝다. 따라서 한번 서버와 연결된 커넥션을 끊지 않고 유지하여 커넥션을 연결하는데 쓰이는 오버헤드를 줄인다.

    HTTP 1.1 부터 적용된 방식이다.


  1. 파이프라이닝 커넥션

    하나의 요청이 끝나기를 기다리지 않고 병렬적으로 요청을 전송한다. 동시에 요청을 처리하기 때문에 마지막 요청이 끝나기까지의 시간을 대폭 줄일 수 있다. 그러나 HOL Blocking 문제로 인해 브라우저는 기본적으로 파이프라이닝 커넥션을 사용하지 않는다.

    따라서, 동시에 여러개의 요청을 처리하는 방법은 여러개의 지속 커넥션을 연결하는 방법 밖에 없다.

    여러개의 커넥션을 유지하고 있는 것은 클라이언트와 서버 모두 부담이 된다. 특히 서버는 하나의 클라이언트만을 상대하는 것이 아니므로, 100개의 클라이언트가 각각 100개의 커넥션을 가지면 서버는 동시에 1만개의 커넥션을 유지해야할 수 있다. 따라서, 커넥션 정도의 절충안으로 6개가 설정된 것이다.

    • HOL Blocking (Head-Of-Line Blocking) 네트워크에서 같은 큐에 있는 패킷이 첫번째 패킷에 의해 지연될 때 발생하는 성능 저하 현상 1번 input과 3번 input이 동시에 4번 output으로 패킷을 보내려고 하는 상황 같은 Clock Cycle 내에 2개의 패킷을 처리할 수 없기 때문에 하나의 input은 지연되게 됨

  1. 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를 지원한다면 동시 요청 수를 고려하지 않아도 된다.


5. DOM Tree, CSSOM Tree 생성

HTML, CSS 파일을 다운로드한 뒤 구문 분석과정을 거쳐 DOM 트리와 CSSOM 트리를 만든다.


6. Render Tree 생성

DOM 트리와 CSSOM 트리를 기반으로 실제로 화면에 표시하는 객체들로 구성된 Render 트리를 만든다.

Render 트리의 각 노드는 DOM 객체에 스타일이 붙어있는 형태이며, display: none 스타일을 갖는 DOM 객체는 Render 트리에서 탈락한다.


7. Layout

Render 트리를 기반으로 DOM 객체의 위치를 잡는 레이아웃 과정을 진행한다. 브라우저 화면에서 어디에 위치하며, 크기는 얼마로 해야하는지 계산하는 단계


8. Paint

레이아웃 과정 후에 실제로 요소들을 그리는 과정


9. JS 실행

자바스크립트 파일도 다운로드 후 자바스크립트 엔진에 의해 실행된다. 반복되는 코드는 JIT 컴파일러에 의해 컴파일 된다. JIT 컴파일러의 동작과정은 아래 링크에서 확인할 수 있다.

자바스크립트까지 실행되면 비로소 개발자가 의도한 화면이 브라우저에 표시된다.



브라우저 최적화

브라우저 최적화를 진행하는 이유는 사용자 경험이다. 웹 애플리케이션에도 첫인상이라는게 존재한다. 웹의 첫인상은 사이트의 디자인, 적재적소의 UI 애니메이션, 헤드 카피 등의 시각적인 요소일 수도 있다.

시각적인 측면도 웹의 첫인상일 수 있지만, 응답성 측면의 첫인상도 있다. 페이지 로딩이 길어서 사용자가 3초 이상 흰 화면만 보게 된다면, 좋지 않은 첫인상을 갖게 된다.


최적화 점수 측정

PageSpeed Insights

Lighthouse | Tools for Web Developers | Google Developers

구글에서 제공하는 페이지 속도 측정 사이트에서 원하는 페이지의 최적화 점수를 알아낼 수 있다. 크롬 확장 프로그램인 Light House에서도 최적화 점수 측정이 가능하다. (구글 스피드 인사이트가 Light House를 내부적으로 사용)



최적화 항목

1. FCP (First Contentful Paint)

  • 페이지가 로드되기 시작한 시점부터 페이지의 콘텐츠의 일부가 렌더링될 때까지 걸린 시간
  • 전체 콘텐츠가 로딩되지 않아도 사용자는 소비할 수 있는 콘텐츠를 보게 되며, 로딩이 진행되고 있음을 인지하도록 한다.
  • 1.8초 이내에 FCP가 수행되어야 좋은 점수를 받을 수 있다.
  • 3초 이상 FCP에 소요되면 개선이 필요하다. 사용자가 3초동안 아무런 화면도 볼 수 없는 상태이다.

웹 폰트와 FCP

보통 렌더링되는 첫 요소는 텍스트일 확률이 크다. 이미지나 비디오는 다운로드 시간이 별도로 존재하기 때문에 텍스트가 먼저 보이게 되는데, 폰트 설정을 어떻게 하냐에 따라 FCP 시간에 영향이 갈 수 있다.

각각의 폰트 역시 하나의 모듈이므로 다운로드 시간을 거친다. 만약 폰트 파일의 파일 용량이 크면 다운로드 시간이 길어질 것이고 사용자가 폰트가 적용된 텍스트를 보기까지의 시간이 더 걸리게 된다.

또 다른 최적화 방법으로는 폰트 파일이 다운로드되기 전에 사용자에게 텍스트를 보여주는 방법이다. 브라우저가 가지고 있는 기본 폰트를 사용하여 우선적으로 화면에 텍스트를 띄우고, 폰트 파일이 다운로드 완료되면 그때서야 지정된 폰트를 적용한다.


  1. 폰트 용량 줄이기
    @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 파일을 다운로드 한다.

  1. 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 시간을 단축하기 위해 적용할 수 있다.


2. TTI (Time to Interactive)

  • 페이지가 완전히 상호작용할 준비를 마치기까지 걸린 시간
  • Idle Window 시간 이내에 있는 Last Long-Task가 끝나는 시간을 측정한다.
    • Idle Window = CPU Idle & Network Idle
      • CPU Idle = 메인 스레드에 작업이 없는 짧은 기간
      • Network Idle = 2개 이하의 네트워크 요청이 존재하는 기간
  • 웹에서의 상호작용은 자바스크립트를 통해 이뤄지므로, Last Long-Task가 끝이 나면 상호작용 준비가 되었다고 간주한다.

  • 3.8초 이내에 상호작용 준비가 되면, 사용자가 이벤트를 발생시켜도 정상 동작하므로 경험을 해치지 않는다.
  • 7.3초 이상으로 상호작용 준비를 한다면, 그 사이에 사용자가 버튼을 클릭해도 아무런 반응이 없을 수 있다. 따라서 개선이 필요하다.

3. TBT (Total Blocking Time)

  • 메인 쓰레드가 차단된 시간의 총합
  • FCP와 TTI 사이에서 메인 쓰레드가 얼마나 차단되었는지 측정
  • 50ms 넘게 메인 쓰레드를 차지하는 작업을 Long-Task로 간주하며, Long-Task의 작업 시간에서 50ms를 뺀 시간들의 총합이 TBT이다.

TTI 와의 관계

  • TTI 만으로 측정 불가한 영역을 TBT가 채워주는 상호보완적인 측정지표이다.
  1. 51ms 인 작업 3개가 10초에 걸쳐 진행되는 상황
  2. 10초짜리 작업 1개를 수행하는 상황
  • 위 2가지 작업은 모두 TTI 가 10초이다. Last Long-Task가 모두 10초지점에 끝나기 때문이다.
  • 그러나 TBT는 1번의 경우 3ms, 2번의 경우 9950ms이다. 즉, 9950ms동안 메인 쓰레드를 어떤 작업이 차지하고 있기 때문에 사용자와의 상호작용이 그만큼 지연된다.
  • 200ms 이내가 되도록 해야 사용자의 이벤트가 지연이 없거나 알아채지 못할 정도로만 지연된다.
  • 600ms 이상 메인 쓰레드가 차단되면, 사용자는 상호작용이 지연되는 느낌을 받을 수 있다.

4. SI (Speed Index)

  • 페이지의 콘텐츠가 채워지는 속도
  • 성능 측정 툴은 페이지가 로드되는 과정을 프레임 단위로 캡쳐하고 시각적인 변화를 계산한다.
  • 시각적 변화가 안정되는 시점까지 걸린 시간을 측정한다.
  • 3.4초 이내에 페이지에 콘텐츠를 다 채우면, 사용자에게 빠르게 로딩되는 인상을 준다.
  • 5.8초 이상으로 페이지가 그려진다면, 사용자에게 느리게 로딩되는 인상을 준다.

5. LCP (Largest Contentful Paint)

  • 페이지 로드 시점부터 첫 화면 영역 내의 가장 큰 이미지나 텍스트 블록이 렌더링될 때까지의 시간
  • 페이지 로딩 스냅샷마다 화면 영역에서 가장 큰 블록을 찾아서 LCP의 시점을 조정한다.

  • LCP가 항상 페이지 로딩 완료를 뜻하지는 않는다. 페이지 로딩 완료는 SI 에서 측정한다.

왜 LCP를 측정할까?

  • 페이지에서 가장 중요한 요소를 사용자에게 빠르게 전달해야 좋은 사용자 경험을 만들 수 있다.
  • 따라서 가장 중요한 요소가 페이지에 렌더링된 시점을 측정할 수 있어야 하고, 가장 중요한 요소를 알고 있어여 한다.
  • FMP (First Meaningful Paint) 이라는 지표는 스크롤 없이 보여지는 화면 영역이 렌더링되는 시점으로, 이를 가장 중요한 요소가 표시되는 시점으로 간주했었으나 현재는 사용하지 않는다.
  • 구글의 연구 결과로는 주요 콘텐츠 렌더링 시점보다 가장 큰 요소의 렌더링이 사용자 경험에 더 좋았고, 이로 인해 LCP를 측정하게 되었다.

  • 2.5초 이내에 가장 큰 요소가 그려지도록 한다.
  • 4초 이상 LCP가 소요되면 개선이 필요하다.

6. CLS (Cumulative Layout Shift)

  • 페이지의 수명 동안 발생하는 모든 예기치 않은 레이아웃 변화에 점수를 매겨서 측정
  • 페이지가 로딩되어 콘텐츠를 소비하는 중간에 광고 배너, API 호출 후 만들어진 DOM, 나중에 로드된 이미지 등이 공간을 차지하여 텍스트가 밀리거나, 레이아웃이 변경되는 것들이 좋지 않은 사용자 경험을 유발한다.
  • 따라서 CLS 지표를 두어 페이지 스냅샷마다 레이아웃이 얼마나 변경되었는지를 측정한다.
  • 뷰포트의 75%를 차지하는 텍스트 박스가 뷰포트의 25% 아래로 이동하게 된 상태
  • 0.75 * 0.25 = 0.1875 의 점수가 누적된다.

  • 0.1 이내로 CLS를 유지하면 사용자가 보고 있던 요소가 시야를 벗어나지 않는다.
  • 0.25 이상이면 광고 배너나 이미지의 위치를 대략적으로 설정하고 공간을 미리 차지하는 등의 개선이 필요하다.


최적화 방법

화면을 띄우기까지 걸리는 시간 = 필요한 파일 다운로드 시간 + 브라우저에 화면을 그리는 시간

파일 다운로드에 초점을 맞춰서 브라우저 최적화를 진행하려면 어떻게 해야할까?

  • 파일의 용량을 줄인다.
  • 한 도메인에서 다운로드 받는 파일의 개수를 너무 많이 늘리지 않는다.
  • 초기 화면에 필요한 파일만 다운로드 받거나 지연시킨다.
  • 여러 페이지에 걸쳐 사용되는 파일을 캐싱한다.
  • 이미지 다운로드를 뒤로 미뤄서 CSS, JS 파일을 더 빨리 다운로드시킨다.

1. Webpack 사용

  • 파일 용량 압축
  • 모듈 번들링을 통한 파일 개수 감소

2. Image Lazy Loading

<img loading="lazy" src="..." />
  • 화면에 실제로 이미지가 보여져야 할 때 이미지를 다운로드하여, CSS, JS를 더 앞서 다운로드하게 함

3. WebP

<img src="some-image.webp" onerror="this.src='some-image.png'" />
  • 압축율이 좋은 이미지 포맷을 사용하여 파일 용량 압축

4. Lazy Component

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 }
]
  • 첫화면에 보이지 않는 컴포넌트는 나중에 다운로드하여 파일 개수 감소
  • 당장 필요하지 않은 컴포넌트의 코드가 분리되어 파일 용량 압축

5. Browser Caching

  • 브라우저에 파일을 캐싱하여 다운로드할 파일 개수 감소
  • 다운로드하는 시간 자체가 없어지므로 큰 이득


References

브라우저 렌더링 과정 - 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?

First Contentful Paint

First Contentful Paint (FCP)

Speed index - MDN Web Docs Glossary: Definitions of Web-related terms | MDN

Cumulative Layout Shift (CLS)

Largest Contentful Paint (LCP)

First Meaningful Paint

Total Blocking Time (TBT)

Speed Index

Time to Interactive

WebP는 정말 JPG보다 뛰어날까? 최신 이미지 파일 3종 비교 - 테크잇

Vue.js Lazy load 적용하기1

https://www.linkedin.com/pulse/why-does-your-browser-limit-number-concurrent-ishwar-rimal/

NAVER D2

[기타] 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?

HTTP1 vs HTTP2

HTTP/2 알아보기 - 1편 | 와탭 블로그

HTTP/2 소개 | Web Fundamentals | Google Developers

[기타] HOL 블로킹(Head-Of-Line Blocking)

profile
프론트엔드 개발자

10개의 댓글

comment-user-thumbnail
2021년 10월 16일

크롬 및 엣지의 자바스크립트 엔진은 V8이고
FF는 스파이더 몽키인 거 같습니다

그리고, 글 잘 보고 갑니다. 명쾌한 설명이네요...

1개의 답글
comment-user-thumbnail
2021년 10월 18일

좋은 글 감사합니다@.@ 많이 도움이 되었어요! 나중에 하나하나 실행해보면서 최적화 작업해봐야겠어요!

1개의 답글
comment-user-thumbnail
2021년 10월 20일

좋은글 감사합니다

1개의 답글
comment-user-thumbnail
2021년 11월 13일

좋은글 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 9월 24일

렌더링 최적화 블로깅을 위해 찾다보니 좋은글이 있어서 정독했는데 엘리스부트캠프에서 뵈었던 코치님 이셨네요 좋은 글 정말 감사드리고 지난 부트캠프에서의 코칭들도 다시 한번 감사의 말씀드립니다.

답글 달기