내 웹 페이지 성능을 어떻게 올릴까?— 기초

JungHoon Park
10 min readMay 27, 2021

개요

여러 페이지를 들어가다 보면 어떤 사이트는 느리고 어떤 사이트는 빠른 경우를 체감하게 된다. 나의 경우 그 비교 대상은 쿠팡과 무신사이다. 실제 물건을 하나 클릭한 후 Lighthouse로 측정을 해보면 performance 점수가 각각 56, 24점이 나온다.

(좌)쿠팡과 (우)무신사 Performance 점수

무신사의 경우 보여주는 사진이 더 많아서 느린 것도 있겠지만, HTML이 로딩된 후 첫 이미지 또는 글자 등을 보여주기까지의 시간을 의미하는 FCP가 각각 0.6, 1.4 초로 거의 2배 이상의 차이가 난다. 속도에 따라서 가입자, 방문자 수와 수익이 증가한다고 하니 무신사는 쿠팡보다 그동안 기회비용을 더 잃지 않았을까?

본 시리즈에서는 HTML, CSS, JS가 어떻게 브라우저에서 다뤄지는지 알아본 후 간단히 용어 설명을 한다. 그 후 쉬운 예제 하나를 성능 최적화한 후 실제 React 앱에서 어떻게 성능을 최적화할지 알아보겠다.

목차

  1. CRP란?
  2. 브라우저에서 페이지 표시 방법
  3. 용어 정리

1. CRP(Critical Rendering Path)란?

브라우저가 HTML, CSS, JS를 읽고 픽셀로 표현되는 일련의 과정을 말한다.

그리고 CRP 최적화란 사용자 작업과 관련된 콘텐츠 표시의 우선순위를 지정하는 것을 말한다.

https://developers.google.com/web/fundamentals/performance/critical-rendering-path?hl=ko

2. 브라우저에서 페이지 표시 방법

브라우저가 네트워크에서 데이터를 읽으면 가능한 빠르게 DOM, CSSOM 트리를 생성해야 한다.

DOM(Document Object Model)

바이트 -> 문자열 -> 토큰 -> Node -> DOM

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model?hl=ko

HTML을 parsing 하면서 DOM 구조를 만드는데, 이때 외부에서 CSS, JS를 불러오는 link, script tag를 만나면 각각의 파일을 네트워크 통신으로 가져온다.

이후에 구체적으로 설명할 내용이지만 JS는 parser blocking이다. 즉, HTML parsing이 진행되다가 script를 읽는 순간 JS 파일을 네트워크 통신으로 가져오고(외부 script일 경우) CSSOM이 생성 완료되기까지 지연시킨다.

<html>  <head></head>  <body>    <p>      <span>Hello</span>      <!-- script를 만나면 HTML parsing을 잠시 중단한다.  -->      <script>      // 여기서 Hello span을 가져와서 어떤 작업을 수행이 가능하다.      // 하지만 이 이후의 DOM을 읽지  않았으므로, World div를 가져와서 어떤 작업을 수행할 수 없다.      </script>      <div>World</div>    </p>  </body></html>

이때 만약 script를 2개 이상 불러온다면 하나의 script가 다 불러올 때까지 다른 한 개는 기다려야 하는 문제가 생긴다.

브라우저에서는 Preload Scanner를 사용해 parser blocking 상태여도 다음 줄을 계속해서 읽으며 script, style, 이미지 등의 데이터를 네트워크 통신으로 가져온다. 이때 최소 라운드 트립은 1회이다.

이 때 다운로드 우선순위를 낮추고 특정한 상황에서만 style 적용하는 방법이 있다. 바로 media를 사용한다.

<!-- 다운로드 우선순위가 낮아지고 특정한 상황에만 style을 적용한다. --><link rel="stylesheet" href="style-print.css" media="print">

CSSOM(CSS Object Model)

바이트 -> 문자열 -> 토큰 -> Node -> CSSOM

css를 읽으면 아래와 같이 CSSOM으로 변환된다.

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model?hl=ko

CSS는 render blocking이다. 즉, CSSOM이 생성되기까지 브라우저는 처리되는 모든 콘텐츠를 렌더링하지 않는다.

JS

렌더링 엔진이 script 태그를 만나면 src 어트리뷰트에 따라 불러들인 JS 파일이나 태그 내의 코드를 파싱하기 위해 JS 엔진에 제어권을 넘긴다.

JS 엔진은 이후

토크나이징-> 파싱(Abstract Syntax Tree 생성) -> 바이트코드 생성-> 인터프리터에서 저수준 언어로 변환 및 실행한다.

그리고 렌더링 엔진에게 다시 제어권을 넘긴다.

JS는 DOM API를 사용하여 DOM과 CSSOM을 조작할 수 있다. CSSOM이 생성되기 전 JS가 실행된다면 원하지 않는 side effect를 발생시킬 수 있다. 그러므로 브라우저는 JS가 먼저실행되면 CSSOM이 완료되기까지 DOM 생성 작업을 지연시킨다.(parser blocking)

parser blocking을 해결 또는 최소화하기 위해서는 아래의 방법이 있다.

  1. body아래에 script를 놓는다.
  2. defer 속성 사용: 스크립트 다운로드는 비동기적으로 실행되지만, JS 파싱과 실행은 HTML paring이 완료된 직후(DOMContentLoaded)에 진행된다.
  3. async 속성 사용: 스크립트 다운로드가 끝나면 바로 실행된다. HTML parsing 중간에 실행될 수 있다.
  4. window.onload 사용

이 중 2, 3번은 src 어트리뷰트를 통해 외부 js 파일을 불러들이는 경우에만 사용할 수 있다.

Render Tree

생성된 DOM과 CSSOM을 병합한다.

https://classroom.udacity.com/courses/ud884/lessons/1464158642/concepts/15290985580923

한 가지 알면 좋은 것은 CSS의 visibility 속성에 비해 display는 병합하는 과정에서 CSSOM의 display: none을 만나면 아예 무시하고 진행하기 때문에 성능 최적화가 이뤄진다.

Layout(Reflow)

엘리먼트가 어떻게 위치할 것인지 결정한다.

https://classroom.udacity.com/courses/ud884/lessons/1464158642/concepts/15290985630923

body 태그의 width는 <meta/>에 적힌 viewport 기준이다.

layout은 하드웨어 자원이 많이 필요한 작업이다. 그러므로 batch 형식으로 진행하는 것이 layout을 여러번 실행시키지 않으므로 성능상 도움이 된다.

Paint(Repaint)

layer에 픽셀화를 진행한다. Paint는 여러 layer에 나눠서 진행될 수 도 있다. 참고

Layout에 변경이 없을 때는 Repaint만 수행된다.

Composite Layers

여러 layer에서 그린 것을 하나로 합친다.

3. 용어 정리

1. FP(First Paint)

첫 픽셀이 그려지는 시점

2. FCP(First Contentful Paint)

페이지가 로딩을 시작해서 어떤 콘텐츠(텍스트, 이미지, svg, canvas, 등)가 화면에 보이기 시작할 때를 가리킴

https://newrelic.com/blog/how-to-relic/browser-first-paint-first-contentful-paint

3. FID(First Input Delay)

유저가 처음 페이지와 인터렉션 하고 응답이 오는 시간. 유저의 첫 인상을 결정짓는다. 이벤트 리스너가 붙지 않아도 측정된다.

대상: <input>, <textarea>, <select>, <a>, etc, 단 줌인아웃, 스크롤링을 판별대상이 아니다.

4. TTI(Time to Interactive)

페이지가 완전히 상호작용(interactive) 가능하기까지 얼마나 걸리는지 측정

다음의 방법으로 측정한다.

1. FCP가 존재

2. 페이지의 보이는 부분에 이벤트 헨들러가 등록 됨

3. 페이지가 유저 인터렉션에 50ms 안에 반응

5. TBT(Total Blocking Time)

FCP에서 TTI까지 main thread가 입력 응답에 얼마나 시간이 걸렸는지 측정

6. TTFB(Time to First Byte)

서버에 데이터 응답을 요청하고 다운받기까지 걸린시간

7. LCP(Largest Contentful Paint)

가장 사이즈가 큰 컨텐츠가 렌더링 되는 시간을 나타냄(img, svg, video, url(). text nodes)

사이즈는 용량이 아닌 viewport에 보여지는 고유 너비, 높이로 판단한다.(maring, padding은 포함되지 않음)

LCP가 가장 중요한 컨텐츠는 아니다.

8. L(onload Event)

각 페이지 로드의 최종 단계로, 브라우저가 추가 애플리케이션 로직을 트리거할 수 있는 onload 이벤트를 발생시킴.

9. DCL(DOMContentLoaded Event)

DOM이 준비되고 그 시점에 자바스크립트 실행을 차단하는 스타일시트가 없는 시점을 표시. 즉, 이제 (잠재적으로) 렌더링 트리를 생성 가능하다. DCL이 더 빠를수록 다음 로직이 더 빠르게 실행된다.

10. Long task

50ms 보다 큰 작업을 말한다.

11. Lab Test

사전 정의 된 장치 및 네트워크 설정을 사용하여 제어 된 환경에서 수집 된 성능 데이터.

간단하게 말하면 로컬환경에서 build한 후 Lighthouse를 돌렸을 때 나오는 점수이다.

12. Field Test

사용자가 경험하는 환경에서 수집 된 성능 데이터.

간단하게 말하면 실제 호스팅된 사이트에서 Lighthouse를 돌렸을 때 나오는 점수이다.

CRP와 용어에 대해 알았으니 다음화에서 여기서 배운것을 바탕으로 예제를 최적화 해보겠다.

다음화 -> 내 웹페이지 성능을 어떻게 올릴까? — 예제

--

--