Lighthouse 100점에 집착해봤다
Article
이 블로그를 만들고 나서 처음 Lighthouse를 돌렸을 때 Performance가 87점이었다. 나쁘지 않은 점수다. 그런데 87이라는 숫자를 보고 나니 100이 아닌 이유가 궁금해졌다. 그래서 한동안 집착해봤다.
폰트가 제일 컸다
가장 큰 감점 요인은 폰트였다. 처음에는 구글 폰트를 <link>로 불러오고 있었는데, 이러면 외부 요청이 하나 더 생기고 폰트가 로드되기 전까지 텍스트가 흔들린다. CLS 감점의 주범이었다.
next/font/google로 바꾸면 빌드 타임에 폰트를 셀프 호스팅하고, font-display: swap과 폴백 폰트 메트릭 조정까지 알아서 해준다.
import { Noto_Sans_KR } from "next/font/google";
const notoSansKr = Noto_Sans_KR({
subsets: ["latin"],
variable: "--font-noto-sans-kr",
display: "swap",
});
이것 하나로 CLS가 거의 0에 수렴했다. 폰트 최적화는 노력 대비 효과가 가장 확실한 항목이다.
이미지는 sizes가 핵심이었다
next/image는 이미 쓰고 있었다. 그런데 sizes를 안 넣으면 뷰포트 전체 폭 기준으로 이미지를 내려받는다. 카드 그리드에서 실제로는 화면의 3분의 1만 차지하는 썸네일인데도.
<Image
src={post.thumbnail}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, 33vw"
/>
sizes를 레이아웃에 맞게 선언하니 모바일에서 내려받는 이미지 용량이 눈에 띄게 줄었다. 히어로 이미지에는 priority를 붙여서 LCP도 앞당겼다.
'use client'를 걷어냈다
제일 재미있었던 부분이다. 컴포넌트를 뒤져보니 'use client'가 습관적으로 붙어 있는 파일이 꽤 있었다. useState도 이벤트 핸들러도 없는데 그냥 붙어 있는 것들.
'use client'가 붙으면 그 컴포넌트의 JS가 클라이언트 번들에 포함된다. 서버 컴포넌트로 되돌리면 그만큼 번들이 빠진다. Footer, 카드 목록 같은 정적인 컴포넌트에서 지시어를 지우고, 정말 상호작용이 필요한 부분(스크롤 버튼, 검색 입력)만 작은 클라이언트 컴포넌트로 분리했다. First Load JS가 수십 KB 줄었다.
'use client'는 파일 단위가 아니라 상호작용 단위로 붙이는 게 맞다.
그래서 100점을 찍었는데
며칠 만에 네 항목 모두 100을 찍었다. 스크린샷도 찍어뒀다. 그런데 솔직히 말하면, 87점일 때와 100점일 때 체감 속도 차이는 크지 않다. 원래도 빠른 사이트였으니까.
남은 건 점수가 아니라 과정에서 얻은 이해다. 폰트가 언제 로드되는지, 이미지가 어떤 크기로 내려오는지, 내 번들에 뭐가 들어 있는지 이제는 안다. 이건 다음 프로젝트에서도 그대로 쓰인다.
100점은 한 번쯤 찍어볼 만하다. 다만 거기 계속 머물 필요는 없다. 점수는 도구지 목표가 아니다. 숫자에 과몰입하기 시작하면 0.01점 때문에 사용자에게 아무 의미 없는 코드를 비틀게 된다. 나는 딱 그 직전에서 멈췄다.