이미지 최적화 진행기
성능 측정하기
저는 이미 배포된 프로덕션 모드에서 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
는 페이지가 클릭, 키보드 입력 등의 사용자 입력에 응답하지 않도록 차단된 시간을 총합한 지표입니다. 측정은 FCP
와 TTI
사이의 시간동안 일어납니다.
🖇️ Cumulative Layout Shift (CLS) - 15%의 가중치
CLS
는 페이지 로드 과정에서 발생하는 예기치 못한 레이아웃 이동을 측정한 지표입니다. 레이아웃 이동이란 화면상에서 요소의 위치나 크기가 순간적으로 변하는 것을 말합니다.
(Ref : 프론트 엔드 성능 최적화 가이드 )
어떤 부분을 최적화 해야할까요?
위의 라이트 하우스의 측점 점수 아래에, 진단해주는 문제점을 기준으로 최적화를 진행할 예정입니다. 저는 다음과 같은 부분에서 문제가 발생했습니다.
대부분 이미지 관련 문제였으므로, 우선 이미지의 문제를 해결합니다.
이미지 사이즈 최적화
첫번째로 진행할 부분은 이미지 사이즈와 관련된 부분입니다. 이미지는 블로그를 사용하는 입장에서 꼭 필요한 부분이었고, 저의 블로그는 상당히 많은 이미지들을 불러오고 있었습니다. 그 중, 하나의 이미지를 대상으로 최적화를 진행해보도록 하겠습니다.
첫번째로 진행할 부분은 이미지 사이즈와 관련된 부분입니다.
문제가 된 이미지 부분을 찾아가 보니, 렌더링된 크기와 원본 크기를 확인할 수 있었습니다. 화면에 실제로 렌더링되는 크기는 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 포맷 사용하기
기존에 사용하고 있는 이미지 포맷은 png
와 jpg
였습니다. 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 )
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
점수를 올릴 수 있었습니다.