Files
claude-skills-reference/engineering-team/senior-frontend/scripts/bundle_analyzer.py
Alireza Rezvani 829a197c2b fix(skill): rewrite senior-frontend with React/Next.js content (#63) (#118)
Replace placeholder content with real frontend development guidance:

References:
- react_patterns.md: Compound Components, Render Props, Custom Hooks
- nextjs_optimization_guide.md: Server/Client Components, ISR, caching
- frontend_best_practices.md: Accessibility, testing, TypeScript patterns

Scripts:
- frontend_scaffolder.py: Generate Next.js/React projects with features
- component_generator.py: Generate React components with tests/stories
- bundle_analyzer.py: Analyze package.json for optimization opportunities

SKILL.md:
- Added table of contents
- Numbered workflow steps
- Removed marketing language
- Added trigger phrases in description

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 03:40:48 +01:00

408 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Frontend Bundle Analyzer
Analyzes package.json and project structure for bundle optimization opportunities,
heavy dependencies, and best practice recommendations.
Usage:
python bundle_analyzer.py <project_dir>
python bundle_analyzer.py . --json
python bundle_analyzer.py /path/to/project --verbose
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
# Known heavy packages and their lighter alternatives
HEAVY_PACKAGES = {
"moment": {
"size": "290KB",
"alternative": "date-fns (12KB) or dayjs (2KB)",
"reason": "Large locale files bundled by default"
},
"lodash": {
"size": "71KB",
"alternative": "lodash-es with tree-shaking or individual imports (lodash/get)",
"reason": "Full library often imported when only few functions needed"
},
"jquery": {
"size": "87KB",
"alternative": "Native DOM APIs or React/Vue patterns",
"reason": "Rarely needed in modern frameworks"
},
"axios": {
"size": "14KB",
"alternative": "Native fetch API (0KB) or ky (3KB)",
"reason": "Fetch API covers most use cases"
},
"underscore": {
"size": "17KB",
"alternative": "Native ES6+ methods or lodash-es",
"reason": "Most utilities now in standard JavaScript"
},
"chart.js": {
"size": "180KB",
"alternative": "recharts (bundled with React) or lightweight-charts",
"reason": "Consider if you need all chart types"
},
"three": {
"size": "600KB",
"alternative": "None - use dynamic import for 3D features",
"reason": "Very large, should be lazy-loaded"
},
"firebase": {
"size": "400KB+",
"alternative": "Import specific modules (firebase/auth, firebase/firestore)",
"reason": "Modular imports significantly reduce size"
},
"material-ui": {
"size": "Large",
"alternative": "shadcn/ui (copy-paste components) or Tailwind",
"reason": "Heavy runtime, consider headless alternatives"
},
"@mui/material": {
"size": "Large",
"alternative": "shadcn/ui or Radix UI + Tailwind",
"reason": "Heavy runtime, consider headless alternatives"
},
"antd": {
"size": "Large",
"alternative": "shadcn/ui or Radix UI + Tailwind",
"reason": "Heavy runtime, consider headless alternatives"
}
}
# Recommended optimizations by package
PACKAGE_OPTIMIZATIONS = {
"react-icons": "Import individual icons: import { FaHome } from 'react-icons/fa'",
"date-fns": "Use tree-shaking: import { format } from 'date-fns'",
"@heroicons/react": "Already tree-shakeable, good choice",
"lucide-react": "Already tree-shakeable, add to optimizePackageImports in next.config.js",
"framer-motion": "Use dynamic import for non-critical animations",
"recharts": "Consider lazy loading for dashboard charts",
}
# Development dependencies that should not be in dependencies
DEV_ONLY_PACKAGES = [
"typescript", "@types/", "eslint", "prettier", "jest", "vitest",
"@testing-library", "cypress", "playwright", "storybook", "@storybook",
"webpack", "vite", "rollup", "esbuild", "tailwindcss", "postcss",
"autoprefixer", "sass", "less", "husky", "lint-staged"
]
def load_package_json(project_dir: Path) -> Optional[Dict]:
"""Load and parse package.json."""
package_path = project_dir / "package.json"
if not package_path.exists():
return None
try:
with open(package_path) as f:
return json.load(f)
except json.JSONDecodeError:
return None
def analyze_dependencies(package_json: Dict) -> Dict:
"""Analyze dependencies for issues."""
deps = package_json.get("dependencies", {})
dev_deps = package_json.get("devDependencies", {})
issues = []
warnings = []
optimizations = []
# Check for heavy packages
for pkg, info in HEAVY_PACKAGES.items():
if pkg in deps:
issues.append({
"package": pkg,
"type": "heavy_dependency",
"size": info["size"],
"alternative": info["alternative"],
"reason": info["reason"]
})
# Check for dev dependencies in production
for pkg in deps.keys():
for dev_pattern in DEV_ONLY_PACKAGES:
if dev_pattern in pkg:
warnings.append({
"package": pkg,
"type": "dev_in_production",
"message": f"{pkg} should be in devDependencies, not dependencies"
})
# Check for optimization opportunities
for pkg in deps.keys():
for opt_pkg, opt_tip in PACKAGE_OPTIMIZATIONS.items():
if opt_pkg in pkg:
optimizations.append({
"package": pkg,
"tip": opt_tip
})
# Check for outdated React patterns
if "prop-types" in deps and ("typescript" in dev_deps or "@types/react" in dev_deps):
warnings.append({
"package": "prop-types",
"type": "redundant",
"message": "prop-types is redundant when using TypeScript"
})
# Check for multiple state management libraries
state_libs = ["redux", "@reduxjs/toolkit", "mobx", "zustand", "jotai", "recoil", "valtio"]
found_state_libs = [lib for lib in state_libs if lib in deps]
if len(found_state_libs) > 1:
warnings.append({
"packages": found_state_libs,
"type": "multiple_state_libs",
"message": f"Multiple state management libraries found: {', '.join(found_state_libs)}"
})
return {
"total_dependencies": len(deps),
"total_dev_dependencies": len(dev_deps),
"issues": issues,
"warnings": warnings,
"optimizations": optimizations
}
def check_nextjs_config(project_dir: Path) -> Dict:
"""Check Next.js configuration for optimizations."""
config_paths = [
project_dir / "next.config.js",
project_dir / "next.config.mjs",
project_dir / "next.config.ts"
]
for config_path in config_paths:
if config_path.exists():
try:
content = config_path.read_text()
suggestions = []
# Check for image optimization
if "images" not in content:
suggestions.append("Configure images.remotePatterns for optimized image loading")
# Check for package optimization
if "optimizePackageImports" not in content:
suggestions.append("Add experimental.optimizePackageImports for lucide-react, @heroicons/react")
# Check for transpilePackages
if "transpilePackages" not in content and "swc" not in content:
suggestions.append("Consider transpilePackages for monorepo packages")
return {
"found": True,
"path": str(config_path),
"suggestions": suggestions
}
except Exception:
pass
return {
"found": False,
"suggestions": ["Create next.config.js with image and bundle optimizations"]
}
def analyze_imports(project_dir: Path) -> Dict:
"""Analyze import patterns in source files."""
issues = []
src_dirs = [project_dir / "src", project_dir / "app", project_dir / "pages"]
patterns_to_check = [
(r"import\s+\*\s+as\s+\w+\s+from\s+['\"]lodash['\"]", "Avoid import * from lodash, use individual imports"),
(r"import\s+moment\s+from\s+['\"]moment['\"]", "Consider replacing moment with date-fns or dayjs"),
(r"import\s+\{\s*\w+(?:,\s*\w+){5,}\s*\}\s+from\s+['\"]react-icons", "Import icons from specific icon sets (react-icons/fa)"),
]
files_checked = 0
for src_dir in src_dirs:
if not src_dir.exists():
continue
for ext in ["*.ts", "*.tsx", "*.js", "*.jsx"]:
for file_path in src_dir.glob(f"**/{ext}"):
if "node_modules" in str(file_path):
continue
files_checked += 1
try:
content = file_path.read_text()
for pattern, message in patterns_to_check:
if re.search(pattern, content):
issues.append({
"file": str(file_path.relative_to(project_dir)),
"issue": message
})
except Exception:
continue
return {
"files_checked": files_checked,
"issues": issues
}
def calculate_score(analysis: Dict) -> Tuple[int, str]:
"""Calculate bundle health score."""
score = 100
# Deduct for heavy dependencies
score -= len(analysis["dependencies"]["issues"]) * 10
# Deduct for dev deps in production
score -= len([w for w in analysis["dependencies"]["warnings"]
if w.get("type") == "dev_in_production"]) * 5
# Deduct for import issues
score -= len(analysis.get("imports", {}).get("issues", [])) * 3
# Deduct for missing Next.js optimizations
if not analysis.get("nextjs", {}).get("found", True):
score -= 10
score = max(0, min(100, score))
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
return score, grade
def print_report(analysis: Dict) -> None:
"""Print human-readable report."""
score, grade = calculate_score(analysis)
print("=" * 60)
print("FRONTEND BUNDLE ANALYSIS REPORT")
print("=" * 60)
print(f"\nBundle Health Score: {score}/100 ({grade})")
deps = analysis["dependencies"]
print(f"\nDependencies: {deps['total_dependencies']} production, {deps['total_dev_dependencies']} dev")
# Heavy dependencies
if deps["issues"]:
print("\n--- HEAVY DEPENDENCIES ---")
for issue in deps["issues"]:
print(f"\n {issue['package']} ({issue['size']})")
print(f" Reason: {issue['reason']}")
print(f" Alternative: {issue['alternative']}")
# Warnings
if deps["warnings"]:
print("\n--- WARNINGS ---")
for warning in deps["warnings"]:
if "package" in warning:
print(f" - {warning['package']}: {warning['message']}")
else:
print(f" - {warning['message']}")
# Optimizations
if deps["optimizations"]:
print("\n--- OPTIMIZATION TIPS ---")
for opt in deps["optimizations"]:
print(f" - {opt['package']}: {opt['tip']}")
# Next.js config
if "nextjs" in analysis:
nextjs = analysis["nextjs"]
if nextjs.get("suggestions"):
print("\n--- NEXT.JS CONFIG ---")
for suggestion in nextjs["suggestions"]:
print(f" - {suggestion}")
# Import issues
if analysis.get("imports", {}).get("issues"):
print("\n--- IMPORT ISSUES ---")
for issue in analysis["imports"]["issues"][:10]: # Limit to 10
print(f" - {issue['file']}: {issue['issue']}")
# Summary
print("\n--- RECOMMENDATIONS ---")
if score >= 90:
print(" Bundle is well-optimized!")
elif deps["issues"]:
print(" 1. Replace heavy dependencies with lighter alternatives")
if deps["warnings"]:
print(" 2. Move dev-only packages to devDependencies")
if deps["optimizations"]:
print(" 3. Apply import optimizations for tree-shaking")
print("\n" + "=" * 60)
def main():
parser = argparse.ArgumentParser(
description="Analyze frontend project for bundle optimization opportunities"
)
parser.add_argument(
"project_dir",
nargs="?",
default=".",
help="Project directory to analyze (default: current directory)"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Include detailed import analysis"
)
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
if not project_dir.exists():
print(f"Error: Directory not found: {project_dir}", file=sys.stderr)
sys.exit(1)
package_json = load_package_json(project_dir)
if not package_json:
print("Error: No valid package.json found", file=sys.stderr)
sys.exit(1)
analysis = {
"project": str(project_dir),
"dependencies": analyze_dependencies(package_json),
"nextjs": check_nextjs_config(project_dir)
}
if args.verbose:
analysis["imports"] = analyze_imports(project_dir)
analysis["score"], analysis["grade"] = calculate_score(analysis)
if args.json:
print(json.dumps(analysis, indent=2))
else:
print_report(analysis)
if __name__ == "__main__":
main()