OTTER-LOG

next 블로그 성능 최적화 진행기, 첫번째

next 블로그 성능 최적화 진행기, 첫번째
by otter2023년 2월 5일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

접근성을 진행하던 중 가장 눈에 띄는 부분은 다음이었습니다.

어쩌구 저쩌구

bookmark

저는 next 어쩌구입니당~~~~~~~

성능 측정하기

저는 이미 배포된 프로덕션 모드에서 lighthouse 를 이용했습니다. 우선, 성능만을 측정할 예정이라 성능탭만 선택하고 진행했습니다.

위와 같은 결과가 나왔습니다. 그런데, 측정항목의 의미가 무엇일까요?

🖇️  First Contentful Paint (FCP) - 10%의 가중치

FCP 는 페이지가 로드될 때 브라우저가 DOM 컨텐트의 첫번째 부분을 렌더링 하는데 걸리는 시간에 관한 지표입니다. 구체적으로, otter-log.world 페이지는 페이지에 진입하여 컨텐츠가 뜨기까지 0.3 초라 걸렸음을 확인할 수 있습니다.

🖇️  Speed Index (SI) - 10%의 가중치

SI 는 페이지 로드 중에 컨텐츠가 시각적으로 표시되는 속도를 나타내는 지표입니다.

🖇️  Largest Contentful Paint(LCP) - 25%의 가중치

LCP 는 페이지가 로드될 때 화면 내에 있는 가장 큰 이미지나 텍스트 요소가 렌더링되기까지 걸리는 시간을 나타내는 지표입니다. otter-log.world 페이지는 가장 큰 컨텐츠가 보여질때까지 1.6 초가 걸렸음을 확인할 수 있습니다.

🖇️  Time to Interactive (TTI) - 10%의 가중치

TTI 는 사용자가 페이지와 상호 작용이 가능한 시점까지 걸리는 시간을 측정한 지표입니다. 여기서 상호작용은 클릭 또는 키보드 누름같은 사용자 입력을 의미합니다. 이 시점 전까지느 화면이 보이더라도 클릭 같은 입력이 동작하지 않습니다.

🖇️  Total Blocking Time (TBT) - 30%의 가중치

TBT 는 페이지가 클릭, 키보드 입력 등의 사용자 입력에 응답하지 않도록 차단된 시간을 총합한 지표입니다. 측정은 FCPTTI 사이의 시간동안 일어납니다.

🖇️  Cumulative Layout Shift (CLS) - 15%의 가중치

CLS 는 페이지 로드 과정에서 발생하는 예기치 못한 레이아웃 이동을 측정한 지표입니다. 레이아웃 이동이란 화면상에서 요소의 위치나 크기가 순간적으로 변하는 것을 말합니다.

(Ref : 프론트 엔드 성능 최적화 가이드 )

어떤 부분을 최적화 해야할까요?

위의 라이트 하우스의 측점 점수 아래에, 진단해주는 문제점을 기준으로 최적화를 진행할 예정입니다. 저는 다음과 같은 부분에서 문제가 발생했습니다.

대부분 이미지 관련 문제였으므로, 우선 이미지의 문제를 해결합니다.

이미지 사이즈 최적화

이미지 최적화와 관련해, 저는 크기가 줄어가는 것을 확인해보고 싶었습니다. next 환경이었지만 next/Image 를 적용하지 않고 진행했습니다.

첫번째로 진행할 부분은 이미지 사이즈와 관련된 부분입니다. 이미지는 블로그를 사용하는 입장에서 꼭 필요한 부분이었고, 저의 블로그는 상당히 많은 이미지들을 불러오고 있었습니다. 그 중, 하나의 이미지를 대상으로 최적화를 진행해보도록 하겠습니다.

첫번째로 진행할 부분은 이미지 사이즈와 관련된 부분입니다.

문제가 된 이미지 부분을 찾아가 보니, 렌더링된 크기와 원본 크기를 확인할 수 있었습니다. 화면에 실제로 렌더링되는 크기는 317 * 200 였지만 이미지를 받아오는 원본의 크기는 1350 * 720 이었습니다. 또 이미지의 크기는 411Kb 이었습니다.

일반적으로, 이미지의 크기를 화면에 표시되는 사이즈의 두배정도로 하는 것이 좋다고 하는 레퍼런스를 참고해 600 * 400 정도 크기의 이미지를 불러오는 것이 적절해 보입니다.

(Ref : 프론트 엔드 성능 최적화 가이드 19p )

다른 크기의 이미지를 받아오기

저는 마침 cloudinary 를 이미지 클라우드로 사용하고 있었고, cloudinary 와 같은 경우에는 다음 방법을 통해 다른 크기의 이미지를 받아올 수 있었습니다.

res.cloudinary.com/<cloud name>/image/upload/<이미지 resize 속성>/<cloud id>/...

위의 방법을 통해, url 을 파싱해 필요한 이미지속성을 추가해주는 함수를 작성했습니다. 그리도 다음과 같이, 파싱한 이미지의 크기를 적용했습니다.

const getResizedImage = ({ src }) => { const prefixIndex = src.lastIndexOf("upload"); const prefix = src.slice(0, prefixIndex); const restUrl = src.replace(prefix, "").replace("upload/", ""); const resizedUrl = prefix + `upload/c_thumb,h_400,w_600/` + restUrl; // cloudinary는 중간에 삽입해야 해서 꽤 복잡한 모양새가 되었습니다. 😅 return resizedUrl; }; <img src={getResizedImage({ src: thumbnailImg })} alt={title} className='h-[50%] rounded-t-lg object-cover' /> // 저는 현재 next 환경을 사용하고 있으나 우선 img 태그를 이용했습니다.

이미지의 크기가 400kB 에서 49.7kB 90% 정도로 줄어들었습니다.

이미지를 확인할 때의 고유 크기도 우리가 원하는 바 대로 잘 적용되었음을 확인할 수 있습니다.

webp 포맷 사용하기

기존에 사용하고 있는 이미지 포맷은 pngjpg 였습니다. png 는 무손실 압축 방식으로 원본을 훼손없이 압축하고, jpg 는 압축 과정에서 정보 손실이 발생하지만 더 작은 사이즈로 줄일 수 있습니다.

webp 이미지 포맷은, 무손실 압축과 손실 압축을 모두 제공하는 최신 이미지 포맷으로 기존의 포맷보다 효율적으로 이미지를 압축할 수 있습니다.

저는 cloudnary 를 사용하고 있었으므로 다음 부분을 통해 쉽게 webp 이미로 포맷된 이미지를 불러올 수 있었습니다.

const getResizedImage = ({ src }) => { const prefixIndex = src.lastIndexOf("upload"); const prefix = src.slice(0, prefixIndex); const restUrl = src.replace(prefix, "").replace("upload/", ""); _**const resizedUrl = prefix + `upload/c_thumb,h_400,w_600/f_wepb/` + restUrl;**_ // cloudinary는 f_webp 부분을 추가해주면 webp 포맷으로 이미지를 불러옵니다. return resizedUrl; }; <img src={getResizedImage({ src: thumbnailImg })} alt={title} className='h-[50%] rounded-t-lg object-cover' />

기존에 png 를 사용했을때에는 49.7KB 의 크기였습니다.

webp 포맷을 사용하면, 38.5KB 로 20%정도 크기가 줄어든 것을 확인할 수 있습니다.

그런데 webp 포맷에는 브라우저 호환성의 문제가 존재합니다.

최근에는 대부분의 브라우저에서 문제없이 작동하지만, 세세하게 챙길 필요가 있습니다.

picture

이 문제를 해결하기 위해 <picture> 태그를 사용할 수 있습니다. picture 태그는 다음과 같은 특징을 가집니다.

<picture> <source media="(min-width:650px)" srcset="img_pink_flowers.jpg"> <source media="(min-width:465px)" srcset="img_white_flower.jpg"> // 뷰포트 너비에 따른 다른 이미지를 사용하도록 할 수 있습니다. <img src="img_orange_flowers.jpg" alt="Flowers" style="width:auto;"> // 일치하는 소스 태그가 없는 경우 대체옵션으로 사용됩니다. </picture>

picture 태그는 하나 이상의 source 태그와 하나의 img 태그를 사용합니다. 이 태그는, 일치하는 source 태그가 있다면 source 태그를 사용하고 일치하지 않는다면 img 태그를 사용합니다.

이러한 picture 태그의 속성을 이용해 다음과 같이 진행할 수 있습니다.

<picture> <source srcSet={getResizedWebpImage({ src: thumbnailImg })} // webpImage로 url을 변환하는 함수 type='image/webp' /> // wepb 이미지에 오류가 발생하면 아래 이미지로 대체됩니다. <img src={getResizedImage({ src: thumbnailImg })} // 기존 이미지 포맷으로 변환하는 함수 alt={title} className='h-[50%] rounded-t-lg object-cover' /> </picture>

(ref : HTML <picture> Tag )

next/image 와 함께 사용하기

이제 이미지의 최적화는 마무리 되었고 이를 next image 에 적용해야 합니다. 아마 react 를 사용하시는 분들은 이 부분은 필요없으실 겁니다.

그런데, next image 태그를 사용할 때에 picture 태그를 같이 사용하지 못하는 것으로 보입니다.

(ref : vercel/next.js discussions #25393 )

그래서 다음과 같이 커스텀 image 태그를 작성해 진행했습니다.

import Image, { ImageProps } from "next/future/image"; import { useState } from "react"; interface ImageWithFallbackProps extends ImageProps { // 기존 Image 태그에서 사용하는 Props 타입을 받아왔습니다. fallbackSrc: string; // fallbackSrc만 추가해주었습니다. } const ImageWithFallback = (props: ImageWithFallbackProps) => { const { src, fallbackSrc, alt, ...restProps } = props; const [imgSrc, setImgSrc] = useState(src); return ( <Image {...restProps} src={imgSrc} alt={alt} onError={() => { setImgSrc(fallbackSrc); // Error가 발생하면, fallbackSrc로 다시 렌더링됩니다. }} /> ); };

(ref: What is the best way to have a fallback image in NextJS?)

next/Image 에서 기본적으로 제공하는 최적화까지 진행되면 다음의 이미지 크기를 확인할 수 있습니다.

38.5KB 에서 27.8KB 까지 30% 정도 이미지 크기를 줄일 수 있었습니다.

lazyload 제외하기

그런데, 아직 수정할 부분이 남아있습니다.

light house 에서는 해당 부분이 문제가 된다고 파악하고 있습니다. 일련의 과정중에서 next/image 컴포넌트를 사용하고 있었고 next/image 는 기본적으로 lazyload 가 적용되어 있습니다. 그런데, otter-log 의 메인 페이지는 데스크톱 기준으로 스크롤 없이 모든 이미지가 화면에 적용됩니다. 따라서 이 부분에 레이지로드를 적용할 필요가 없습니다. 오히려 레이지 로드로 인해 성능이 저하되었을 가능성이 존재합니다.

왜냐하면, LCP 점수가 측정될때 지연로드가 되면 지연로드가 된 시간이 측정 시간에 포함되기 때문입니다.

<ImageWithFallback src={getResizedImage({ src: thumbnailImg })} fallbackSrc={getResizedImage({ src: thumbnailImg, format: "webp" })} alt={title} width={600} height={400} className='h-[50%] rounded-t-lg object-cover' priority={true} // next/image는 priority를 true로 설정하면 lazyload 하지 않습니다. />

이미지 최적화 정리하기

이미지 최적화를 위해 진행한 방법은 다음과 같았습니다.

  • 이미지의 크기를 resize 해서 필요한 크기로 불러오기
  • 이미지의 포맷을 webp 로 사용하기
  • 불필요한 이미지 lazyload 제거하기

이러한 과정을 통해 최초 411kB 에서 27.8kB 까지 이미지의 크기를 줄일 수 있었습니다. 그리고 이를 통해, lighthouse 의 문제도 해결되었습니다.

또 불필요한 이미지 lazyload 를 제거해 LCP 점수를 올릴 수 있었습니다.