Vite 프로젝트 번들링 최적화

Thomas·2024년 1월 28일
6

Intro

SPA로 웹 프론트엔드를 개발하다보면 번들링 사이즈에 대한 문제를 경험합니다.
빈 html 과 js 파일과 여러 assest 을 내려받아 클라이언트에서 랜더링을 하는 SPA 의 특성 상 초기 js 파일이 크다면 상대적으로 사용자가 초기 로딩이 되는 시간을 기다려야 한다는 UX 단점이 있습니다.
이를 해결하기 위해 여러 방법이 있는데, (예를 들면 이미지, 폰트 등 에셋 최적화가 있습니다.) 가장 직관적으로 번들링 사이즈를 최적화 하는 방법이 있습니다.
오늘은 개발중인 Vite 프로젝트의 번들링 최적화를 했던 과정을 남겨보겠습니다.

문제 도출

일단 아무 변경 없이 빌드를 해봅니다.

yarn run v1.22.19
$ tsc && vite build
vite v5.0.11 building for production...
✓ 3070 modules transformed.
dist/index.html                                0.39 kB │ gzip:   0.28 kB
dist/assets/Challengers_Gray-0NqQiTLY.png     26.26 kB
dist/assets/index-uoqaOAYj.js              1,572.70 kB │ gzip: 507.24 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 3.76s
✨  Done in 5.86s.

창피하게도 하나의 파일에 모든 파일이 번들링되어 들어갑니다.
무려 1572kb 나 되는군요..
사실 개발하고 배포해 테스트 하는 과정에서 초기 로딩에 대한 불편함은 없었습니다.
하지만 개발자로써 빌드된 결과물을 확인했을때 창피함이 있었고, Vite 또한 청크파일이 너무 크다고 노티를 주고 있습니다.

작업 과정

코드 스플리팅

첫 번째 과정으로 코드 스플리팅을 진행해줍니다.
코드 스플리팅이란 런티임 시 동적으로 컴포넌트를 로드해오는 방식으로 번들 파일을 분리해 번들링 최적화를 하는 기법입니다.

https://ko.legacy.reactjs.org/docs/code-splitting.html#code-splitting

React 에서는 lazy 함수를 활용해 코드 스플리팅을 할 수 있습니다.
다양한 곳에서 코드 스플리팅을 했지만, 대표적으로 페이지 컴포넌트를 직접 import 해오는 라우터 파일에서 코드 스플리팅을 진행합니다.

// 원래의 import 방식
import Dashboard from "./pages/dashboard";
import Monthly from "./pages/key-indicator/monthly";
import Daily from "./pages/key-indicator/daily";
import Weekly from "./pages/key-indicator/weekly";
import Nru from "./pages/retention/nru";
import Dau from "./pages/retention/dau";
import Rau from "./pages/retention/rau";
import Pu from "./pages/retention/pu";
import AdminSetting from "./pages/admin-setting";

// 코드 스플리팅 진행
const Dashboard = lazy(() => import("./pages/dashboard"));
const Daily = lazy(() => import("./pages/key-indicator/daily"));
const Weekly = lazy(() => import("./pages/key-indicator/weekly"));
const Monthly = lazy(() => import("./pages/key-indicator/monthly"));
const AdminSetting = lazy(() => import("./pages/admin-setting"));
const Nru = lazy(() => import("./pages/retention/nru"));
const Dau = lazy(() => import("./pages/retention/dau"));
const Rau = lazy(() => import("./pages/retention/rau"));
const Pu = lazy(() => import("./pages/retention/pu"));

코드 스플리팅을 통해 페이지 마다의 lazy loading 을 적용해줍니다.
이때 <Suspense/> 를 통해 동적으로 컴포넌트가 로딩되는 시점에 fallback 될 컴포넌트를 보여줄 수 있습니다.

이렇게 코드 스플리팅을 진행하고 다시 빌드를 합니다.

yarn run v1.22.19
$ tsc && vite build
vite v5.0.11 building for production...
✓ 3075 modules transformed.
dist/index.html                              0.39 kB │ gzip:   0.28 kB
dist/assets/Challengers_Gray-0NqQiTLY.png   26.26 kB
dist/assets/BarLineChart-4XDhKXBQ.js         0.41 kB │ gzip:   0.30 kB
dist/assets/useClosable-oc50gMjq.js          0.44 kB │ gzip:   0.28 kB
dist/assets/MoreOutlined-baqrH10w.js         0.48 kB │ gzip:   0.32 kB
dist/assets/key-P9409q_B.js                  0.84 kB │ gzip:   0.30 kB
dist/assets/CaretUpOutlined-Sg5RFDZA.js      0.85 kB │ gzip:   0.46 kB
dist/assets/key-2fnO2p3A.js                  0.97 kB │ gzip:   0.31 kB
dist/assets/useBreakpoint-iaOUbofd.js        1.77 kB │ gzip:   0.87 kB
dist/assets/index-PxbJ5INO.js                2.17 kB │ gzip:   1.17 kB
dist/assets/index-0pZeAOn3.js                2.17 kB │ gzip:   1.11 kB
dist/assets/index-xEiE-vH-.js                2.17 kB │ gzip:   1.17 kB
dist/assets/index-EFHlVKY6.js                2.17 kB │ gzip:   1.17 kB
dist/assets/index-Ai2poq1i.js                2.17 kB │ gzip:   1.17 kB
dist/assets/index-0tI0w_uT.js                2.87 kB │ gzip:   1.25 kB
dist/assets/index-GTnm32UL.js                2.94 kB │ gzip:   1.26 kB
dist/assets/index-kv-gzahi.js                2.94 kB │ gzip:   1.27 kB
dist/assets/index-BAQOTswa.js                3.47 kB │ gzip:   1.55 kB
dist/assets/index-WC47FAgw.js                3.56 kB │ gzip:   1.59 kB
dist/assets/slide-yIMYm9_I.js                4.07 kB │ gzip:   1.64 kB
dist/assets/index-9rkfBNGP.js                4.12 kB │ gzip:   1.95 kB
dist/assets/index-QIAPLW3r.js                4.84 kB │ gzip:   1.84 kB
dist/assets/SideBar-l_AOEjT7.js              8.60 kB │ gzip:   3.82 kB
dist/assets/Filters-44vUwZYU.js             11.85 kB │ gzip:   4.23 kB
dist/assets/index-YNV9_hfE.js               14.13 kB │ gzip:   5.34 kB
dist/assets/index-6UZeBtEp.js               16.32 kB │ gzip:   5.31 kB
dist/assets/index-25eJolgm.js               16.53 kB │ gzip:   5.97 kB
dist/assets/index-U8JjDU6O.js               17.24 kB │ gzip:   6.58 kB
dist/assets/index-zJzDHQcY.js               27.96 kB │ gzip:   9.13 kB
dist/assets/index-x1Fdu3Ov.js               31.37 kB │ gzip:  12.70 kB
dist/assets/index-n720ydXI.js               53.02 kB │ gzip:  17.15 kB
dist/assets/Spinner-T23UMG59.js            116.80 kB │ gzip:  36.30 kB
dist/assets/common-BpxCeiXT.js             119.39 kB │ gzip:  37.10 kB
dist/assets/roundedArrow-vk3URZAF.js       135.89 kB │ gzip:  48.51 kB
dist/assets/LineChart-AuKRno-q.js          206.60 kB │ gzip:  71.11 kB
dist/assets/Table-et9tYGwF.js              285.71 kB │ gzip:  91.17 kB
dist/assets/index-ye21VB28.js              478.43 kB │ gzip: 159.56 kB
✓ built in 3.55s
✨  Done in 5.51s.

이전에는 하나의 파일로 빌드가 되었는데, 지금은 여러 파일로 분리가 된 것을 확인할 수 있습니다.
하지만 그럼에도 몇개의 파일은 사이즈가 너무 크네요. 이를 해결해봅시다!

rollupOption

Vite 의 빌드 도구인 Rollup 을 활용해봅니다.

import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          if (id.indexOf("node_modules") !== -1) {
            const module = id.split("node_modules/").pop().split("/")[0];
            return `vendor-${module}`;
          }
        },
      },
    },
  },
});

rollupOptionmanulChunk 를 설정해줍니다. 해당 기능은 공통으로 사용하는 모듈을 분리하는 역할을 합니다. 콜백 함수의 첫 번째 파라미터인 id 에서 node_modules 가 있는지 찾고, node_modules 를 스플릿 해서 vendor 라는 prefix 를 붙인 module 을 넣어줍니다.

manualChunk 를 설정한 후 다시 빌드를 해줍니다.

https://rollupjs.org/configuration-options/#output-manualchunks

yarn run v1.22.19
$ tsc && vite build
vite v5.0.11 building for production...
✓ 3075 modules transformed.
Generated an empty chunk: "vendor-compute-scroll-into-view".
Generated an empty chunk: "vendor-copy-to-clipboard".
Generated an empty chunk: "vendor-json2mq".
Generated an empty chunk: "vendor-scroll-into-view-if-needed".
Generated an empty chunk: "vendor-string-convert".
Generated an empty chunk: "vendor-toggle-selection".
dist/index.html                                              3.33 kB │ gzip:  0.82 kB
dist/assets/Challengers_Gray-0NqQiTLY.png                   26.26 kB
dist/assets/vendor-copy-to-clipboard-w40geAFS.js             0.00 kB │ gzip:  0.02 kB
dist/assets/vendor-compute-scroll-into-view-w40geAFS.js      0.00 kB │ gzip:  0.02 kB
dist/assets/vendor-json2mq-w40geAFS.js                       0.00 kB │ gzip:  0.02 kB
dist/assets/vendor-scroll-into-view-if-needed-w40geAFS.js    0.00 kB │ gzip:  0.02 kB
dist/assets/vendor-string-convert-w40geAFS.js                0.00 kB │ gzip:  0.02 kB
dist/assets/vendor-toggle-selection-w40geAFS.js              0.00 kB │ gzip:  0.02 kB
dist/assets/vendor-qrcode.react-I3nGUaY9.js                  0.04 kB │ gzip:  0.06 kB
dist/assets/vendor-rc-rate-fYpeyA9N.js                       0.08 kB │ gzip:  0.08 kB
dist/assets/vendor-rc-steps-fYpeyA9N.js                      0.08 kB │ gzip:  0.08 kB
dist/assets/vendor-rc-switch-fYpeyA9N.js                     0.08 kB │ gzip:  0.08 kB
dist/assets/vendor-rc-slider-ZXSktmKQ.js                     0.11 kB │ gzip:  0.10 kB
dist/assets/vendor-rc-upload-8Z3f2Obj.js                     0.11 kB │ gzip:  0.10 kB
dist/assets/vendor-has-proto-dzJH2Y5C.js                     0.12 kB │ gzip:  0.12 kB
dist/assets/vendor-rc-input-number-RQn0eAQd.js               0.15 kB │ gzip:  0.11 kB
dist/assets/vendor-hasown-sJRJCkSq.js                        0.15 kB │ gzip:  0.15 kB
dist/assets/vendor-rc-segmented-qvH_Dj7p.js                  0.15 kB │ gzip:  0.11 kB
dist/assets/vendor-gopd-_DNEr5XH.js                          0.17 kB │ gzip:  0.17 kB
dist/assets/vendor-rc-cascader-7kSEOp1W.js                   0.19 kB │ gzip:  0.13 kB
dist/assets/vendor-rc-collapse-yk_uFs3Q.js                   0.19 kB │ gzip:  0.13 kB
dist/assets/vendor-rc-tree-select-ZUDZ2t3r.js                0.23 kB │ gzip:  0.15 kB
dist/assets/vendor-rc-drawer-gUFTnDLG.js                     0.23 kB │ gzip:  0.14 kB
dist/assets/vendor-hoist-non-react-statics-Pt3SWaE8.js       0.24 kB │ gzip:  0.17 kB
dist/assets/vendor-rc-image-9q1lTAWp.js                      0.27 kB │ gzip:  0.16 kB
dist/assets/vendor-rc-mentions-8-cIay7s.js                   0.31 kB │ gzip:  0.18 kB
dist/assets/vendor-rc-tabs-6DzsAkEX.js                       0.31 kB │ gzip:  0.17 kB
dist/assets/vendor-has-property-descriptors-WRcnEilg.js      0.32 kB │ gzip:  0.24 kB
dist/assets/BarLineChart-ToZ3QyM9.js                         0.47 kB │ gzip:  0.32 kB
dist/assets/vendor-throttle-debounce-5p4B7Tol.js             0.69 kB │ gzip:  0.40 kB
dist/assets/vendor-set-function-length-yX0dKDkn.js           0.70 kB │ gzip:  0.44 kB
dist/assets/vendor-prop-types-g0p8o0B7.js                    0.74 kB │ gzip:  0.47 kB
dist/assets/LineChart-KVRkcl1S.js                            0.78 kB │ gzip:  0.44 kB
dist/assets/vendor-classnames-jWJbLr47.js                    0.80 kB │ gzip:  0.51 kB
dist/assets/key-pvRwfm3K.js                                  0.84 kB │ gzip:  0.30 kB
dist/assets/vendor-function-bind-HMk2skFd.js                 0.95 kB │ gzip:  0.48 kB
dist/assets/key-eMbPPFQa.js                                  0.97 kB │ gzip:  0.30 kB
dist/assets/vendor-has-symbols-fNtejGuA.js                   0.98 kB │ gzip:  0.40 kB
dist/assets/Spinner-HMgFUWs1.js                              1.25 kB │ gzip:  0.79 kB
dist/assets/vendor-react-router-dom-OwAsU530.js              1.25 kB │ gzip:  0.66 kB
dist/assets/vendor-side-channel-ynwnfowU.js                  1.27 kB │ gzip:  0.57 kB
dist/assets/vendor-rc-checkbox-EaXHrO9h.js                   1.39 kB │ gzip:  0.70 kB
dist/assets/vendor-call-bind-m2rrydzu.js                     1.54 kB │ gzip:  0.77 kB
dist/assets/vendor-define-data-property-rgmy2o8_.js          1.56 kB │ gzip:  0.64 kB
dist/assets/common-Yec69nDV.js                               1.69 kB │ gzip:  0.70 kB
dist/assets/vendor-react-chartjs-2-vpBp4of-.js               1.70 kB │ gzip:  0.80 kB
dist/assets/vendor-zustand-heHSJhJT.js                       2.06 kB │ gzip:  0.96 kB
dist/assets/vendor-use-sync-external-store-2l_RzKh8.js       2.22 kB │ gzip:  0.97 kB
dist/assets/vendor-@react-oauth-_2xE31nX.js                  2.63 kB │ gzip:  1.13 kB
dist/assets/vendor-rc-resize-observer-grJp9lIj.js            2.64 kB │ gzip:  1.26 kB
dist/assets/Filters-PB6zR29f.js                              2.67 kB │ gzip:  1.21 kB
dist/assets/vendor-rc-tooltip-cp2MX5tp.js                    2.91 kB │ gzip:  1.18 kB
dist/assets/vendor-react-csv-pgCiorVR.js                     3.45 kB │ gzip:  1.60 kB
dist/assets/vendor-rc-dropdown-uoQOPMR8.js                   3.61 kB │ gzip:  1.60 kB
dist/assets/vendor-scheduler-iwWdm5Ml.js                     4.10 kB │ gzip:  1.78 kB
dist/assets/vendor-rc-progress-mm54KzCZ.js                   4.33 kB │ gzip:  2.03 kB
dist/assets/index-VxZUJkJ0.js                                4.44 kB │ gzip:  2.03 kB
dist/assets/index-CX5tIL5N.js                                4.45 kB │ gzip:  2.03 kB
dist/assets/index-PhB-RNvf.js                                4.45 kB │ gzip:  2.03 kB
dist/assets/index-q3xhv9YR.js                                4.45 kB │ gzip:  2.03 kB
dist/assets/vendor-react-is-TX1QBbwd.js                      4.55 kB │ gzip:  1.29 kB
dist/assets/index-8EqRcKDB.js                                4.58 kB │ gzip:  2.07 kB
dist/assets/index-zCSSkpGN.js                                4.58 kB │ gzip:  2.02 kB
dist/assets/index-t3IAXeQI.js                                5.12 kB │ gzip:  2.18 kB
dist/assets/index-ZBct2CYc.js                                5.16 kB │ gzip:  2.11 kB
dist/assets/index-9-CofZKP.js                                5.23 kB │ gzip:  2.12 kB
dist/assets/index-1_BJ5xrF.js                                5.23 kB │ gzip:  2.13 kB
dist/assets/SideBar-f72_aDMY.js                              5.34 kB │ gzip:  2.36 kB
dist/assets/vendor-rc-overflow-ARH6giey.js                   5.48 kB │ gzip:  2.46 kB
dist/assets/index-jDCJjciP.js                                5.90 kB │ gzip:  2.47 kB
dist/assets/index-AMCNsXZa.js                                5.99 kB │ gzip:  2.51 kB
dist/assets/vendor-rc-dialog-qjxy6SOS.js                     6.48 kB │ gzip:  2.60 kB
dist/assets/vendor-rc-input-PxhWouqQ.js                      6.63 kB │ gzip:  2.63 kB
dist/assets/vendor-rc-textarea-aUsJQTxe.js                   7.22 kB │ gzip:  3.04 kB
dist/assets/vendor-rc-notification-8myIlwAu.js               7.49 kB │ gzip:  3.07 kB
dist/assets/vendor-@kurkle-sRCxMDZz.js                       7.67 kB │ gzip:  3.84 kB
dist/assets/vendor-resize-observer-polyfill-B32NGzNS.js      7.71 kB │ gzip:  2.54 kB
dist/assets/index-ogouW05b.js                                8.00 kB │ gzip:  2.86 kB
dist/assets/vendor-get-intrinsic-ipxF7d6m.js                 8.01 kB │ gzip:  2.62 kB
dist/assets/vendor-react-EKpGEB28.js                         8.05 kB │ gzip:  3.05 kB
dist/assets/vendor-stylis-T_tok5ay.js                        8.07 kB │ gzip:  2.80 kB
dist/assets/vendor-react-router-ICvzhgIO.js                  8.36 kB │ gzip:  3.22 kB
dist/assets/index--lGOqGrx.js                                8.67 kB │ gzip:  3.74 kB
dist/assets/vendor-rc-pagination-pnjr7jC0.js                 9.00 kB │ gzip:  3.63 kB
dist/assets/vendor-rc-motion-VttFEQWa.js                     9.02 kB │ gzip:  3.79 kB
dist/assets/vendor-@remix-run-w2aNq8l3.js                    9.12 kB │ gzip:  3.97 kB
dist/assets/vendor-object-inspect-pjU9sBk0.js                9.26 kB │ gzip:  3.37 kB
dist/assets/vendor-qs-A-50LpiL.js                           11.17 kB │ gzip:  4.19 kB
dist/assets/vendor-@babel-n0-8bDZp.js                       11.89 kB │ gzip:  4.23 kB
dist/assets/vendor-@ctrl-_Y40OLZn.js                        14.15 kB │ gzip:  4.77 kB
dist/assets/vendor-rc-virtual-list-jchRkFIp.js              14.70 kB │ gzip:  5.89 kB
dist/assets/vendor-async-validator-BHjhHa7C.js              16.90 kB │ gzip:  5.41 kB
dist/assets/vendor-dayjs-ZZCX540N.js                        17.00 kB │ gzip:  6.42 kB
dist/assets/index-Sa-onBeO.js                               17.17 kB │ gzip:  5.88 kB
dist/assets/vendor-rc-util-L0L_1efi.js                      17.85 kB │ gzip:  8.04 kB
dist/assets/vendor-@emotion-O9w_oL51.js                     20.01 kB │ gzip:  8.11 kB
dist/assets/vendor-react-hook-form-utXS2Mic.js              20.36 kB │ gzip:  7.95 kB
dist/assets/vendor-react-responsive-xQWjK2Wh.js             22.22 kB │ gzip:  7.58 kB
dist/assets/vendor-rc-menu-G-l310He.js                      22.35 kB │ gzip:  8.48 kB
dist/assets/vendor-@rc-component-CzsCMju_.js                22.45 kB │ gzip:  8.91 kB
dist/assets/vendor-axios-G2rPRu76.js                        29.44 kB │ gzip: 11.91 kB
dist/assets/vendor-rc-field-form-qmbq2LNh.js                32.12 kB │ gzip: 10.44 kB
dist/assets/vendor-rc-select-8OEneE87.js                    33.62 kB │ gzip: 12.41 kB
dist/assets/vendor-@tanstack-I76hcoAo.js                    36.22 kB │ gzip: 10.70 kB
dist/assets/vendor-rc-table-vLPQGhfb.js                     38.03 kB │ gzip: 13.80 kB
dist/assets/vendor-rc-tree-F51dS5Lx.js                      39.46 kB │ gzip: 12.85 kB
dist/assets/vendor-@ant-design-kBuMDe-P.js                  46.74 kB │ gzip: 16.31 kB
dist/assets/vendor-rc-picker-2KyeN1Lg.js                    61.45 kB │ gzip: 19.52 kB
dist/assets/vendor-lodash-eCiNlUIx.js                       71.82 kB │ gzip: 26.66 kB
dist/assets/vendor-sweetalert2-jY80U3j3.js                  76.92 kB │ gzip: 20.77 kB
dist/assets/vendor-react-dom-4LKLwjx8.js                   130.36 kB │ gzip: 41.93 kB
dist/assets/vendor-chart.js-2x18pcoT.js                    196.55 kB │ gzip: 66.46 kB
dist/assets/vendor-antd-n90qULgp.js                        345.75 kB │ gzip: 94.20 kB
✓ built in 3.55s
✨  Done in 5.38s.

하나의 청크파일인 antd 모듈이 다른 파일에 비해 사이즈가 큽니다. 비어있는 청크파일도 몇몇 생성이 되어있는데, 해당 파일을 찾아본 결과 antd 에 포함된 파일입니다. UI 라이브러리 사이즈가 크다고 생각했는데 빌드 결과를 보니 예상보다 훨씬 더 큰다고 생각이 듭니다.

결과

빌드된 결과물을 확인해봅니다. 크게 변화된 것은 없지만 번들링 최적화 이후 초기 로딩이 이전보다 빨라짐이 체감됩니다.

Lighthouse 의 지표에도 조금의 변화가 생겼습니다.

기존 코드 스플리팅 진행 전 절감할 수 있는 JS 파일의 크기는 위와 같습니다.
또한 메트릭을 살펴봐도 지표가 나쁨을 확인할 수 있습니다.

코드 스플리팅 진행 후 다음과 같습니다. 물론 절감할 수 있는 번들링 파일이 나옵니다. 아래 두 파일은 antd 와 chart.js 번들링 파일입니다. 아래 파일들을 더욱 절감할 수 있는지 방안을 찾아봐야 할 것 같습니다.
메트릭에서도 나름 개선이 된 것을 확인할 수 있습니다.
LCP 지표는 아직도 나쁘네요. 이를 해결하는 방안도 찾아봐야겠습니다.

결과적으로 Lighthouse 의 성능이 72점에서 90점으로 올라갔습니다.
아직 만족하지 못하는 결과입니다. 랜딩 페이지이지만 이전에 개발했던 랜딩 페이지는 성능이 100점이었습니다.
프레임워크의 차이도 있습니다. 현재 개발한 웹은 Vite 를 활용한 SPA 이지만 랜딩 페이지는 Next.js 프레임워크를 사용했습니다.

마무리하며

아직 개발중인 프로젝트이기에 개선할 점들이 많이 있습니다. 앞으로도 계속해서 웹 성능 최적화를 고려하며 개발해 사용자에게 최상의 만족감을 줄 수 있도록 노력해야겠습니다. 감사합니다.

profile
안녕하세요! 주니어 웹 개발자입니다 😆

1개의 댓글

감사합니다! 많은 도움이 되었습니다

답글 달기