--- id: perf-bundle-analysis title: JS Bundle Analysis — Tree-shake / Split / Size budget category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [performance, bundle, vite, webpack, vibe-coding] tech_stack: { language: "TS / Vite / Webpack", applicable_to: ["Frontend"] } applied_in: [] aliases: [bundle analysis, tree shake, code split, dynamic import, size limit, source map explorer] --- # Bundle Analysis > 1MB+ JS bundle = 모바일 3G = 10초+ load. **분석 → tree-shake → split → lazy → size budget**. CI 가 회귀 차단. ## 📖 핵심 개념 - Tree-shake: 안 쓴 export 제거. - Code split: route / component 별 chunk. - Dynamic import: 사용 시 로드. - Size budget: CI gate. ## 💻 코드 패턴 ### Vite — visualizer ```bash yarn add -D rollup-plugin-visualizer ``` ```ts // vite.config.ts import { visualizer } from 'rollup-plugin-visualizer'; plugins: [ visualizer({ open: true, gzipSize: true, brotliSize: true }), ]; ``` ```bash yarn build # stats.html 자동 open — sunburst chart ``` ### Source map explorer ```bash npx source-map-explorer dist/assets/*.js ``` → 각 module 의 byte 시각화. ### Webpack — bundle analyzer ```bash yarn add -D webpack-bundle-analyzer ``` ```js // webpack.config.js const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); plugins: [new BundleAnalyzerPlugin({ analyzerMode: 'static' })]; ``` ### Next.js ```bash ANALYZE=true yarn build ``` ```ts // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' }); module.exports = withBundleAnalyzer({}); ``` ### Tree-shake 깨지는 패턴 ```ts // ❌ default export + barrel import lib from 'lib'; // 모든 거 import import { fn } from 'lib'; // 더 좋음 — but lib 가 sideEffects 인지 // ❌ side effects (top-level) console.log('module loaded'); // tree-shake 차단 // ❌ require (dynamic) const x = require(`module-${name}`); // ✅ Named ESM imports + sideEffects: false ``` ```jsonc // package.json — library publishing { "sideEffects": false // 또는 ["./styles.css"] } ``` → Bundler 가 안전하게 제거. ### Dynamic import (route split) ```tsx // React lazy const Heavy = lazy(() => import('./Heavy')); }> ``` ```ts // 일반 dynamic button.onclick = async () => { const { default: heavy } = await import('./heavy'); heavy.run(); }; ``` ### Route-level split (Vite) ```tsx // react-router 6+ const router = createBrowserRouter([ { path: '/admin', lazy: async () => { const { AdminPage } = await import('./AdminPage'); return { Component: AdminPage }; }, }, ]); ``` ### Manual chunks ```ts // vite.config.ts build: { rollupOptions: { output: { manualChunks: { react: ['react', 'react-dom'], query: ['@tanstack/react-query'], editor: ['@tiptap/core', '@tiptap/react'], }, }, }, }, ``` ### Library swap (큰 → 작은) ``` moment (200KB) → date-fns (modular, ~10KB used) lodash (full) → lodash-es / specific imports react-icons (모두) → 단일 svg 또는 lucide-react (tree-shake) material-ui (?? KB) → headless UI + Tailwind ``` ```ts // ❌ import _ from 'lodash'; // 모두 // ✅ import debounce from 'lodash/debounce'; // 또는 lodash-es import { debounce } from 'lodash-es'; ``` ### CDN / external (큰 lib) ```ts // vite.config — 일부 lib 외부화 build: { rollupOptions: { external: ['react', 'react-dom'], output: { globals: { react: 'React', 'react-dom': 'ReactDOM' }, }, }, }, ``` ```html ``` → Bundle 작아짐 + CDN cache. ### Modern build (Vite 자동) - ES module syntax 그대로 (browser native). - 옛 browser 만 polyfill (`@vitejs/plugin-legacy`). ### Size budget (size-limit) ```bash yarn add -D size-limit @size-limit/preset-app ``` ```jsonc // package.json { "size-limit": [ { "path": "dist/**/*.{js,css}", "limit": "300 KB" }, { "path": "dist/index-*.js", "limit": "150 KB" } ], "scripts": { "size": "size-limit" } } ``` ```bash yarn size # CI 에서 강제 ``` ### Bundlephobia (NPM 패키지 사이즈) ```bash # 새 lib 추가 전 # https://bundlephobia.com/package/ ``` → 의존 추가 결정. ### Gzip / Brotli compression ```nginx gzip on; gzip_types text/css application/javascript application/json; gzip_min_length 1024; brotli on; brotli_types text/css application/javascript application/json; ``` → 30-70% 추가 절감. ### Async lib (dynamic only) ```ts // 무거운 lib = 사용자 인터랙션 시만 let echarts: typeof import('echarts') | null = null; async function showChart() { if (!echarts) echarts = await import('echarts'); echarts.init(...); } ``` ### Image vs JS ``` JS 100KB → parse + execute = 실제 시간 큼. Image 100KB → 그냥 바이트. → JS 가 비용 더 높음. 줄이기 우선. ``` ## 🤔 의사결정 기준 | 작업 | 도구 | |---|---| | 분석 | Vite visualizer / Source map explorer | | Route split | React.lazy / loader | | 큰 lib → 작은 | bundlephobia 비교 | | Library export 검사 | sideEffects: false | | CI 회귀 | size-limit | | Modern stack | Vite/Rollup (vs Webpack legacy) | ## ❌ 안티패턴 - **모든 거 한 bundle**: 첫 load 큼. - **`import * as`**: tree-shake 어려움. - **CSS-in-JS 큰 lib**: runtime 비용. Tailwind 또는 vanilla extract. - **모든 component lazy**: waterfall — 첫 paint 느림. - **Polyfill 모든 browser**: 작은 점유율 위해 큰 cost. - **Bundle analysis 안 함**: 큰 lib 모름. - **Source map prod 노출**: 코드 leak. 또는 sentry 만. ## 🤖 LLM 활용 힌트 - Vite visualizer 매 빌드 + size-limit CI. - Route lazy + manual chunks. - 큰 lib = bundlephobia + 작은 alternative. ## 🔗 관련 문서 - [[Web_Performance_Core_Vitals]] - [[React_Code_Splitting]] - [[TS_Build_Bundler_Patterns]]