[딜리버블] ← 해당 프로젝트에 적용한 내용입니다. 

 

LCP(Largest Contentful Paint)란?

LCP는 웹의 성능을 측정하기 위한 대표적인 지표 중 하나입니다. 이름 그대로 페이지가 처음 로드를 시작한 시점부터 뷰포트내에 있는 가장 큰 이미지 또는 텍스트 블록이 렌더링 되는 시점까지 걸리는 시간을 의미합니다. 

좋은 사용자 경험을 제공하기 위해서는 페이지가 처음으로 로딩된 후 2.5초 이내인 것이 좋다고 합니다. 

 

LCP를 측정하는 방법은 여러가지가 있지만 저는 크롬 브라우저 내 개발자 도구에서 사용할 수 있는 라이트 하우스라는 퍼포먼스 측정 도구를 사용하였습니다. (크롬 확장 프로그램 설치 필요)

 

LCP 최적화 전

제가 개선하고자 하는 페이지는 딜리버블의 홈 페이지입니다. 

 

홈의 상단에는 다음과 같은 3개의 배너 슬라이드가 존재합니다. 

또한 첫 번째 슬라이드 같은 경우는 화면의 너비가 960px 이하가 되면 다른 디자인의 마이크 이미지가 보입니다. 

상단 배너에 들어가는 이미지만 모두 5개입니다.

  • 첫 번째 슬라이드의 배경
  • 첫 번째 슬라이드의 마이크 이미지 2개
  • 두 번째 슬라이드의 이미지
  • 세 번째 슬라이드의 이미지

이미지 최적화가 제대로 되어 있지 않으면 좋은 LCP를 기대하기 어려울 것 같습니다. 

 

라이트 하우스를 통해서 로컬 환경에서 LCP를 측정하니 네트워크 페이로드가 커지지 않게 관리하라는 진단이 나왔습니다. 목록을 열어보니 상위권에 배너의 이미지들이 나열되어 있습니다. 

 

 

추가로 렌더링 차단 리소스를 제거하라는 추천도 해주었습니다. 

Pretendard 웹폰트를 불러오는 과정에서 문제가 있는 것 같습니다. 

 

참고로 Next.js의 공식 문서에는 라이트 하우스의 정확한 성능 측정을 위해 프로덕션 환경에서 테스트하는 것을 권장합니다. 로컬에서 yarn build로 프로덕션 빌드를 한 후, yarn start로 실행해서 테스트 하겠습니다. 

yarn build && yarn start

 

LCP 최적화 전의 성능은 다음과 같습니다. 

 

웹 폰트 preload하기

먼저 라이트 하우스가 추천해준 렌더링 차단 리소스 제거가 무슨 의미인지 알아보겠습니다. 

 

[출처] WebFont 로딩 및 렌더링 최적화

브라우저가 페이지를 렌더링하려면 먼저 HTML 마크업을 파싱하여 DOM을 빌드해야 합니다. 이 과정에서 외부 웹 폰트 링크로 정의된 부분을 만나면 해당 리소스가 다운로드될 때까지 기다려야 하며, 이로 인해 페이지의 첫 렌더링 시간이 지연될 수 있습니다. 

 

개발자 도구를 통해 네트워크 waterfall을 보겠습니다. 

브라우저는 css 파일을 다운로드 받기 전에는 어떤 폰트 요청을 보내야 하는지 알 수 없습니다. 폰트를 얻는 과정은 다음과 같습니다. 

  1. 브라우저가 웹 폰트 css가 필요함을 확인하고 GET을 요청합니다. 
  2. 브라우저가 css를 읽고 폰트를 요청합니다. 
  3. 브라우저가 폰트를 획득합니다. 

 

페이지에서 중요도가 높은 자원(폰트, 이미지, 스크립트 파일 등)을 의도적으로 먼저 로딩할 때 preload 옵션을 사용할 수 있습니다. 이제 CSSOM이 생성될 때까지 기다릴 필요가 없습니다.

<link
  rel="stylesheet preload"
  as="style"
  crossOrigin=""
  href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css"
/>

<link> 태그의 rel 속성에 preload 옵션을 추가했습니다. 또한, 기존 다이나믹 서브셋보다 용량이 더 적은 가변 다이나믹 서브셋으로 변경하였습니다. (22.0 kB → 13.3kB)

예상 절감치대로 LCP가 2.0s에서 1.8s로 0.2s 줄어든 것을 볼 수 있습니다. 

 

avif 포맷 사용하기

png나 jpg 등 기존의 포맷보다 작은 용량을 자랑하는 webp지만 여전히 용량이 큽니다. webp보다 20% 더 높은 압축률을 보여준다는 avif를 적용해봅시다. 

[참고] avif는 일반적으로 인코딩하는 데 20% 더 오래 걸리지만 webp에 비해 20% 더 작게 압축됩니다. 즉, 이미지를 처음 요청할 때는 일반적으로 속도가 느리고 이후 캐시된 요청은 더 빨라집니다.

 

[출처] Next.js 공식문서

Next.js는 v12.0.0부터 avif를 지원하기 시작했습니다. 다음과 같이 next.config.js를 설정하면 avif 지원을 활성화할 수 있습니다. 

module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'], // default: ['image/webp']
  },
};

 

Next.js의 Image Optimization API는 요청의 Accept 헤더를 통해 브라우저에서 지원되는 이미지 형식을 자동으로 감지합니다. 만일 Accept 헤더가 설정한 포맷 중 둘 이상과 일치하는 경우 배열에서 첫 번째로 일치하는 포맷이 사용됩니다. 일치하는 항목이 없다면 Image Optimization API는 원본 이미지의 포맷으로 fallback 합니다. - Next.js 공식문서

 

Next.js에서 avif 포맷을 사용하고 싶다면 당연하지만 Image 컴포넌트를 사용해야 합니다. 

background: url('/assets/images/img_banner_ver${ver}_deco.webp') no-repeat right / auto;

하지만 저는 위 코드처럼 배너 이미지의 대부분을 위와 같이 background 속성을 이용해서 보여주고 있었습니다. 당연히 avif가 적용이 안되었고 용량은 그대로였습니다.

 

방법이 없을까 하면서 찾아보다가 background-image가 있는 LCP는 적절하지 못하다는 글까지 보게 되었고, 결국 첫 번째 슬라이드의 배경 이미지를 제외하고 나머지 이미지는 모두 Image 컴포넌트를 사용하는 방식으로 리팩토링하였습니다. 오히려 좋아 ;D..

 

CSS에 정의된 리소스(url 을 통해 요청된 모든 리소스)는 기본적으로 느리다. 여기서 말하는 리소스는 background-image와 web font 등이다. 이러한 리소스가 느린 이유는, 브라우저가 해당 리소스를 필요로 하는 DOM 노드를 그릴 준비가 될 때 까지 리소스를 요청하지 않기 때문이다. 이는 background-image 가 LCP의 마지막 순간에 요청된다는 것을 의미인데, 이는 사실 너무 늦은 타이밍이긴하다. 따라서 background-image가 있는 LCP는 적절하지 못하다.
- Largest Contentful Paint (LCP) 최적화하기

 

이미지의 크기가 엄청나게 감소한 것을 볼 수 있다. avif로 포맷이 바뀐 것 말고도 Next.js의 Image 컴포넌트를 사용함으로써 기본적으로 얻을 수 있는 최적화가 이루어진 듯 하다.

 

LCP 최적화 후

LCP 최적화 후 2.0s → 1.4s로 시간이 감소한 것을 확인할 수 있습니다. 🥳

 

 

복사했습니다!