Files
2nd/10_Wiki/Topics/Coding/Perf_Bundle_Analysis.md
T
2026-05-09 21:08:02 +09:00

279 lines
6.0 KiB
Markdown

---
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'));
<Suspense fallback={<Spinner />}>
<Heavy />
</Suspense>
```
```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
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
```
→ 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/<lib>
```
→ 의존 추가 결정.
### 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]]