Skip to content

[Week 03] 강지훈 - 랜딩 페이지 성능 최적화 사후 검증 #14

Description

@theSnackOverflow

1. 작업 내용

6월에 데모데이 매칭 서비스의 소개 랜딩(/intro)을 최적화했습니다. 이미지를 WebP로 바꾸고, 화면 아래 이미지는 지연 로딩으로 돌리고, 코드를 쪼개고, 배경 글로우의 무한 애니메이션을 걷어냈습니다. PR 두 개로 나눠 머지했습니다.

그리고 포트폴리오에 이렇게 적어 뒀습니다.

한계: 최적화 전 baseline을 별도 보관하지 않아 개선 전후 수치 비교를 제시하기 어렵습니다.

두 달째 같은 문장이 지원서 여러 곳에 들어가 있습니다. 이번 주 키워드가 테스트와 벤치마킹이라 이 문장을 다시 봤는데, 전제가 틀렸더군요. baseline은 없어진 게 아니라 git에 있었습니다. 최적화 직전 커밋을 worktree로 꺼내 빌드하면 그때 페이지가 그대로 뜹니다.

그래서 이번 주에 그걸 실제로 했습니다. 각 perf 커밋을 부모 커밋과 1:1로 비교해서, 그 커밋 하나만 차이나는 두 지점을 각각 5회씩 쟀습니다.

결과부터 적으면, 제가 성과라고 적어 둔 항목은 전부 측정 편차 안이었고, 포트폴리오에 안 적은 커밋 하나가 개선의 거의 전부였습니다.

커밋 한 일 전송량 LCP 판정
aea0d1c2 이미지 WebP·SVG 최적화 −1297 KB −20.8 ms 편차 안
3d5b91c9 코드 스플리팅·뷰포트 게이팅 −3 KB −13.6 ms 편차 안
a79a6ab3 이미지 WebP·지연 로딩 −1274 KB −67.2 ms 편차 안
23adde19 인증 SDK 지연 로드 −141 KB −548.8 ms 유의
686f1315 GPU 부하 완화 0 KB −4.0 ms 편차 안

1.3MB를 줄인 커밋은 LCP를 21ms 움직였고, 141KB를 줄인 커밋은 549ms를 움직였습니다.

측정 조건은 M4 Pro / Chromium 145 / 412×823 모바일 뷰포트 / CPU 4배 감속 / RTT 150ms·1.6Mbps / 프로덕션 빌드를 vite preview로 서빙 / 매 실행 새 컨텍스트로 콜드 캐시입니다. 판정은 차이가 두 지점 표준편차의 2배를 넘는지로 잡았습니다.


2. Deep Dive 해본 것

2.1 141KB가 1.3MB보다 센 이유

같은 페이지에서 1274KB를 줄인 커밋은 LCP가 안 움직였고 141KB를 줄인 커밋은 549ms가 움직였습니다. 처음엔 측정이 잘못된 줄 알았습니다.

줄인 대상이 다릅니다. 1274KB는 화면 한참 아래에 있는 목업 이미지들입니다. 아무것도 막지 않고 남는 대역폭으로 천천히 받습니다. 141KB는 index.html에 이렇게 박혀 있던 것들입니다.

<script src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.4/kakao.min.js"></script>
<script src="https://accounts.google.com/gsi/client"></script>

deferasync도 없는 <script src>는 파서를 세웁니다. 게다가 외부 도메인이라 DNS 조회부터 TLS 핸드셰이크까지 새로 해야 합니다. RTT 150ms를 걸어두면 이 왕복이 그대로 첫 렌더 앞에 쌓입니다.

23adde19는 이걸 loadAuthSdk.ts로 옮겨서 로그인이 필요할 때만 받게 바꾼 커밋입니다. /intro는 로그인 페이지가 아니니 아예 안 받습니다.

정리하면 바이트 크기가 아니라 크리티컬 패스 위에 있느냐가 정합니다. 머리로는 알던 얘긴데, 제 경우엔 정반대 순서로 일했습니다. 눈에 보이는 큰 파일부터 줄였고, 크리티컬 패스에 있던 141KB는 나중에 다른 이유로 건드렸습니다.

2.2 GPU 최적화는 로딩 지표로 증명할 수 없다

686f1315은 PR #534의 본체이고, 배경 글로우를 이렇게 바꾼 커밋입니다.

- <motion.div className="... size-[764px] blur-[150px]"
-   animate={{ x: [0, 18, 0], y: [0, -14, 0] }}
-   transition={{ duration: 14, repeat: Infinity }} />
+ <StaticGlow left={760} top={308} size={1004} opacity={0.34} />

764px 원소에 150px 블러를 걸고 무한 애니메이션을 돌리는 게 다섯 개 있었습니다. 이걸 radial-gradient 정적 렌더로 교체했습니다.

측정해보니 LCP −4.0ms, FCP −8.0ms, 전송량 0, CLS 0. 전부 편차 안입니다.

프레임을 재봤습니다. 5초간 requestAnimationFrame 간격을 수집했는데 전후 모두 120fps, 33ms 넘는 프레임 0개로 똑같았습니다. 헤드리스가 문제인가 싶어 실제 GPU로 다시 재도 같았습니다. M4 Pro에는 이 정도 블러가 부담이 아니었습니다.

프레임 드롭은 증상이고, 비용 자체를 재야 했습니다. 유휴 상태에서 메인 스레드 누적 작업량을 재니 그제서야 나왔습니다.

유휴 5초 누적 before after
TaskDuration 0.1047 초 0.0005 초
ScriptDuration 0.0205 초 0.0002 초
RecalcStyleCount 600.2 회 0 회

5초에 600회면 초당 120회, 즉 매 프레임입니다. 아무것도 안 하고 페이지를 띄워만 놔도 스타일 재계산이 계속 돌고 있었습니다. 작업량으로 209배 차이인데 LCP·FCP·CLS·FPS 어느 것으로도 안 보였습니다.

지표를 잘못 고르면 내가 한 일이 안 보입니다. 이 커밋을 Lighthouse 점수로 방어하려 했으면 아무 근거도 못 댔을 겁니다.

한 가지 더. 원래 이 작업의 출발점은 "스크롤이 끊긴다"였는데, 그 현상이 관찰된 기기가 제 맥북이 아닙니다. 저사양 안드로이드에서 나온 얘기였습니다. 그걸 M4 Pro에서 재고 있으니 체감 개선은 애초에 재현이 안 됩니다. 측정 환경이 문제 환경과 다르면 개선을 증명할 수 없습니다.

2.3 재는 동안 네 번 틀렸습니다

측정 자체를 틀린 게 더 많았습니다.

캐시. 처음엔 페이지를 열어놓고 리로드해서 쟀습니다. 요청이 전부 304로 돌아와 본문이 안 왔습니다. 그 상태 LCP가 2826ms, 콜드로 다시 재니 4505ms. 40% 차이입니다.

집계 경합. Playwright의 request.sizes()가 비동기인데 기다리지 않고 더했습니다. 마지막에 도착한 응답이 빠져서 981KB로 나왔고, 제대로 기다리니 2247KB였습니다. 2.3배 틀렸습니다. 이건 dist 파일 크기와 대조해보고 발견했습니다. step4.png 하나가 410KB인데 이미지 전체가 260KB로 나올 리가 없으니까요.

비교 구간. 처음엔 최적화 시작 직전(06-16)과 PR #534 머지 직후(06-20)를 before/after로 잡았습니다. 그런데 그 사이에 커밋이 267개 있습니다. 팀원 넷이 나흘간 작업한 전부입니다. 이 구간의 LCP −527ms는 진짜지만 제 최적화의 성과는 아닙니다.

커밋 목록. src/features/intro 경로로 필터링해서 perf 커밋을 찾았습니다. index.htmlsrc/features/auth를 고친 23adde19가 그래서 빠졌습니다. 유일하게 유의했던 그 커밋입니다.

넷 다 결과 숫자를 크게 바꾸는 실수인데, 넷 다 "측정했다"고 말한 뒤에도 남아 있을 수 있는 종류입니다.

2.4 얼마나 재야 하나

Node.js core는 성능 주장에 표본 30회, 변동계수 5% 미만, p < 0.05를 요구합니다. doc/contributing/writing-and-running-benchmarks.md에 별표가 없으면 improvement 수치로 결론 내지 말라고 못 박아 놨습니다.

프론트엔드에 그대로 가져올 수 있나 궁금해서 5~7회씩 재고 편차를 봤습니다.

지표 변동계수
전송량·요청 수 0% (매번 완전히 동일)
loadEvent 0.5 ~ 1.9%
LCP 0.9 ~ 2.2%
FCP 1.1 ~ 2.2%

localhost에 프로덕션 번들을 올려놓고 조건을 고정하면 생각보다 안정적입니다. 5회로 CV 2% 안쪽이니 30회까지 갈 필요는 없었습니다. 전송량은 정적 번들이라 아예 변동이 없습니다.

다만 이건 localhost라서 나온 안정성입니다. 실제 배포 환경에서 Lighthouse를 돌리면 CDN, 실제 RTT 분포, 백그라운드 탭까지 얹혀서 훨씬 흔들릴 겁니다. 안정적인 환경을 만들어놓고 재면 적은 표본으로 되지만, 그 대신 실제 사용자 조건과는 멀어집니다. 둘 다 가질 수는 없는 것 같습니다.

2.5 커밋이 어디로 갔는지도 몰랐습니다

측정 지점을 잡다가 알게 된 건데, aea0d1c23d5b91c9main에 안 들어갔습니다. 리베이스 과정에서 없어졌고, 같은 내용이 나중에 다른 해시로 다시 들어갔습니다.

그리고 실제로 머지된 PR #434는 제목이 🎨 style: /intro 랜딩 페이지 최적화 및 디자인·크로스브라우징 개선입니다. 커밋 12개가 들어있는데 perf:는 2개고 나머지는 style과 fix입니다. 성능 작업이 디자인 PR에 섞여 들어갔습니다.

하나 더. 포트폴리오에 "LCP 이미지를 <link rel="preload">로 앞당겼습니다"라고 썼는데, 확인해보니 이 레포 전체 히스토리에 rel="preload"가 한 번도 없습니다. 실제로 한 건 FloatingMockups.tsxfetchPriority="high" 한 줄 붙인 것입니다. 목적은 비슷하지만 다른 기법이고, 면접에서 한 번만 더 물어보면 바로 드러납니다.


3. 알게된 것

측정 전에 예측을 적어두니 틀린 지점이 남았습니다. 재기 전에 예측 파일을 만들고 측정 후에 고치지 않기로 했습니다. 결과는 이렇습니다.

예측 실제
전송량 2.5MB → 800KB 안팎 2247 → 862KB. 맞음
LCP 절반 이하로 개선 2731 → 2203ms, 19% 개선. 틀림
WebP·preload가 전체 개선의 절반 이상 편차 안. 틀림
GPU 최적화는 Lighthouse로 안 잡힐 것 맞음
Lighthouse 5회 편차 ±3점, LCP ±0.3초 LCP 편차 ±0.06초. 과대 예측

제일 자신 없다고 적어둔 항목(GPU 최적화는 안 잡힐 것)이 맞았고, 자신 있게 적은 항목이 틀렸습니다.

"baseline이 없다"는 대부분 틀린 말입니다. 코드는 git이 갖고 있습니다. 진짜로 날아가는 건 측정 조건입니다. 어떤 기기에서 어떤 네트워크로 어떤 도구로 쟀는지, 그게 없으면 나중에 재도 그때 숫자와 비교가 안 됩니다. 저장했어야 할 건 스크린샷이 아니라 조건이었습니다.

커밋 메시지에 숫자를 안 남기면 두 달 뒤엔 없는 것과 같습니다. 제 perf 커밋 다섯 개는 전부 제목 한 줄이고 본문이 비어 있습니다. 그래서 포트폴리오를 쓸 때 "미확보"라고 적을 수밖에 없었고, 지원서 여러 곳에 그대로 실려 나갔습니다. 재는 것보다 재고 나서 어디에 적느냐가 더 문제였습니다.

개인 목표에 비춰서. 1주차에 "쓰레기 = 내가 검증하지 않고 통과시킨 산출물"이라고 정의했는데, 이번 건 검증을 안 한 게 아니라 검증했다고 착각한 경우에 가깝습니다. 눈으로 보고 빨라졌다고 판단했고, 그 판단을 그대로 이력서에 옮겼습니다. 다섯 개 중 넷은 지금 기준으로 근거가 없습니다.

한 가지 덧붙이면, 근거가 없다는 게 잘못한 작업이라는 뜻은 아닙니다. 686f1315은 실제로 초당 120회 도는 스타일 재계산을 없앴습니다. 다만 그걸 제가 아는 지표로는 증명할 수 없었고, 증명할 수 있는 지표를 이번에 처음 찾았습니다.

앞으로. 포트폴리오의 해당 문단을 고칠 생각입니다. rel="preload"는 사실이 아니니 빼고, 편차 안이었던 항목은 성과로 쓰지 않고, 23adde19를 실제 성과로 올립니다. 이번 측정 조건과 raw 값도 같이 붙일 예정입니다.

아직 모르는 것

  • 저사양 안드로이드에서 686f1315의 체감 차이가 실제로 있는지. 지금은 잴 방법이 없습니다.
  • localhost preview와 실제 배포(CDN·압축·HTTP/2) 사이의 차이가 어느 정도인지.
  • Pretendard dynamic-subset이 13개 블록 333KB를 받고 있는데, 여기는 아직 안 건드렸습니다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

documentationImprovements or additions to documentation

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions