phy-bundle-size-audit

GitHub 作者 PHY041

JavaScript bundle size auditor and budget enforcer. Parses webpack stats JSON, Vite bundle report, Rollup output, or Next.js build output to identify largest chunks, flag chunks that exceed configurable size budgets, detect duplicate dependencies across chunks, find treeshaking failures (packages that should be side-effect-free but aren't), and generate CI fail-gate commands. Outputs a GitHub PR annotation-ready summary with per-chunk size, gzip estimate, and actionable optimization suggestions. Zero external API — pure local file analysis. Triggers on "bundle too large", "webpack stats", "chunk size", "bundle budget", "treeshaking", "bundle analyzer", "/bundle-size-audit".

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install github:LeoYeAI~openclaw-master-skills~phy-bundle-size-audit
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/github%3ALeoYeAI~openclaw-master-skills~phy-bundle-size-audit/file -o phy-bundle-size-audit.md
# Bundle Size Auditor

You added one import. Your Lighthouse score dropped 12 points. Your LCP went from 1.8s to 3.1s.

This skill parses your bundler's output — webpack stats JSON, Vite build report, or Next.js `.next/` directory — identifies the largest chunks, finds which packages are bloating them, detects treeshaking failures, and gives you the exact code changes to fix it.

**Supports webpack, Vite, Rollup, Next.js. Zero external API.**

---

## Trigger Phrases

- "bundle too large", "my bundle is huge", "reduce bundle size"
- "webpack stats", "analyze webpack output", "chunk size"
- "bundle budget", "size limit", "bundle regression"
- "treeshaking not working", "dead code in bundle"
- "which package is largest", "find large dependencies"
- "Next.js bundle", "Vite build report"
- "/bundle-size-audit"

---

## How to Provide Input

```bash
# Option 1: Webpack stats JSON (most detailed)
webpack --profile --json > stats.json
/bundle-size-audit stats.json

# Option 2: Vite build (auto-detected)
vite build
/bundle-size-audit dist/

# Option 3: Next.js build (auto-detected)
next build
/bundle-size-audit .next/

# Option 4: Rollup stats
rollup -c --bundleConfigAsCjs
/bundle-size-audit rollup-stats.json

# Option 5: Set size budgets
/bundle-size-audit --max-chunk 250kb --max-initial 500kb

# Option 6: Focus on a specific chunk or package
/bundle-size-audit --chunk main --package lodash

# Option 7: Generate CI fail-gate only
/bundle-size-audit --ci-config
```

---

## Step 1: Detect Build Artifacts

```bash
python3 -c "
import os, json
from pathlib import Path

# Priority order for detection
checks = [
    ('webpack stats', ['stats.json', 'webpack-stats.json', 'build/stats.json']),
    ('Next.js', ['.next/build-manifest.json', '.next/server/pages-manifest.json']),
    ('Vite', ['dist/', 'dist/assets/']),
    ('Rollup', ['dist/', 'build/']),
    ('Create React App', ['build/static/js/', 'build/asset-manifest.json']),
]

found = []
for name, paths in checks:
    for p in paths:
        if os.path.exists(p):
            found.append((name, p))
            break

if found:
    for name, path in found:
        size = 0
        if os.path.isfile(path):
            size = os.path.getsize(path)
            print(f'Detected {name}: {path} ({size:,} bytes)')
        else:
            # Directory — count JS files
            js_files = list(Path(path).rglob('*.js'))
            total = sum(f.stat().st_size for f in js_files)
            print(f'Detected {name}: {path}/ ({len(js_files)} JS files, {total/1024:.0f} KB total)')
else:
    print('No build artifacts found.')
    print('Run your build first:')
    print('  webpack --profile --json > stats.json')
    print('  vite build')
    print('  next build')
"
```

---

## Step 2: Parse Bundle Data

### For Webpack Stats JSON

```python
import json
from pathlib import Path

def parse_webpack_stats(stats_path):
    """Parse webpack stats.json into chunk/asset summary."""
    with open(stats_path) as f:
        stats = json.load(f)

    assets = []
    for asset in stats.get('assets', []):
        name = asset['name']
        size = asset['size']
        # Only JS and CSS assets
        if not any(name.endswith(ext) for ext in ['.js', '.css', '.mjs']):
            continue
        assets.append({
            'name': name,
            'size': size,
            'size_kb': round(size / 1024, 1),
            'gzip_estimate_kb': round(size / 1024 * 0.3, 1),  # ~30% compression typical
            'chunks': asset.get('chunkNames', []),
            'initial': asset.get('isOverSizeLimit', False),
        })

    # Sort by size desc
    assets.sort(key=lambda x: x['size'], reverse=True)

    # Module analysis — find largest modules
    modules = []
    for module in stats.get('modules', []):
        if module.get('size', 0) > 10 * 1024:  # >10KB only
            modules.append({
                'name': module.get('name', 'unknown'),
                'size': module.get('size', 0),
                'size_kb': round(module.get('size', 0) / 1024, 1),
                'reasons': [r.get('moduleName', '') for r in module.get('reasons', [])[:3]],
            })
    modules.sort(key=lambda x: x['size'], reverse=True)

    return assets, modules[:50]  # top 50 modules


def parse_webpack_chunks(stats_path):
    """Identify chunks and their composition."""
    with open(stats_path) as f:
        stats = json.load(f)

    chunks = []
    for chunk in stats.get('chunks', []):
        chunks.append({
            'id': chunk.get('id'),
            'names': chunk.get('names', []),
            'size': chunk.get('size', 0),
            'size_kb': round(chunk.get('size', 0) / 1024, 1),
            'initial': chunk.get('initial', False),
            'entry': chunk.get('entry', False),
            'module_count': len(chunk.get('modules', [])),
            'files': chunk.get('files', []),
        })

    chunks.sort(key=lambda x: x['size'], reverse=True)
    return chunks
```

### For Vite / CRA (dist/ directory)

```python
import os
from pathlib import Path

def parse_dist_directory(dist_path):
    """Parse build output directory for Vite/CRA/Rollup."""
    js_dir = Path(dist_path)

    # Find all JS files
    js_files = []
    for fpath in js_dir.rglob('*.js'):
        if 'node_modules' in str(fpath):
            continue
        size = fpath.stat().st_size
        js_files.append({
            'name': str(fpath.relative_to(dist_path)),
            'path': str(fpath),
            'size': size,
            'size_kb': round(size / 1024, 1),
            'gzip_estimate_kb': round(size / 1024 * 0.3, 1),
            'is_chunk': 'chunk' in fpath.name.lower() or fpath.parent.name == 'assets',
            'is_vendor': 'vendor' in fpath.name.lower(),
        })

    js_files.sort(key=lambda x: x['size'], reverse=True)

    # CSS files
    css_files = []
    for fpath in js_dir.rglob('*.css'):
        size = fpath.stat().st_size
        css_files.append({
            'name': str(fpath.relative_to(dist_path)),
            'size_kb': round(size / 1024, 1),
        })

    return js_files, css_files


def estimate_gzip(size_bytes):
    """Estimate gzip size. JS typically compresses 65-75%."""
    return round(size_bytes * 0.30 / 1024, 1)
```

### For Next.js (.next/ directory)

```bash
# Next.js provides build-manifest.json and .next/analyze/ if ANALYZE=true
python3 -c "
import json, os
from pathlib import Path

next_dir = Path('.next')
if not next_dir.exists():
    print('No .next/ directory found. Run: next build')
    exit(1)

# Read build-manifest
manifest_path = next_dir / 'build-manifest.json'
if manifest_path.exists():
    manifest = json.loads(manifest_path.read_text())

    # Collect all JS files referenced
    all_files = set()
    for page, files in manifest.get('pages', {}).items():
        for f in files:
            all_files.add(f)

    print(f'Pages: {len(manifest.get(\"pages\", {}))}')
    print(f'Unique JS chunks: {len(all_files)}')

# Read static/chunks/
chunks_dir = next_dir / 'static' / 'chunks'
if chunks_dir.exists():
    chunks = []
    for fpath in chunks_dir.rglob('*.js'):
        size = fpath.stat().st_size
        chunks.append((fpath.name, size))
    chunks.sort(key=lambda x: x[1], reverse=True)

    total = sum(s for _, s in chunks)
    print(f'\\nTotal chunks: {len(chunks)} ({total/1024:.0f} KB)')
    print('\\nTop 10 chunks:')
    for name, size in chunks[:10]:
        print(f'  {name}: {size/1024:.1f} KB (~{size*0.3/1024:.1f} KB gzip)')
"
```

---

## Step 3: Detect Duplicate Dependencies

```python
import re
from pathlib import Path

def find_duplicate_modules(stats_path):
    """Find the same package duplicated across chunks (common with npm hoisting issues)."""
    with open(stats_path) as f:
        stats = json.load(f)

    # Track package versions seen in different chunks
    package_chunks = {}

    for module in stats.get('modules', []):
        name = module.get('name', '')
        # Extract package name from ./node_modu