12장 모든 웹 개발자가 관심을 가져야 할 핵심 웹 지표
12.1 웹사이트와 성능
사용자가 웹사이트에 접속했을 때 기대하는 사항
- 방문한 목적을 쉽게 달성할 수 있어야 함
- 방문한 목적을 달성하는데 시간이 짧아야 함
- 웹사이트에서 개인정보가 누출되는 등의 사고 없이 보안에 철저해야 함
12.2 핵심 웹 지표란?
구글에서 만든 지표로, 웹사이트에서 뛰어난 사용자 경험을 제공하는 데 필수적인 지표를 일컫는 용어이다
핵심 웹 지표 3가지
- 최대 콘텐츠 풀 페인트(LCP)
- 최초 입력 지연(FID)
- 누적 레이아웃 이동(CLS)
특정 문제를 진단하는 데 사용할 수 있는 2가지 지표
- 최초 바이트까지의 시간(TTFB)
- 최초 콘텐츠풀 시간(FCP)
12.3 최대 콘텐츠 풀 페인트(LCP)
12.3.1 정의
페이지가 처음으로 로드를 시작한 시점부터 뷰포트 내부에서 가장 큰 이미지 또는 텍스트를 렌더링하는 데 걸리는 시간을 말한다
뷰포트 내부에서 '큰 이미지와 텍스트'
<img>
<svb> 내부의 <img>
- poster 속성을 사용하는
<video>
- url()을 통해 불러온 배경 이미지가 있는 요소
- 텍스트와 같이 인라인 텍스트 요소를 포함하고 있는 블록 레벨 요소
- 이 블록 레벨 요소에는
<p>, <div> 등이 포함된다
이미지와 텍스트가 각각 사용자의 시점에 언제 노출됐는지를 확인하는 정확한 시점은 W3C 문서에 나와 있다.
12.3.2 의미
- 웹페이지가 로딩이 완료되어 사용자에게 노출되기까지 걸리는 시간의 기준을
DOMCotentLoaded 이벤트가 호출되는 시간으로 잡으면 안 된다
DOMCotentLoaded는 HTML 문서를 완전히 불러오고 파싱했을 때 발생하는 이벤트로, 페이지의 document를 대상으로 일어나며 단 한 번만 호출된다. 이 이벤트는 스타일 시트, 이미지, 하위 프레임의 로딩은 기다리지 않는다.
- 사용자는 화면에 노출되는 부분만 로딩돼 있다면 사용자는 페이지 로딩이 완료되었다고 느끼기 때문에 뷰포트에 메인 콘텐츠가 화면에 완전히 전달되는 속도를 기준으로 한다.
- 따라서 사용자에게 페이지의 정보를 화면에 전달하는 속도를 객관적으로 판단하기 위한 지표로 만들어진 것이 바로 최대 콘텐츠풀 페인트이다.
12.3.3 예제
- 생략
- LCP는 페이지 로딩에 따라 변화하는 지표이다. 사용자가 이용하는 디바이스의 크기에 따라 그리고 그것이 이미지와 같이 크기가 큰 리소스라면 실제로 로딩에 필요한 시간에 따라 LCP의 값이 달라질 수 있다
12.3.4 기준 점수
- 기준 점수를 측정하는 방법은 자바스크립트 API를 호출하는 방법과 다른 도구를 활용하는 방법(더 많이 사용)이 있다
- 해당 지표가 2.5초 내로 응답이 오는 것이 좋은 점수이다. 4초 이내로 응답이 온다면 보통, 그 이상이 걸리면 나쁨으로 판단한다
12.3.5 개선 방안
텍스트는 언제나 옳다
- 가장 확실한 방법이다.
- 이미지를 최적화해도 텍스트 노출이 훨씬 더 빠르다.
이미지는 어떻게 불러올 것인가?
-
이미지 노출하는 방법으론 아래와 같이 여러 가지가 있다
-
<img>
<img src="lcp.jpg" ... />
- 브라우저의 프리로드 스캐너에 의해서 먼저 발견되어 빠르게 요청이 일어난다.
- 프리로드 스캐너란 HTML을 파싱하는 단계를 차단하지 않고 이미지와 같이 빠르게 미리 로딩하면 좋은 리소스를 먼저 찾아 로딩하는 브라우저의 기능이다
- HTML 파싱이 미처 완료되지 않더라도 프리로드 스캐너가 병렬적으로 리소스를 다운로드하므로 LCP 요소를 불러오기에 적절한 방법이다
-
<svg> 내부의 <img>
<svg xmls="http://www.w3.org/1000/svg">
<image href="lcp.jpg" />
</svg>
- 모든 리소스를 다 불러온 이후에 이미지를 불러온다
- 따라서 프리로드 스캐너에 의해 발견되지 않아 병렬적으로 다운로드가 일어나지 않는다. 이는 결국 최대 LCP 점수에도 악영향을 미쳐 권장하지 않는다
-
<video>의 poster
<video poster="lcp.jpg"></video>
- poster는 사용자가 video 요소를 재생하거나 탐색하기 전까지 노출되는 요소다
- 프리로드 스캐너에 의해 조기에 발견되어 적절한 방법이다
- 향후 poster가 없는 video의 경우 video를 실제로 로딩해 첫 번째 프레임을 해당 poster 리소스로 대체할 예정이기 때문에 poster를 반드시 넣어주는 것이 좋다
-
background-image
<div style="backgroud-image: url(lcp.jpg)">...</div>
- CSS에 있는 리소스는 항상 느리다. 이러한 리소스는 브라우저가 해당 리소스를 필요로 하는 DOM을 그릴 준비가 될 때까지 리소스 요청을 뒤로 미루기 때문에 사용하지 않는 것이 좋다.
그 밖에 조심해야 할 사항
- 이미지 무손실 압축 : 이미지는 가능한 한 무손실 형식으로 압축해 최소한의 용량으로 서비스하는 것이 좋다
- loading=lazy 주의 : loading=lazy는 리소스를 중요하지 않음으로 표시하고 필요할 때만 로드하는 전략이기 때문에 LCP의 이미지에 사용하면 로딩 속도를 늦춰서 사용하지 않는 것이 좋다
- fadein과 같은 각종 애니메이션 : 이미지가 그냥 뜨는 것보다 fadein을 쓰면 LCP도 늦어진다
- 클라이언트에서 빌드하지 말 것 : 서버에서 빌드해온 HTML을 프리로드 스캐너가 바로 읽어서 최대 콘텐츠풀 페인트로 빠르게 가져가는 것이 최덕의 시나리오이다
- 최대 콘텐츠풀 리소스는 직접 호스팅 : 이미 연결이 맺어진 현재 출처가 아니라 완전 새로운 출처의 경우에는 네트워크 커넥션부터 다시 수행하기 때문에 권장하지 않는다
12.4 최초 입력 지연(FID)
12.4.1 정의
- 사용자가 페이지와 처음 상호 작용할 때(예: 링크를 클릭하거나 버튼을 탭하거나 사용자 지정 JavaScript 기반 컨트롤을 사용할 때)부터 해당 상호 작용에 대한 응답으로 브라우저가 실제로 이벤트 핸들러 처리를 시작하기까지의 시간을 측정한다
- 쉽게 말해 사용자가 최초의 입력을 할 때 얼마나 빠르게 웹페이지와의 상호작용에 대한 응답을 받을 수 있는지를 측정하는 지표이다
12.4.2 의미
- 대부분 웹사이트 내부의 이벤트가 반응이 늦어지는 이유는 해당 입력을 처리해야 하는 브라우저의 메인 스레드가 바쁘기 때문이다.
- 메인 스레드가 바쁜 이유는 대규모 렌더링이 일어나고 있거나 다른 작업을 처리하는 데 리소스를 할애하고 있기 때문이다.
- 즉, 이벤트가 발생하는 시점에 최대한 메인 스레드가 다른 작업을 처리할 수 있도록 여유를 만들어 둬야 사용자에게 빠른 반응성을 보장할 수 있다.
구글에서 정의한 사용자 경험 4가지(RAIL)
- Response: 사용자의 입력에 대한 반응 속도를 50ms 미만으로 이벤트를 처리할 것 (FID에 해당당)
- Animation: 애니메이션의 각 프레임을 10ms 이하로 생성할 것
- Idle: 유휴 시간을 극대화해 페이지가 50ms 이내에 사용자 입력에 응답하도록 할 것
- Load: 5초 이내에 콘텐츠를 전달하고 인터렉션을 준비할 것
정리
- 화면이 최초에 그려지고 난 뒤, 사용자가 웹페이지에서 클릭 등 상호작용을 수행했을 때 메인 스레드가 이 이벤트에 대한 반응을 할 수 있을 때까지 걸리는 시간을 의미한다.
- 이 사간은 메인 스레드가 처리해야 하는 다른 작업이 많을수록 느리다
12.4.3 예제
- 크롬으로 디버깅하는 것은 이벤트가 처리되는 것이 얼마나 지연되는지만 알 수 있다
- 즉, 최초 이벤트 발생으로부터 해당 이벤트의 핸들러가 실행되는 순간까지 사이의 기간만 측정한다
- 만약 이벤트 핸들러의 실행 시간을 측정하고 싶다면 Event Timing API를 사용하는 것이 좋다
12.4.4 기준 점수
- 좋은 점수의 기준은 100ms 이내로 응답이 와야 하며, 300ms 이내인 경우 보통, 그 이후에는 나쁨으로 처리된다
12.4.5 개선 방안
실행에 오래 걸리는 긴 작업을 분리
-
만약 실행에 오래 걸리는 작업이 있다면 몇 가지 대안을 연구해야 한다
-
꼭 웹페이지에서 해야 하는 작업인가?
꼭 웹페이지에서 해야 되는 작업이 아니라면 서버로 옮겨서 처리하는 것이 좋다. 서버에서 처리하면 상대적으로 빠르고 쾌적한 환경에서 처리해 브라우저의 메인 스레드를 오래 점유하지 않게 할 수 있다.
-
긴 작업을 여러 개로 분리하기
하나의 긴 작업이 메인 스레드를 계속 점유할수록 사용자는 페이지에서 응답을 받지 못하고 있을 가능성이 크다. 웹페이지 최초 로딩에 필요하지 않은 내용을 나중에 불러오는 것도 포함해서 분리하는 것이 좋다
자바스크립트 코드 최소화
- 현재 사용하지 않는 코드 중에서는 특정 이벤트에 따라 실행되는 코드, 예기치 못한 상황에서 실행될 코드 등 다양한 것들이 존재할 수 있다
- 이러한 코드들은 당장에 급하지 않은 코드로 간주해서 앞서 언급한 지연 로딩 기법, 사용자가 필요로 하는 순간에 불러오거나 우선순위를 낮춰서 불러오는 것이 좋다
- 폴리필 확인하기
폴리필이란 브라우저에서 지원하지 않는 기능을 사용하기 위해 웹페이지에서 직접 구현하고 집어넣는 코드를 의미한다
한 메서드에 들어가는 폴리필의 크기가 제법 크기 때문에 폴리필을 넣기 전에 반드시 2가지를 확인해 봐야 한다
- 폴리필이 필요한 환경인가? : 만약 구현 브라우저 환경을 지원하지 않기로 결심했다면 폴리필을 집어 넣을 필요가 없다
- 꼭 필요한 폴리필인가? : 애플리케이션의 여러 군데에서 자주 사용하는 코드인지 반드시 확인해 봐야 한다. 만약 사용하는 곳이 몇 군데 되지 않는다면 직접 저수준 자바스크립트 코드를 작성해 구현하는 편이 도움이 될 수 있다
타사 자바스크립트 코드 실행의 지연
- Google Analytics나 Firebase와 같이 웹페이지의 통계 집계를 위해 제3자가 만든 타사 스크립트를 넣는 경우도 있다. 이러한 코드의 실행으로 메인 스레드가 점유될 수 있다.
- 이러한 타사 스크립트는 대부분 웹 페이지 로드에 중요한 자원이 아니므로
<script>의 async와 defer를 이용해 지연 불러오기를 하는 것이 좋다.
defer
먼저 해당 스크립트를 다른 리소스와 함께 병렬로 다운로드한다.
다운로드 중에도 HTML 파싱 등의 메인 스레드 작업은 멈추지 않는다.
다운로드가 완료됐다 하더라도 이 스크립트의 실행은 페이지가 완전히 로딩된 이후에 맨 마지막에 실행된다
async
해당 스크립트를 다른 리소스와 함께 병렬로 다운로드한다.
리소스의 다운로드가 완료되어 버리면 다른 리소스의 다운로드가 완료되는 것을 기다리지 않고 바로 실행한다.
따라서 async 리소스의 실행 순서는 다운로드가 완료된 순서대로 실행된다
둘 다 없는 경우
script를 만나는 순간 다운로드가 우선되며, 다운로드가 완료되면 코드 실행이 우선된다. 다른 작업은 다운로드와 실행이 끝날 때까지 미뤄진다.
- 따라서 타사 스크립트는 가능하면 async를, 더 가능하면 defer로 지연하는 것이 성능에 도움이 된다
12.5 누적 레이아웃 이동(CLS)
12.5.1 정의
- 페이지의 생명주기 동안 발생하는 모든 예기치 않은 이동에 대한 지표를 개선하는 것이다.
- 다른 지표와 마찬가지로 이 지표가 낮을수록, 즉 사용자가 겪는 예상치 못한 레이아웃 이동이 적을수록 더 좋은 웹사이트이다.
12.5.2 의미
- 누적 레이아웃 이동은 사용자의 가시적인 콘텐츠에 영향을 미쳐야 하기 때문에 뷰포트 내부의 요소에 대해서만 측정한다.
- 최초 렌더링이 시작된 위치에서 만약 레이아웃의 이동이 발생한다면 누적 레이아웃 이동 점수로 기록하게 된다.
- 단순히 요소가 추가된다고 해서 무조건 누적 레이아웃 이동으로 간주되는 것은 아니다. 요소가 추가됐다 하더라도 다른 요소의 시작 위치에 영향을 미치지 않았다면 레이아웃 이동으로 간주되지 않는다.
- 사용자 액션으로 인해 발생한 레이아웃 이동은 점수에 포함되지 않는다. - 이 점수를 계산할 때 포함되는 내용은 다음과 같다
- 영향분율: 레이아웃 이동이 발생한 요소의 전체 높이와 뷰포트 높이의 비율을 의미한다
- 거리분율: 레이아웃 이동이 발생한 요소가 뷰포트 대비 얼마나 이동했는지를 의미한다
12.5.3 예제
12.5.4 기준 점수
- 좋은 점수는 0.1 이하인 경우, 0.25 이하인 경우 보통이며 그 외에는 개선이 필요한 나쁜 점수로 보고된다
12.5.5 개선 방안
삽입이 예상되는 요소를 위한 추가적인 공간 확보
- useEffect의 내부에서 요소에 영향을 미치는 작업, 특히 뷰포트 내부에서 노출될 확률이 높은 작업은 최소화하는 것이 좋다.
- 스켈레톤 UI처럼 미리 무언가 동적으로 뜰 것으로 예상되는 공간을 미리 확보해 두는 것도 좋은 방법이다. 레이아웃 이동을 막으면서 클라이언트 시점에 정해지는 콘텐츠를 안정적으로 보여줄 수 있다
- 가장 좋은 방법은 서버 사이드 렌더링이다. 서버에서 이러한 동적인 요소의 유무를 사전에 판단해 클라이언트에 HTML을 미리 제공해 준다면 클라이언트에서는 이러한 고민을 할 필요 없이 깔끔하게 처리할 수 있다.
폰트 로딩 최적화
-
폰트로 인해 레이아웃 이동이 발생할 수 있는 문제는 아래 2가지이다
- FOUT: HTML 문서에서 지정한 폰트가 보이지 않고 대체 기본 폰트로 보이다가 뒤늦게 폰트가 적용되는 현상
- FOIT: HTML 문서에서 지정한 폰트가 보이지 않고, 기본 폰트도 없어서 텍스트가 없는 채로 있다가 뒤늦게 폰트가 로딩되면서 페이지에 렌더링되는 현상
-
사용자 기기의 기본 폰트 이외에 다른 폰트로 웹페이지를 보여주고 싶다면 아래와 같은 점을 유념해야 한다
<link>의 preload 사용
<link> 요소의 rel=preload는 페이지에서 즉시 필요로 하는 리소스를 명시하는 기능이다.
- preload로 지정된 요소는 웹페이지의 생명주기에서 초기에 불러와야 하는 중요한 리소르로 간주되므로 브라우저는 리소를 더 빠르게 사용할 수 있도록 준비해 준다.
- 따라서 rel=preload로 스타일이나 폰트를 지정하면 페이지의 렌더링을 가로막거나 레이아웃을 방해할 가능성이 줄어든다.
- font-family
- 폰트를 불러올 수 있는 방법은 크게 5개로 나뉜다
- auto(기본값): 브라우저가 폰트를 불러오는 방법을 결정한다.
- block: 폰트가 로딩되기 전까지 렌더링 중단(최대 3초), 웹 폰트의 로딩이 완료되면 비로스 폰트 적용
- swap: 앞서 언급한 FOUT 방식, 폴백 폰트로 렌더링한 다음, 웹 폰트의 로딩이 완료되면 웹 폰트 적용
- fallback: 100ms간 텍스트가 보이지 않고 그 이후에 폴백 폰트로 렌더링, 3초 안에 폰트가 로딩되면 해당 웹 폰트로 전환하고, 그렇지 않으면 폴백 폰트 유지
- optional: fallback과 유사, 100ms 동안 보이지 않고 폴백 폰트로 렌더링한다는 점은 동일하나 네트워크 상태를 파악해 0.1초 이내로 폰트가 다운로드되지 않으면 해당 폰트를 보여주지 않음
-
요약하자면 최대한 중요한 폰트의 다운로드를 우선순위에 밀어넣고, 이 우선순위를 활용했음에도 빠르게 로딩하는데 실패했다면 다음을 기약하고 기본 폰트를 노출하는 것이다.
적절한 이미지 크기 설정
-
많은 웹사이트가 반응형 웹사이트를 구현하면서 누적 레이아웃 이동이 커지는 결과를 낳게 되었다
-
width:100% height:auto로 지정하면 이미지의 높이에 자동으로 비례해서 auto를 설정해준다. 하지만 이미지가 완전히 다운로드되기 전까지는 높이를 알 수 없기 때문에 이미지의 높이를 높게 잡아 뒀다가 레이아웃 이동이 크게 발생한다
-
적절한 이미지 크기를 설정하기 위한 방법은 아래와 같다
-
width, height 지정
- width:100% height:auto; 와 함께 width, height를 원하는 비율로 지정하면 브라우저가 이미지를 로딩하기 전에 적절한 가로세로 비율을 계산해 이미지가 표시되는 만큼 면적을 할당해 둔다.
- 이는 aspect-ratio 속성 덕분인데, 이 속성은 브라우저의 유저 에이전트 스타일시트에 포함돼 있으며, 이미지의 가로세로 비율을 자동으로 맞춰주는 역할을 한다
import "./style.css";
export default function App() {
return (
<div className="App">
<img src="/image.jpg" alt="이미지" width="1600" height="900" />
</div>
);
}
- 만약 뷰포트 너비에 맞춰서 다른 이미지를 제공하고 싶은 경우 srcset 속성을 사용하는 것이 좋다
<img
width="1000"
height="1000"
src="image-1000.jpg"
srcset="image-1000.jpg 1000w, image-2000.jpg 2000w, image-3000.jpg 3000w"
alt="이미지"
/>
12.5.6 핵심 웹 지표는 아니지만 성능 확인에 중요한 지표들
최초 바이트까지의 시간(Time To First Byte, TTFB)
- 페이지를 요청했을 때 요청이 완전히 완료되는 데 걸리는 시간을 측정하는 것이 아니라 최초의 응답이 오는 바이트까지가 얼마나 걸리는지를 측정하는 지표이다.
- 이 지표는 600ms 이상 걸릴 경우 개선이 필요한 것으로 간주된다
- 최초 바이트까지의 시간을 개선하기 위한 고려사항
- 서버 사이드 렌더링을 수행하고 있다면 로직와 API 호출을 최적화해서 페이지를 빨리 준비 시켜야 한다
- 웹페이지의 주된 방문객의 국적을 파악해 해당 국적과 가깝게 서버를 위치시키는 것이 좋다
- 리액트 서버 사이드 렌더링이라면 renderToNodeStream, renderToStaticNodeStream과 같은 스트리밍 API를 사용하는 것이 좋다
최초 콘텐츠풀 페인트(FCP)
- 웹사이트에 접속한 순간부터 페이지에 뭐라도 뜨기 시작한 시점까지의 시간이다
- 일반적으로 좋은 점수는 1.8초 이내이며, 3.0초 이내는 보통, 그 이후는 개선이 필요하다
- FCP 개선방법
- TTFB를 개선한다. 렌더링을 최대한 빠르게 하기 위해 최초 바이트까지의 시간을 단축해야 한다
- 렌더링을 가로막는 리소스를 최적화한다
- 페이지 리다이렉트를 최소화한다.
- DOM이 복잡하고 크다면 렌더링되는데 시간이 오래 걸리기 때문에 크기를 최적화한다