완벽한 Lighthouse 점수를 만드는 모든 최적화 기법

저는 제 사이트에서 지속적으로 Lighthouse를 실행합니다. 로컬 실행 시에는 항상 100점을 유지하며, Vercel의 Real Experience Score에서도 100점을 기록합니다.

이 점수들은 단순히 일반적인 체크리스트를 따랐기 때문에 얻은 것이 아닙니다. 브라우저가 해야 할 작업을 빌드 단계로 옮김으로써 달성한 결과입니다.

제가 정확히 어떻게 하는지 소개합니다.

작업을 빌드 타임으로 이동하기

대부분의 가이드는 모든 것을 지연 로딩(lazy-load)하라고 말합니다. 하지만 저는 프리렌더링(prerender)을 선호합니다. 저는 프리렌더링이 활성화된 TanStack Start를 사용합니다.

이렇게 하면 빌드 중에 사이트 전체가 정적 HTML로 변환됩니다. 브라우저는 첫 페이지를 보여주기 위해 무거운 JavaScript를 실행할 필요가 없습니다. 사용자가 엔터를 누르는 순간 HTML은 이미 준비되어 있습니다.

복잡한 로직 사전 계산하기

제 홈페이지에는 5,000개의 점이 있는 세계 지도가 있습니다. 보통은 라이브러리가 GeoJSON을 파싱하고 메인 스레드에서 수학 연산을 수행합니다. 이 과정에서 페이지가 1,000ms 동안 차단됩니다.

저는 수학 연산을 빌드 스크립트로 옮겨 이 문제를 해결했습니다.

  • 5,000개의 점 전체를 위한 단일 SVG path 문자열을 생성합니다.
  • 브라우저가 5,000개의 개별 원을 렌더링하는 것보다 하나의 경로(path)를 렌더링하는 것이 훨씬 빠릅니다.
  • 좌표 조회 테이블(lookup tables)을 미리 계산하여 브라우저가 런타임에 수학 연산을 전혀 하지 않도록 합니다.

1,000ms의 지연 시간이 단 한 번의 즉각적인 페인트(paint)로 바뀝니다.

폰트 로딩 최적화

주요 폰트에는 rel="preload"를 사용합니다.

흔히 하는 실수는 crossOrigin 속성을 잊는 것입니다. 이 속성을 생략하면 브라우저가 폰트를 두 번 가져오게 됩니다. 이는 Largest Contentful Paint (LCP)를 망가뜨립니다. 저는 화면 상단(above the fold)에 사용되는 세 가지 폰트만 프리로드합니다.

애니메이션에 적합한 도구 사용하기

지도 마커의 간단한 펄스(pulse) 애니메이션에는 SMIL을 사용합니다. React 상태(state)를 사용하여 애니메이션 루프를 구동하는 것보다 비용이 저렴합니다. 또한 브라우저가 컴포지터 스레드(compositor thread)에서 작업을 처리할 수 있게 해줍니다.

복잡한 경로에는 motion을 사용합니다. 최대한 단순하게 유지합니다. 마운트 시점에 한 번만 애니메이션을 실행하고, 스크롤 위치를 감지하는 것은 피합니다.

벡터와 WebP 사용하기

로고나 도형이라면 SVG를 사용하세요. 사진이라면 WebP를 사용하세요. 이렇게 하면 파일 크기를 낮게 유지하고 레이아웃 시프트(layout shifts)를 방지할 수 있습니다.

과도한 엔지니어링 피하기

저는 이미지 CDN을 사용하지 않습니다. 복잡한 코드 분할(code-splitting)도 사용하지 않습니다. 제 사이트는 규모가 작기 때문에 라우트 단위의 분할(route-level splitting)만으로도 충분합니다.

완벽한 점수는 허영 지표(vanity metric)가 될 수 있습니다. 진짜 목표는 성능을 측정하고, 가능한 한 많은 작업을 사용자의 기기에서 분리해내는 것입니다.

My Portfolio: brodin.dev

Source code: github.com/NathanBrodin/Portfolio

TanStack Start prerendering: tanstack.com/start

Paper Shaders: shaders.paper.design

Full post: https://dev.to/nathan-brodin/every-optimization-behind-a-perfect-lighthouse-score-283n