LCP 개선 - boostcampwm-2022/web33-Mildo GitHub Wiki

Mildo의 초기 성능(배포 환경)

  • chrome 개발자 도구의 lighthouse를 활용하여 Mildo의 성능을 측정한 결과 테스크톱 환경에서는 88점, 모바일 환경에서는 58점인 것으로 나타났다.
    • lighthouse 성능 점수가 실제 성능을 보장한다고 볼 수는 없으나, 일부 팀원이 실제로 모바일 환경에서 성능 저하를 경험했다.
  • 그래서 모바일 환경 기준 15.4초로 가장 성능이 가장 낮은 Largest Contentful Paint 항목을 집중적으로 개선하기로 하였다.

데스크톱 환경

데스크톱 환경

모바일 환경

모바일 환경

코드 스플리팅으로 bundle.js 사이즈 개선(개발 환경)

  • 우선 LCP의 세부 내역을 확인한 결과 Reduce unused JavaScript의 시간이 5.85초로 매우 오래 걸렸는데, 가장 큰 요인은 cra로 build한 bundle.js 의 크기가 무려 835.1KiB였다.

사용하지 않는 자바스크립트 줄이기.PNG.png

  • 이렇게 bundle.js가 비정상적으로 큰 이유 중 하나가 외부 라이브러리이다. 우리는 로그인 모달24시간 인구 그래프를 구현하며 각각 react-icons와 apexCharts를 사용했는데, 이것의 용량이 매우 컸던 것이다.

사용하지 않는 자바스크립트 줄이기.PNG.png

  • 그래서 React에서 제공하는 lazy를 활용하여, 크기가 큰 로그인 모달24시간 인구 그래프를 스플리팅하였다.

    • loadable 등 코드 스플리팅 외부 라이브러리를 사용하지 않고 React lazy를 활용한 이유는
      1. 컴포넌트를 쉽게 동적 import 할 수 있으며,
      2. 이미 외부 라이브러리로 bundle.js 크기가 매우 크기 때문이다.
    // client/src/components/infoDetailModal/infoDetailModal.tsx
    ...
    const SecondLevelComponent = lazy(
      () => import('../SecondLevelComponent/SecondLevelComponent')
    );
    ...
    return (
    ...
    {isSecondLevel && (<SecondLevelComponent ... />)}
    ...
    );
    
  • SecondLevelComponent 가 렌더될 때 네트워크 탭에서 별도의 build 파일을 받아오는 것을 확인할 수 있다.

캡처.PNG

  • 또한, lazy로 코드 스플리팅을 진행한 결과 Reduce unused JavaScript의 시간이 5.85초에서 4.11초1.74초 감소하였고, bundle.js의 용량이 835.1KiB에서 465.9KiB369.2KiB 감소하였다.

최종.PNG.png

  • 하지만 처음엔 더 오래 걸렸다.

    • 코드 스플리팅 작업 직후에 스플리팅된 파일을 바로 받아와 오히려 Reduce unused JavaScript의 시간이 훨씬 더 오래 걸렸다.

    캡처.PNG.png

    • 이는 우리가 최초 코드에서 로그인 모달을 제한 조건 없이 바로 가져왔기 때문이다. 아래와 같이 제한 조건을 설정한 후 성능이 바로 개선되었다.

      
      ...
      const LoginModal = lazy(() => import('../../components/LoginModal/LoginModal'));
      ...
      const SecondLevelComponent = lazy(
        () => import('../SecondLevelComponent/SecondLevelComponent')
      );
      return (
          <StyledMainPage>
            <Map
              latitude={coordinates!.latitude}
              longitude={coordinates!.longitude}
            />
            <SearchBarAndMyBtn />
            <DensityFilterList />
            <InfoDetailModal />
            {isLoginModalOpen && <LoginModal />}
            <MyInfoSideBar />
          </StyledMainPage>
        );
      ...
      
  • 코드 스플리팅이 무조건 시간을 줄여주는 것은 아니다.

    • 혹시 몰라 크기가 작은 모달들도 동적 import를 해봤는데, 오히려 bundle.js의 크기와 Reduce unused JavaScript의 시간만 살짝 늘어났다. 오히려 작은 것들은 하나의 파일로 받아오는 것이 훨씬 효율적이라는 것을 알 수 있었다.
  • 그러나 여전히 bundle.js의 크기가 너무 컸다. 가장 많은 용량을 차지하는 모듈은 apexCharts였다.

cra-bundle-analyzer로 분석한 bundle.js의 크기 구성

cra-bundle-analyzer로 분석한 bundle.js의 크기 구성

  • 이를 위한 해결 방법으로는 apexCharts 라이브러리를 사용하지 않거나, 다른 build 방법을 모색하는 것이 있다.

CRA에서 vite로 번들러 마이그레이션(개발 환경)

  • 24시간 인구 그래프가 apexCharts를 매우 의존하고 있어 대체하기가 어려웠다.

  • 그래서 build 도구를 vite로 변경하기로 결정하였다. 이유는 cra보다 bundle.js 파일의 크기를 크게 줄여줄 것이라고 기대했고, cra로 생성한 프로젝트도 vite로 쉽게 migration 할 수 있을 것으로 판단했기 때문이다.

  • vite.config.js 파일 생성을 위해 기본 라이브러리들을 설치하였고, 기본 환경을 세팅하였다.

    // client/vite.config.ts
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    import viteCompression from 'vite-plugin-compression';
    
    export default defineConfig({
      plugins: [react(), viteCompression()],
      server: {
        open: true,
        port: 3000
      }
    });
    
  • vite 적용 후 cra 대비 Reduce unused JavaScript의 시간이 4.11초에서 3.6초0.51초 감소하였고, 빌드된 번들 파일의 크기는 465.9KiB에서 104.2KiB361.7KiB 감소하였다.

바이트 배포.PNG.png

  • 최종적으로 MainPage도 스플리팅을 진행하였고, 개발에 사용되는 chrome의 확장 프로그램을 중지 시켜 Reduce unused JavaScript0.3초로, 번들 파일65.6KiB로 감소하였다.

진짜 최종.PNG.png

  • 그러나 여전히 LCP의 다른 부분에서 시간 지연이 발생하는 것으로 나타난다.

    진짜 최종2.PNG

  • 사실 몇 가지 어려움이 있었다

    • vite 적용 후 vscode가 네이버 maps api에서 제공하는 namespace naver를 인식하지 못했는데, vite 설정 과정에서 tsconfingtypes 옵션을 함부로 변경해서 그런 것이었다.

      tsconfig 옵션을 다시 되돌려 해결했다.

    • vite 적용 직후에는 LCP 시간이 비정상적으로 증가하였다. 알고 보니 vite의 개발 환경과 배포 환경의 차이었고, build 후 실행하여 배포 환경과 유사하게 조성하니 정상적으로 시간이 감소하였다.

      바이트 처음.PNG

기타 개선 사항(개발 환경)

  • Preload Largest Contentful Paint Image와 Eliminate render-blocking resources, Enable text compression을 중점적으로 개선하고자 했다.

Preload Largest Contentful Paint Image

  • Mildo에서 Preload Largest Contentful Paint Image가 오래 걸리는 핵심적인 이유는 네이버 maps api를 통해 지도 이미지를 매우 많이 불러오기 때문이다.

    가장 큰 문제는 네이버 지도.PNG

  • 모바일 환경에서도 10장이 넘는 사진을 한 번에 받아온다.

    이미지.PNG

  • Preload Largest Contentful Paint Image의 시간을 줄이는 방법 중 Mildo의 상황에 가장 적합한 것은 브라우저가 이미지의 크기를 미리 정할 수 있게 각 이미지 태그에 preload 설정을 걸어주는 것이다.

  • 그러나 네이버 maps api의 지도 이미지를 직접 컨트롤할 방법이 없어 근본적인 문제해결이 불가능했다.

  • 그래서 최대한 지도 이미지를 적게 불러오고자 네이버 지도의 기본 줌을 14에서 16으로 축소하였고, Preload Largest Contentful Paint Image 시간이 3.13초에서 2.17초0.96초 감소하였다.

기본 줌 줄임.PNG

Eliminate render-blocking resources

  • index.html에서 네이버 maps api script 태그가 다른 DOM의 렌더링을 차단하여 시간이 지연되어 발생하는 시간이다.

  • 이에 해당 script 태그defer 속성을 추가하였고, 이 script 태그와 상관없이 페이지 내 다른 DOM이 생성되게 만들어 Eliminate render-blocing resources 문제를 완전히 해결하였다.

    async 더한 이후.PNG

Enable text compression(배포 환경)

  • 실제 배포 환경에서 lighthouse 성능을 다시 측정하였는데, Enable text compression라는 새로운 문제와 함께 번들 파일의 크기가 307.1KiB로 커진 문제가 발생하였다.

    1.PNG

  • 이에 번들 파일의 Response Headers를 확인하니 Content-Encoding 헤더가 비어있었고, 번들 파일이 제대로 압축되지 않았다는 것을 알 수 있었다.

    2.PNG

  • 그래서 번들 파일을 제공하는 nginx 서버에서 압축을 위해 gzip 설정을 사용하였다.

    • /etc/nginx/nginx.config

    캡처.PNG

  • 번들 파일의 Response Headers에서 Content-Encoding 헤더의 gzip이 제대로 설정된 것을 확인할 수 있다.

    해결2.PNG

  • 결과적으로 압축을 통해 Enable text compression 문제를 해결하였고, 번들 파일의 크기가 정상적으로 돌아왔다.

    • 이 과정에서 MainPage를 다시 번들 파일로 합쳐 용량이 살짝 증가했다.

    해결1.PNG

결과(배포 환경)

  • 최종적으로 배포 환경에서 데스크톱 환경의 점수는 98점으로 10점 증가했고, 모바일 환경의 점수는 76점으로 18점 증가했다.
  • 특히, 모바일 환경에서 Largest Contentful Paint 시간이 6.6초로 이전 대비 8.8초나 감소하였고, 다른 세부 항목들도 함께 개선된 것을 확인할 수 있었다.

데스크톱 환경.PNG

모바일 환경1.PNG

  • 다만, 여전히 Preload Largest Contentful Paint Image로 인한 지연이 발생하고 있어 추후에 개선이 필요해 보인다.

  • 그래도 코드 스플리팅과 vite를 통해 처음에 의도했던 바와 같이 Reduce unused JavaScript의 시간을 매우 크게 줄였고, 성능 개선에도 유의미한 성과가 있었다.

    모바일 환경2.PNG