#!/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 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()