본문으로 건너뛰기

프론트엔드의 성능 최적화

· 약 14분
손해영 (바다)
리뷰미 FE

성능 개선 이유

성능은 고객 유치와 SEO에 밀접한 관계가 있습니다. 사이트 속도가 빨라지면 사용자 경험(UX)가 개선되며, 검색 엔진의 순위에도 긍정적인 영향을 줍니다.

성능 개선에 따른 효과

  • Tokopedia: 3G 연결에서 렌더링 시간을 14초에서 2초로 줄여 방문자 19% 증가
  • Pinterest: 페이지 성능을 개선해 대기 시간을 40% 줄이고 SEO 트래픽과 전환율이 각각 15% 증가

성능 측정 방식

성능 측정 환경

  • 측정 컴퓨터: 바다 (윈도우 11)
  • 측정 대상 기기 : pc
  • 측정 대상 모드 : dev 배포 페이지
  • 측정 브라우저 : 크롬 브라우저 (시크릿 모드)
  • 측정 전 : 브라우저 캐시를 지우고 시작한다.

성능 측정 대상 및 예산

성능 측정 대상

리뷰미 서비스의 핵심 기능인 리뷰 작성이 있는 리뷰 작성 페이지를 우선으로 성능 개선(리소스 경량화, 애니메이션 최적화)을 하고, 그외의 페이지는 리소스를 경량화하는 작업을 진행했습니다.

항목리뷰 작성 페이지홈 페이지리뷰 연결 페이지그 외 페이지
JS 최적화OOOO
CSS 최적화OOOO
이미지 최적화OOOO
캐러셀 최적화OOxx
Lighthouse 측정 대상OOOx

성능 예산

  • 구글의 RAIL 모델 가이드 라인 참고
  • 지표 (프론트 개인 미션 성능 개선 항목을 우선 따른다)
    • 소스 파일 용량 : 정량 기반 지표 사용
    • LCP ,FCP : 수치 기반 지표 - 빠름 기준 (LCP: 2.5s, FCP:1.8s)

성능 측정 도구

  • Lighthouse : 전반적인 성능 측정
  • Chrome devtools: 네트워크,CPU 성능에 따른 Layout shift, Frame drop 측정
  • Webpack bundle analyzer : 번들링된 리소스 크기 확인
  • WebPageTest : 서울 빠른 3G에서 두 번 요청 시 LCP 측정

성능 최적화

캐시 사용

CloudFront에서 Managed-CachingOptimized캐시 정책을 사용하고 있습니다. Managed-CachingOptimized 캐시 정책을 사용하면 CloudFront는 기본 TTL(하루)을 설정하고 브라우저가 캐시를 받을 수 있도록 합니다. 브라우저는 CloudFront에서 설정된 캐시 정책에 따라 파일을 캐시하며, 기본적으로 하루 동안 캐시된 콘텐츠를 사용합니다. 캐시 정책관련 프론트 회의에서 현재 변경 사항이 많은 단계이기 때문에, 캐시 시간을 더 늘이기 보다는 현재 사용하고 있는 CloudFront에서 제공하는 기본 TTL 시간인 하루를 유지하기로 했습니다. 그래서 추가적인 캐시 작업은 진행하지 않았습니다.

CDN캐시 적용된 모습

JS 최적화

CSS-in-JS인 emotion을 사용 중이라 CSS 최적화를 진행하지 않습니다. 스타일 코드도 JS이기 때문에, 최대한 JS를 압축하고 필요한 리소스만 부르는 방향으로 최적화를 진행했습니다.

1차 JS 최적화 시도

Webpack 의 production 모드로 빌드 시, Webpack에서 JS 최적화를 해줍니다. 이를 기본적으로 사용하되 추가적인 최적화를 위해 아래의 것들을 시도했습니다.

  • TerserPlugin 설정을 통한 불필요한 콘솔, 디버깅 등 코드 삭제
  • 동적 import와 chunk: 'all'옵션을 사용한 Code Splitting

그 결과 작성 페이지에서 부르는 JS 리소스 크기가 629KB에서 167KB로 줄었습니다.

2차 JS 최적화:Brotli 압축

1차 최적화 시도에서는 CloudFront에서 자동으로 제공하는 모듈 압축 방식을 사용했습니다. CloudFront에서 클라이언트 Accept-Encoding 헤더에 따라 압축을 하는 것보다, S3에 압축된 파일을 올리면 리소스 로딩 시 압축 시간이 제외되니 로딩 속도가 더 빠릅니다.

Gzip이 아닌 Brotli를 사용한 이유는 Brotli는 Gzip에 비해 압축률이 20-25%더 높고 압축해제도 더 빠르며, 리뷰미 서비스 대상 브라우저에서 다 지원하고 있습니다. 그렇지만 혹시 있을 수 있는 Brotli 미지원 브라우저 사용자를 위해, 압축하지 않은 리소스도 S3에 올려서 사용자의 브라우저의 요청에 따라 압축된 리소스를 로드할 수 있도록 했습니다.

Brotli로 인한 개선 사항

  • 작성 페이지에서 JS 리소스 크기 107KB로 감소
  • Brotli 압축 시 터미널 창

    Brotli 압축 후 빌드 파일 용량 비교

작성 페이지 기준 JS 최적화 전 후 비교 (네트워크 No throttling, 캐시 사용 중지)

  • JS 리소스 크기: 629KB ->107KB
  • DOMContentLoaded: 1.60s -> 419ms
  • Load: 1.65s -> 1.16s

이미지 최적화

SVG를 사용한 이유

리뷰미 프론트 팀은 SVG 이미지 포맷만을 사용하고 있습니다. SVG는 다양한 화면 크기에서도 동일한 품질을 지원하기 때문에, 웹과 모바일 브라우저 모두 지원하는 리뷰미 서비스에 적합한 포맷이라고 생각합니다.

SVG 최적화

svgo 플러그인을 사용해, 빌드 시 SVG 최적화를 진행했습니다. 최적화를 진행한 결과 상위 SVG 5개 파일 크기 합이 2367KB에서 1736KB로 줄었습니다.

svgo로 최적화를 진행한 후, Brotli로 압축하는 과정을 거쳐 한 번 더 SVG 파일 크기를 줄였습니다.

홈 페이지 기준 이미지 최적화 전 후 비교(1024px, 네트워크 No throttling, 캐시 사용 중지)

  • 리소스 크기: 8.7MB -> 1.4MB
  • Finish: 6.47s -> 5.95s
  • DOMContentLoaded: 1.79s -> 209ms
  • Load: 2.30s -> 727ms

폰트 최적화

리뷰미에서 프리텐다드를 메인 폰트로 사용하고 서브 폰트로 구글 폰트인 Noto Sans를 사용하고 있습니다.

구글 폰트에는 preload를 사용해 해당 폰트에 대한 로딩 속도를 개선했습니다. 프리텐다드 폰트에 preload와 가변 다이나믹 서브셋(Variable Dynamic Subsetting)을 사용해 최적화를 진행했습니다.

가변 다이나믹 서브셋을 사용한 이유

다이나믹 서브셋과 가변 다이나믹 서브셋?

  • 다이나믹 서브셋

    • 웹 페이지에서 사용되는 글리프만 포함된 폰트 파일을 동적으로 생성하여 용량을 줄이는 방식입니다.
  • 가변 다이나믹 서브셋

    • 다이나믹 서브셋과 가변 폰트를 결합하여, 하나의 폰트 파일에 다양한 스타일(굵기, 너비 등)을 포함하면서 필요한 글리프만 동적으로 불러오는 방식입니다.

리뷰미 페이지에서 다양한 굵기의 프리텐다드 폰트를 사용하고 있어서, preload를 사용할 수 있으면서도 다양한 굵기의 폰트를 더 가벼운 용량으로 부르는 가변 다이나믹 서브셋을 사용하기로 했습니다.

preload가 적용된 가변 다이나믹 서브셋 적용으로 인한 최적화

개선 항목개선 전개선 후
폰트 총 리소스 용량2.4 MB209 KB
DOMContentLoaded1.60s216ms
Load1.60s744ms
Waterfall에서 로딩 속도

프리덴다드 가변 다이나믹 서브셋 적용 전

프리덴다드 가변 다이나믹 서브셋 적용 후

캐러셀 애니메이션 최적화 및 불필요한 렌더링 개선

개선 전 네트워크, CPU에 따른 Frame drop, Layout shift

캐러셀네트워크:제한 없음 , CPU: 제한 없음네트워크: 3G ,CPU : X 6(감속)
작성 페이지 캐러셀 Frame drop있음있음 (체감상으로도 버벅임이 느껴짐)
작성 페이지 캐러셀 Layout shift없음없음
홈 페이지 캐러셀 Frame drop캐러셀의 카드가 이동할때 마다 Frame drop 일어남캐러셀의 카드가 이동할때 마다 Frame drop 일어남
홈 페이지 캐러셀 Layout shift없음없음

애니메이션 최적화 속성 사용

window.requestAnimationFrame, will-change, translate3d를 사용해 홈 페이지와 리뷰 작성 페이지에서 사용하는 캐러셀에 대한 애니메이션 최적화를 진행했습니다.

GPU는 CPU와 달리 그랙픽에 특화되어 있으며 여러 작업을 병렬처리해, 애니메이션 시 GPU를 사용하는 것이 좋습니다. translate3d는 GPU 가속을 사용하고 will-change는 브라우저에게 특정 요소에서 곧 발생할 가능성이 있는 변화를 미리 알려줌으로써, 해당 변화를 최적화하도록 도움을 줍니다.

window.requestAnimationFrame 브라우저의 화면 업데이트 주기(초당 60번)에 맞춰 콜백 함수를 실행해 애니메이션이 부드럽게 보이도록 합니다. 캐러셀을 작동하는 콜백 함수는 window.requestAnimationFrame에 넣어, 보다 부드럽게 캐러셀 속 카드나 이미지들이 넘어가도록 했습니다.

메모이제이션 사용

리뷰 작성 페이지에서 사용자가 이전/다음 버튼이나 프로그레스 바를 통해 이전에 렌더링한 질문지를 다시 부를 수 있습니다. 그래서 React.memo를 사용해 이미 한번 렌더링한 질문지를 새로 렌더링하지 않도록 했습니다.

작성 페이지(네트워크: 3G ,CPU : X 6(감속)) 캐러셀 Frame Drop 개선 전 후 비교

Frame개선 전개선 후
이미지

캐러셀 개선 전

캐러셀 개선 전

성능 최적화 개선 전 후 비교

홈 페이지

페이지Lighthouse 성능 점수LCPFCP서울 빠른 3G(webpage test) 에서 두 번 요청 시 LCP
개선 전823.3s0.6s7.215s
개선 후981.0s0.6s1.84s

리뷰 작성 페이지

페이지Lighthouse 성능 점수LCPFCP
개선 전733.3s1.7s
개선 후951.3s0.7s

리뷰 연결 페이지

페이지Lighthouse 성능 점수LCPFCP
개선 전852.7s0.7s
개선 후961.4s0.5s