[TIL] (React + Vite)LightHouse 성능 개선하기(최적화)

Leesu·2025년 4월 26일
0

[TIL] : Today I Learned

목록 보기
22/25

정신없이 프로젝트를 마무리 한 뒤.... 두번째 배포 즈음,
라이트하우스로 성능을 체크했는데 너무나도 충격적인 결과에 무릎을 꿇고 말았다..

🟡 개선하기

1. 콘텐츠가 포함된 최대 페인트 요소

1) Stack 이나 <Box /> 같은 컴포넌트에 sx={{ ... }} 스타일링 바로 추가했던 방식을 변경했다.
상수와 스타일을 정의하여 렌더링을 최적화 할 수 있도록 수정했고
CI 로고 떄문에 이미지가 몇 장 있었는데, webp 확장자로 바꿔줬다.

나는 이미지가 세 개 밖에 되지 않아서 그냥 바꾼걸로 등록했지만
이미지가 많다면 vite-plugin-imagemin 라이브러리를 사용하거나,
또는 vite-plugin-image-optimizer 를 사용하면된다!

2. 기본 스레드 작업 최소화하기

1) 레이지 로딩, 특정 페이지에서는 사용하지 않았는데 협의 후 추가하기로해서 전부다 적용 완.

리액트의 레이지 로딩(Lazy Loading)이란 웹 애플리케이션의 초기 로딩 시간을 개선하기 위해 사용되는 기술입니다. 이 기술은 필요한 시점까지 특정 컴포넌트나 리소스의 로딩을 지연시키는 방식으로 작동합니다.
React.lazy() 이 함수를 사용하여 동적으로 컴포넌트를 임포트할 수 있습니다.
Suspense 레이지 로딩된 컴포넌트가 로드되는 동안 로딩 상태를 표시할 수 있는 컴포넌트입니다.

3. 텍스트 압축 사용

1) 웹 서버에서 텍스트 기반 리소스(HTML, CSS, JavaScript 등)를 압축하여 제공함으로써 네트워크 사용량을 줄일 수 있다고 한다.

gzip : 가장 널리 사용되는 웹 콘텐츠 압축 방식 중 하나로, 텍스트 파일을 효과적으로 압축합니다.
deflate : gzip과 비슷한 압축 알고리즘으로, 일부 오래된 브라우저에서도 지원됩니다.
Brotli : Google이 개발한 비교적 새로운 압축 알고리즘으로, gzip보다 더 효율적인 압축률을 제공합니다.

4. 자바스크립트 줄이기

1) vite-plugin-compression 라이브러리를 사용하면 된다!

   plugins: [
      react(),
      viteTsconfigPaths(),
      svgrPlugin(),
      legacy({
        targets: ["edge >= 87", "chrome >= 90"],
        modernPolyfills: true,
      }),
      viteCompression({
        algorithm: "gzip",
        ext: ".gz",
      }),
      // Brotli 압축 추가
      viteCompression({
        algorithm: "brotliCompress",
        ext: ".br",
      }),
    ],

나는 두 압축 알고리즘(gzip과 brotli)을 동시에 사용했다.
이렇게 설정하면 Vite는 빌드 시 두 가지 버전의 압축 파일을 모두 생성하게 되는데,

.gz 확장자를 가진 gzip 압축 파일
.br 확장자를 가진 brotli 압축 파일

이런 설정의 장점은:
더 넓은 브라우저 호환성: 모든 현대 브라우저가 gzip을 지원하므로 기본적인 호환성 보장
최적의 압축률: brotli는 일반적으로 gzip보다 15-25% 더 나은 압축률을 제공
최적화된 전송: 서버는 클라이언트가 지원하는 최적의 압축 형식을 선택할 수 있음

실제로 이 설정이 적용되면 웹 서버는 클라이언트의 Accept-Encoding 헤더를 확인해서,

brotli를 지원하는 브라우저에는 .br 파일을,
gzip만 지원하는 브라우저에는 .gz 파일을,
둘 다 지원하지 않는 경우 원본 파일을 제공한다고 한다!

둘 다 지원하지 않는 경우는 아주아주아주아주아주 드물지만.. 금융권쪽에는 드물게 있을 수 있을지도?.. 그리고 이런 점 때문에 둘 다 사용하는 것도 있다 ㅎ

5. 중복 모듈 삭제

1) build > rollupOptions > manualChunks 에서 스플리팅 해주고, optimizeDeps 에서 특정 라이브러리를 단일 인스턴스만 사용가능하게 설정.

   build: {
      minify: "esbuild",
      cssMinify: true,
      cssCodeSplit: true,
      commonjsOptions: { transformMixedEsModules: true },
      chunkSizeWarningLimit: 1000,
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes("@mui/icons-material")) {
              return "mui-icons";
            } else if (id.includes("@mui")) {
              return "mui-core"; 
            }
            else if (id.includes("toast-ui")) {
              return "toast-ui-legacy";
            } else if (id.includes("node_modules")) {
              return id
                .toString()
                .split("node_modules/")[1]
                .split("/")[0]
                .toString();
            }
          },
        },
        treeshake: {
          moduleSideEffects: false,
        },
      },
    },

    optimizeDeps: {
      exclude: ["@toast-ui/grid", "@toast-ui/chart", "@toast-ui/react-grid"],
      dedupe: ["react", "react-dom", "react-is", "@mui/utils"],
    },

      

이렇게 하고있는데, 그래도 mui core 및 toast-ui 에서 큰 용량으로 줄여지고있어서, 좀 더 고민해봐야할 것 같다...

🟡 1차 개선 완료 결과

최종 vite.config.js

import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import viteTsconfigPaths from "vite-tsconfig-paths";
import svgrPlugin from "vite-plugin-svgr";
import legacy from "@vitejs/plugin-legacy";
import path from "path";
import viteCompression from "vite-plugin-compression";

export default defineConfig((command, mode) => {
  const env = loadEnv(mode, process.cwd(), "");

  return {
    base: `${env.VITE_BASE_URL}`,
    experimental: {
      renderBuiltUrl() {
        return { relative: true };
      },
    },

    plugins: [
      react(),
      viteTsconfigPaths(),
      svgrPlugin(),
      legacy({
        targets: ["edge >= 87", "chrome >= 90"],
        modernPolyfills: true,
      }),
      viteCompression({
        algorithm: "gzip",
        ext: ".gz",
      }),
      viteCompression({
        algorithm: "brotliCompress",
        ext: ".br",
      }),
    ],
    build: {
      minify: "esbuild",
      cssMinify: true,
      cssCodeSplit: true,
      commonjsOptions: { transformMixedEsModules: true },
      chunkSizeWarningLimit: 1000,
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes("@mui/icons-material")) {
              return "mui-icons";
            } else if (id.includes("@mui")) {
              return "mui-core"; 
            }
            else if (id.includes("toast-ui")) {
              return "toast-ui-legacy";
            } else if (id.includes("node_modules")) {
              return id
                .toString()
                .split("node_modules/")[1]
                .split("/")[0]
                .toString();
            }
          },
        },
      },
    },

    optimizeDeps: {
      exclude: ["@toast-ui/grid", "@toast-ui/chart", "@toast-ui/react-grid"],
      dedupe: ["react", "react-dom", "react-is", "@mui/utils"],
    },

    resolve: {
      alias: {
        "@toast-ui": path.resolve(__dirname, "node_modules/@toast-ui"),
      },
    },
  };
});

36점에서.. 73점이라니 정말..!

사용하지 않는 자바스크립트 줄이기는 개선을 좀 더고민해봐야 하는게,
지금 한 레퍼지토리에서 다른 서비스가 두개가 올라가 있는데,
내가 사용하진 않아도... 다른 팀원이 사용하는 것이 있어서.... 시간이 되실때 이야기해서 개선해나가기로 했다.

목표는 최~소한 85점이였기 때문에, 시간상 이정도로 만족해야 하는게 슬프지만
그래도 너무 뿌듯하다.


번외..

  • treeshake: { moduleSideEffects: false }

    • 사실 이 설정을 사용해보고싶었다.

    • 이 설정은 번들링 과정에서 "사이드 이펙트(부수 효과)"가 없는 모듈을 더 공격적으로 최적화할 수 있게 해주는데 포함시키지 않은 이유는,

      이 설정을 false 로 지정하면 Rollup(Vite의 내부 번들러)은 실제로 사용되지 않는 코드를 더 효과적으로 제거할 수 있는데,

      이는 번들 크기를 상당히 줄일 수 있지만, 모듈이 실제로 사이드 이펙트를 가지고 있다면 예상치 못한 동작이 발생할 수 있다.
      나의 경우 AKS 로 반입될 이미지 버전에서 React Router DOM의 사이드 이펙트를 트리 쉐이킹 과정에서 "사용되지 않는다"고 잘못 판단되어 제거되더라... 그래서 오류가..어흫ㄱ..

  • manualChunks(id)

    • 이 설정도 더 쪼갤 수 있을 거 같은데 아... 하나하나 맛보기엔 시간이 부족했다. 다음에는 깊게 음미해볼 예정이다.

아무튼... 나름(?) ..?

0개의 댓글