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.
이전에는 하나의 파일로 빌드가 되었는데, 지금은 여러 파일로 분리가 된 것을 확인할 수 있습니다.
하지만 그럼에도 몇개의 파일은 사이즈가 너무 크네요. 이를 해결해봅시다!
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}`;
}
},
},
},
},
});
rollupOption
중 manulChunk
를 설정해줍니다. 해당 기능은 공통으로 사용하는 모듈을 분리하는 역할을 합니다. 콜백 함수의 첫 번째 파라미터인 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 프레임워크를 사용했습니다.
아직 개발중인 프로젝트이기에 개선할 점들이 많이 있습니다. 앞으로도 계속해서 웹 성능 최적화를 고려하며 개발해 사용자에게 최상의 만족감을 줄 수 있도록 노력해야겠습니다. 감사합니다.
감사합니다! 많은 도움이 되었습니다