Lighthouse Performance의 LCP 기준으로 2.5s이하를 목표로 성능을 개선해보면서 Next.js에서 어떻게 성능을 최적화하는 방법과 과정을 작성해보겠습니다.
FCP와 LCP는 웹 페이지의 로딩 성능을 측정하는 두 가지 중요한 지표입니다. 사용자가 웹 페이지를 방문했을 때 콘텐츠가 얼마나 빠르게 화면에 나타나는지를 나타내며, 웹 사이트의 사용자 경험을 개선하는 데 핵심 지표입니다.
FCP는 방문자가 페이지를 로딩 시작한 후 화면에 첫 번째 콘텐츠가 표시되는 시간을 측정합니다.
FCP는 사용자가 실제로 페이지에서 인식할 수 있는 첫 번째 지점을 나타냅니다. 빈 페이지에서 시작하여 어떠한 하나의 요소라도 보여지기 까지의 시간입니다.
즉 로딩 스피너가 돌아가도 FCP의 측정 종료 시점이 됩니다.
LCP는 페이지의 가장 큰 콘텐츠 요소가 화면에 완전히 로드되고 표시되는 데 걸리는 시간을 측정합니다.
LCP는 사용자가 페이지의 주요 콘텐츠를 볼 수 있게 되는 시점을 나타내므로 중요한 지표입니다.
좋은 LCP 점수는 사용자가 사이트의 주요 콘텐츠를 빠르게 볼 수 있음을 의미합니다.
FCP에는 중요한 요소가 화면에 보여지는 시점을 측정합니다. 로딩 스피너만 렌더링 되어도 측정 종료 기준에 잡힙니다.
만약 페이지 접속 시 0.1초만에 로딩 스피너가 렌더링되고, 실제 페이지 요소는 10초 뒤에 보여진다면 FCP점수는 좋겠지만 실제 중요한 요소의 렌더링 측정 즉 LCP는 좋지 않습니다.
Google은 단순히 FCP 측정 보다 의미있는 LCP 기준을 더 중요한 측정 요소라고 말합니다.
저 역시 FCP는 0.3s가 측정되지만 정작 중요한 컨텐츠는 3.1s로 빨간색 경고 표시가 발생합니다.
LCP의 권장 속도는 2.5초 이하를 권장하므로 LCP 기준으로 개선하도록 시도 하겠습니다.
라이트 하우스에서 LCP 요소 즉 큰 콘텐츠 부분을 설명해주고 있었습니다.
폰트 로드가 오래 걸리는가?
처음에 바로 의심했던 부분은 폰트 로드가 오래걸려 늦게 보이는가 싶었습니다. 그래서 나름 폰트 최적화를 시도 했습니다.
woff2는 ttf보다 약 30% 더 효율적으로 압축할 수 있습니다. woff2 포맷은 웹 페이지의 로딩 시간을 단축시키고 사용자 경험을 향상시키는 데 도움이 됩니다.
용량 크기 : 80.5kb -> 58.5kb
로드 시간 : 200ms -> 175ms
ttf와 woff2의 성능을 비교해봤는데 확실히 woff2 포맷이 더 우수한 성능을 보여줍니다. 하지만 LCP를 다시 측정해도 개선이 되지 않았습니다.
여기서 의문점이 들었습니다. 어처피 display: swap 속성을 통해 폰트가 로드되기 전에 기본 폰트로 먼저 보여주는데 폰트와 LCP가 영향이 있을까?? 그래서 생각되었던 부분이 있었는데
LCP에 많은 영향을 주는 이미지가 최적화되지 않았나? 생각해서 이미지에 관련하여 개선을 해보았습니다.
이미지를 최적화하는 작업은 성능 개선에 효율이 매우 좋습니다. 이미지 최적화는 곧 LCP 성능에도 많은 영향을 줍니다.
사용자가 웹 페이지를 로드할 때, 이미지 파일들이 서버로부터 다운로드되어야 하며, 이미지 파일 크기가 크면 클수록 더 많은 데이터를 전송해야 하고 이는 로딩 시간을 증가시킵니다.
WebP와 AVIF은 JPEG, PNG에 비해 더 나은 무손실 압축 효율을 제공하여 더 작은 파일 크기로 고품질의 이미지를 전송할 수 있게 합니다.
손실 압축은 원본 데이터에서 일부 정보를 영구적으로 제거하여 파일 크기를 줄이는 방식입니다.
제거된 정보는 복구할 수 없으며, 압축률이 높을수록 원본 데이터와의 품질 차이가 커질 수 있습니다.
무손실 압축은 원본 데이터의 모든 정보를 유지하면서 파일 크기를 줄이는 방식입니다.
압축된 파일은 복원 과정을 통해 원본 데이터와 완전히 동일한 상태로 복구될 수 있습니다.
이 방법은 텍스트, 소스 코드, 중요한 문서 파일, 고품질 이미지 등 원본 데이터의 정확성이 중요한 경우에 주로 사용됩니다.
WebP형식은 JPEG, PNG보다 약 26% 더 효율적인 압축을 제공합니다. 하지만 WebP보다 20% 높은 압축률을 자랑하는 형식이 AVIF 입니다.
AVIF 형식은 JPEG, PNG에 비해 약 30%에서 50% 더 작은 파일 크기를 제공할 수 있습니다.
Next.js에서는 next/image를 사용하면 기본적으로 각 디바이스와 브라우저에 최적화된 이미지를 생성하여 응답으로 전달해주고 WebP형식으로 변환해줍니다.
하지만 AVIF 형식을 사용해서 더 높은 압축률을 사용하기 위해 설정이 필요합니다. next.config.js에서 아래 설정을 추가해주십다.
images: {
formats: ['image/avif', 'image/webp'],
},
이렇게 설정하면 우선적으로 AVIF 형식을 사용하고 만약 지원하지 않는 브라우저라면 WebP 형식으로 변환을 합니다.
용량 크기 : 5.1kb -> 4.4kb, 67.7kb -> 62.7kb, 91.9kb -> 81.8kb
로드 시간 : 94ms -> 60ms, 97ms -> 63ms, 103ms -> 64ms
AVIF 형식이 적용된 네트워크 탭 모습입니다. WebP형식 보다 높은 압축률과 로드 시간도 더 단축되었습니다.
WebP를 지원하지 않는다면 걱정을 할 수 있지만 지원 종료한 익스플로러 브라우저에만 지원하지 않기 때문에 걱정하지 않고 사용해도 됩니다.
https://compressor.io/ 해당 사이트에서 정적 이미지들의 용량을 압축해서 더 용량 크기를 줄였습니다.
용량 크기 : 5.1kb -> 4.4kb, 67.7kb -> 62.7kb, 91.9kb -> 81.8kb
로드 시간 : 94ms -> 60ms, 97ms -> 63ms, 81.8kb -> 64ms
Next.js는 기본적으로 Squoosh를 기본 이미지 최적화 모듈로 사용하고 있습니다. 하지만 Next.js에서 배포 환경에서는 Sharp를 권장하고 있습니다.
Sharp 모듈은 다양한 크기의 JPEG, PNG, WebP, GIF, AVIF와 같은 이미지들을 더 작은 크기로 변환해 주는 매우 빠른 속도의 모듈 입니다.
Warning: For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically
for Image Optimization.
실제로 build 시 Sharp를 권장하는 경고 문구를 알려주고 있습니다.
로드 시간 : 20ms -> 16ms, 34ms -> 13ms, 34ms -> 16ms
용량 크기 변화는 없지만 응답 시간은 Squoosh보다 Sharp가 더 빠르게 개선되었습니다.
하지만 여전히 LCP에서는 변화가 없었습니다.. 그래서 어디서 LCP 성능을 잡아먹는지 찾아보다가 라이트 하우스에서 render dlay라는 퍼센트와 페이지 네트워크 Preview 탭에서 힌트를 얻었습니다.
추측해보자면 LCP요소에는 이미지에 관련이 없어서 이미지 개선에 영향을 받지 않았고 LCP에 필요한 파일 로드는 매우 짧지만 렌더링 쪽에서 많은 영향을 받고 있었습니다.
그리고 애초에 정적인 렌더링 요소인데 이상하게 서버에서는 빈 텍스트로 렌더링되고 있고 클라이언트에서 텍스트를 렌더링하고 있었습니다.
LCP에 해당하는 큰 콘텐츠 부분의 코드입니다.
const MainIntroSection = () => {
return (
<div className="wrap bg-gradient bg-gradient-to-b from-white to-blue-100">
<MainSectionAnimationWrapper className="main-section-intro wrap flex flex-col items-center justify-center p-[2rem] text-[4.4rem] tablet:text-[6rem]">
<h2 className="flex flex-col justify-center font-bold xl:flex-row xl:gap-[1rem]">
<p>
<span className="text-primaries-primary">예상 질문</span>부터
</p>
<p>
<span className="text-primaries-primary">모의 면접</span>까지
</p>
</h2>
<h2 className="flex w-full justify-center font-bold">
개발 면접의 모든 것
</h2>
<p
className={`text-[8rem] xl:text-[9rem] ${goldenPlainsFont.className}`}
>
Honterview
</p>
<ArrowDownSecondaryIcon className="arrow_down absolute bottom-[2rem] h-[5rem]" />
</MainSectionAnimationWrapper>
</div>
);
};
MainSectionAnimationWrapper 컴포넌트에서는 children 요소가 화면에 보이는 즉 inView 상태가 되면 opacity 속성이 1로 변하게 됩니다.
children의 초기 상태는 hidden으로 화면에 보이지 않는 상태이기에 서버 사이드에서는 빈 텍스트 상태를 렌더링하게 되었던 것 입니다.
첫 번째로 해당 LCP 코드는 페이지 진입 시 화면에 바로 보여지는 콘텐츠여서 초기 상태를 inView 상태로 수정했습니다.
하지만 여전히 서버 사이드에서는 빈 텍스트를 렌더링하고 있었고 LCP 수치는 0.1s 정도 개선되는 모습을 보니 핵심 문제는 아니였습니다.
그래서 설마하고 생각한 문제가 opacity 애니메이션이 완전히 끝나는 타이밍에 LCP 측정이 종료되는 건가 싶어서 애니메이션을 제거 했습니다.
LCP : 3.1s -> 0.8s
결과는 애니메이션 문제였습니다. 애니메이션이 완전히 끝나야 LCP 측정이 끝나고 서버 사이드에서도 애니메이션 때문에 렌더링이 되었지만 opacity가 0이기 때문에 빈 텍스트처럼 보였던 것 입니다.
https://ui.toast.com/posts/ko_20220426 해당 글에서도 LCP 타겟에서는 lazy loading 기법이나 fade-in 애니메이션 적용을 피하는 것을 추천합니다.
LCP를 개선하면서 LCP 요소 외 성능을 개선한 부분때문인지 전체적인 성능이 개선되었습니다.. 개선 사항을 보자면
페이지 로드 시간 : 1.06s -> 708ms
resources 용량 : 4.2mb -> 3.2mb
LCP 시간 : 3.1s -> 0.8s
아마 LCP 콘텐츠가 이미지가 포함되어있다면 이미지 최적화 부분에서도 많은 영향을 줬을거라고 생각합니다.
추가적으로 모달 같은 상호작용 렌더링 경우 코드 스플릿을 적용하여 번들 사이드도 개선하고 불필요한 RCC를 RSC로 수정하면서 번들 크기도 살짝 줄여봤습니다.
LCP 개선을 통해 적용하거나 학습한 방법을 정리하자면