[Next.js] 렌더링 성능 개선하기 (2)

bongbong·2024년 7월 7일
2

Next.js

목록 보기
6/6
post-thumbnail

이전 편에서는 렌더링 방식을 변경하고 시각적으로 보이는 이슈들부터 개선하였는데요.
최적화가 필요한 부분은 크게 2가지가 더 있었습니다.

문제점

TBT 지표를 높이는 Blocking 요소들

TBT(Total Blocking Time)는 FCP(First Contentful Paint)와 TTI(Time to Interactive) 사이에서 50ms 이상이 걸리는 긴 작업들을 blocking time으로 간주해 더한 지표입니다. 즉, 사용자가 처음 페이지 콘텐츠를 본 후 마우스 클릭, 터치, 키보드 입력 등이 가능해지기까지의 시간을 측정한 값으로, Lighthouse에서 TBT에 가장 큰 가중치를 두고 있어 매우 중요한 지표 중 하나입니다.

해당 지표가 500ms 이상 나오면서 어떤 부분에서 blocking 되고 있는지 분석이 필요하였습니다.

느린 초기 페이지 렌더링

SSR(Server-Side Rendering)로 전환하면서 초기 HTML을 요청하고 받아오는 시간이 길어졌고, 네트워크의 서버 응답 대기 시간(network waiting for server response)이 많을 때는 7초까지 걸리는 문제가 있었습니다. 이로 인해 처음 페이지를 보는 것조차 오랜 시간이 걸렸습니다. 따라서 SSR 로직 최적화가 필요했습니다.

해결 방법

bundle-analyzer를 사용한 번들 최적화

lightHouse에서는 TBT 지표를 향상시키기 위해서 Javascript 번들의 code splitting, 사용하지 않는 코드 제거, 효율적인 third-party script loading을 제안하고 있습니다.

next/bundle-analyzer를 사용해서 js의 초기 번들 파일의 상태를 분석하였고 next.js의 dynamic import를 사용해서 code split 및 lazy load를 적용하였습니다.

공통 번들 파일 최적화

First Load JS는 페이지가 처음 로드될 때 모든 페이지가 공통으로 로드하는 번들인데 _app.js의 사이즈가 378kB로 매우 큽니다.

+ First Load JS shared by all               464 kB
  ├ chunks/framework-ce84985cd166733a.js    45.2 kB
  ├ chunks/main-e191f7fcdcea027b.js         35.7 kB
  ├ chunks/pages/_app-38596a77fcf1fc39.js   378 kB
  ├ chunks/webpack-8b927309e233a998.js      2.56 kB
  └ css/202272ed9e27e959.css                2.47 kB

_app 파일의 내부를 들여다보면 lottie.js와 sentry가 큰 부분을 차지하고 있었습니다.

sentry 하나가 288kB로 매우 거대한 사이즈를 차지하고 있었고 추가 기능(integrations)으로 사용했던 reply와 breadbreaums를 제거해도 169kB였습니다. sentry가 사용되는 페이지는 초기 번들의 50% 이상을 sentry가 차지하고 있었습니다.

sentry issue에 해결방법을 찾아보았으나 lazy load, tree shaking을 지원하고 있지 않아 개발자들의 민심이 매우 좋지 않은 상황...

https://github.com/getsentry/sentry-javascript/issues/7680
https://github.com/getsentry/sentry-javascript/issues/2707

그래서 현재 회사 이슈로 웹프로젝트에는 sentry를 꼭 사용하지 않아도 되는 상황이었고 굳이 방대한 패키지로 성능에 지장을 줄 필요가 없다고 판단하여 잠시 sentry/nextjs를 내려두었습니다. 추후 에러 트래킹을 위해 성능감소를 안고 sentry를 사용해야하는지 다른 대체방안이 있을지 고민해봐야할 부분이었습니다.

그리고 react-lottie-player는 298kB로 거대한 사이즈를 보여주고 있었는데요. lottie-light 버전을 tree shaking으로 지원하는 lottie-web으로 대체했고 163kB 줄여 약 45% 감소시킬 수 있었습니다.

페이지 번들 최적화


Filter to intial chunks에 원하는 페이지를 필터해서 초기 번들에 어떤 파일들이 포함되는지 확인할 수 있습니다. 이를 통해 초기에 필요하지 않은 컴포넌트나 라이브러리를 dynamic import를 적용하였습니다.

152kB의 ProfileOverView.tsx 내부에는 많은 컴포넌트들이 존재하는데 탭 전환을 통해 보여지는 영역들이 많아 첫번째 탭을 제외한 컴포넌트에 lazy load를 적용하기 적합하였습니다. 152kB -> 13kB로 감소

lodash는 debounce 메서드만 사용하고 있었기 때문에 cherry-picking을 통해 처리하였습니다.

third party script 비동기 로드

그리고 GA, GTM, Google ADS, 영상 광고 SDK 등 많은 thrid party script가 사용되고 있었기 때문에 defer를 추가하여 렌더링이 blocking되지 않도록 수정하였습니다.

SSR 최적화

Warning: data for page "/profile/[platform]/[path]" (path "...") is 502 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance.

해당 페이지에서는 일정 부분을 SSR로 서빙하기 위해 getServerSideProps 내에서 6개의 API를 호출하고 있습니다. promise.all을 사용해서 병렬적으로 처리되도록 개선했고, 사이즈가 큰 API에서는 필요한 데이터만 return 하도록 수정하였습니다.

이미지 최적화

index 페이지에는 lottie 및 큰 이미지들이 많이 사용되고 있었고 cdn 및 lazy load를 처리하여 최적화 하였습니다.

결과

lightHouse Desktop Performance 측정

lightHouse Mobile Performance 측정

SEO 개선을 위해 mobile performance 개선도 함께 진행했는데 점수 올리기가 힘들었습니다. 찾아보니 lightHouse는 모바일 성능 측정시 3G 환경으로 세팅해버리고 Desktop 환경보다 CPU, 네트워크 속도를 4배 느리게 throttling하여 측정하고 있어서 높은 점수를 얻기 힘든 것이었습니다.

해외 SEO 및 마케팅 회사의 블로그를 찾아보니 SEO로 유명한 웹사이트도 60대 중반대를 기록하고 있기에 일반적으로 40~50점대를 기록하는것도 굉장한 개선이라고 말해주고 있습니다.
https://www.seocomponent.com/blog/why-lighthouse-mobile-score-is-bad/

번들 사이즈

  • index page : 4.68MB -> 289kB (약 94% 감소)
  • profile page : 717kB -> 313kB (약 56% 감소)
  • First Load JS : 464kB -> 232kB (약 50% 감소)
// 개선 전
┌ ○ / (359 ms)                              4.13 MB        4.68 MB
├ λ /profile/[platform]/[path]              2.42 kB         717 kB
...

+ First Load JS shared by all               464 kB
  ├ chunks/framework-ce84985cd166733a.js    45.2 kB
  ├ chunks/main-e191f7fcdcea027b.js         35.7 kB
  ├ chunks/pages/_app-38596a77fcf1fc39.js   378 kB
  ├ chunks/webpack-8b927309e233a998.js      2.56 kB

// 개선 후
┌ ● / (ISR: 600 Seconds) (435 ms)           20 kB           289 kB
├ λ /profile/[platform]/[path]              11.1 kB         313 kB
...

+ First Load JS shared by all               232 kB
  ├ chunks/framework-ce84985cd166733a.js    45.2 kB
  ├ chunks/main-e191f7fcdcea027b.js         35.7 kB
  ├ chunks/pages/_app-49bf831d0475e453.js   147 kB
  └ chunks/webpack-c771521b231214a1.js      3.76 kB

성능개선을 하면서 점수가 올라갈수록 빨라지는 페이지 전환과 깔끔한 렌더링을 보면서 뿌듯했고 재밌었습니다. 블로그로 정리하다보니 좀더 개선할 수 있는 부분이 있는데 놓친것 같아 출근하면 좀더 개선을 해봐야겠습니다.
불필요한 번들을 제거하는 것도 중요하지만 SSR로 많은 로직을 처리하면서 코드의 속도도 중요하다는걸 깨달았는데요. 코테 어려워서 손놓고 있었는데 다시 시작해야겠다는 생각이 듭니다.
앞으로는 주기적으로 성능테스트를 하며 관리를 해야겠습니다.

마지막으로 틀린 부분이 있거나 궁금한 것이 있다면 편하게 코멘트 부탁드려요!

0개의 댓글