performance-budget-guard — Claude Code hook: bundle size and deploy
A Claude hook that measures your Next.js JS bundle after a build and blocks the deploy when it goes over the performance budget.
npx claude-code-templates@latest --hook performance/performance-budget-guardWhat performance-budget-guard does
The hook fires on PostToolUse after npm run build, next build, vercel build or yarn build. On a successful build it sums every .js file under .next/static/chunks and compares the total against a 350KB limit. Under budget, it prints the headroom and warns when less than 50KB is left. Over budget, you get the overage, the five heaviest chunks and a list of suggestions, then the script exits with code 2 — the deploy does not go through.
The second trigger covers file edits through Write, Edit and MultiEdit. It looks at .js, .jsx, .ts and .tsx outside node_modules and flags things as you type: a file over 100KB, more than 15 imports, a wildcard import, a full lodash import instead of a per-function one, moment anywhere in the file. Those are notes, not blocks.
One thing to know before you install: the 350KB limit is hardcoded in the script, there is no per-project setting. The size check also depends on a .next directory, so a build that does not produce one gets a message that the folder is missing. The script uses the macOS stat form with a GNU fallback, so it runs on Linux and in CI as well.
What it helps with
- catch a bloated bundle right after the build instead of in production
- stop a deploy that pushes JS past the budget
- list the heaviest chunks under
.next/static/chunks - spot a full
lodashormomentimport in a file you just edited - see when less than 50KB of budget is left
Example prompts for the agent
- build the project and tell me whether we are still inside the bundle budget
- check that my edit to
app/page.tsxdid not pull in heavy imports - run
npm run buildand break down which chunks are the largest
How to install performance-budget-guard
1claude-code-templates
npx claude-code-templates@latest --hook performance/performance-budget-guard2Manually: put the files in place
.claude/settings.jsonThe hooks block: event (PreToolUse, PostToolUse, Stop…) → command. Personal ones go to ~/.claude/settings.json
Step by step, with commands for each platform: Claude Code hooks: what they are, where to configure them, how to block a risky command
Questions about performance-budget-guard
How do I install performance-budget-guard in Claude?
Run npx claude-code-templates@latest --hook performance/performance-budget-guard, then check the entry in .claude/settings.json.
What does performance-budget-guard do?
It measures the JS bundle after a Next.js build and compares it to a 350KB limit. When you go over, it prints a report and blocks the deploy.
When does the hook run?
On PostToolUse for npm run build, next build, vercel build and yarn build, and for edits to .js, .jsx, .ts and .tsx files made with Write, Edit or MultiEdit.
Source text
performance-budget-guard.json · MIT
{
"description": "Monitor bundle size and Core Web Vitals metrics during development, blocking deployments that exceed performance budgets with detailed reports. Automatically analyzes Next.js build output, checks bundle sizes against predefined budgets, and provides optimization recommendations. Hook triggers on PostToolUse for build-related operations and file changes that could affect performance.",
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'input=$(cat); COMMAND=$(echo \"$input\" | jq -r \".tool_input.command // empty\"); SUCCESS=$(echo \"$input\" | jq -r \".tool_response.success // false\"); if [ \"$SUCCESS\" = \"true\" ] && [[ \"$COMMAND\" =~ (npm\\ run\\ build|next\\ build|vercel\\ build|yarn\\ build) ]]; then echo \"📊 Performance Budget Guard: Analyzing build output...\"; if [ -d \".next\" ]; then BUNDLE_SIZE=$(find .next/static/chunks -name \"*.js\" -exec stat -f%z {} + 2>/dev/null | awk \"{total += \\$1} END {print total}\"); BUNDLE_SIZE_KB=$((BUNDLE_SIZE / 1024)); BUDGET_LIMIT=350; echo \"📦 Total bundle size: ${BUNDLE_SIZE_KB}KB\"; if [ $BUNDLE_SIZE_KB -gt $BUDGET_LIMIT ]; then echo \"🚨 PERFORMANCE BUDGET EXCEEDED!\"; echo \"Current bundle size: ${BUNDLE_SIZE_KB}KB\"; echo \"Budget limit: ${BUDGET_LIMIT}KB\"; echo \"Overage: $((BUNDLE_SIZE_KB - BUDGET_LIMIT))KB\"; echo \"\" >&2; echo \"⚠️ Performance budget exceeded by $((BUNDLE_SIZE_KB - BUDGET_LIMIT))KB!\" >&2; echo \"\" >&2; echo \"📋 Bundle Analysis:\" >&2; find .next/static/chunks -name \"*.js\" -exec ls -lah {} + 2>/dev/null | sort -k5 -hr | head -5 >&2; echo \"\" >&2; echo \"💡 Optimization recommendations:\" >&2; echo \"• Use dynamic imports for large components\" >&2; echo \"• Implement code splitting with next/dynamic\" >&2; echo \"• Check for duplicate dependencies\" >&2; echo \"• Optimize third-party libraries\" >&2; echo \"• Run: npm run analyze for detailed bundle analysis\" >&2; exit 2; else echo \"✅ Bundle size within budget: ${BUNDLE_SIZE_KB}KB / ${BUDGET_LIMIT}KB\"; REMAINING=$((BUDGET_LIMIT - BUNDLE_SIZE_KB)); echo \"🎯 Remaining budget: ${REMAINING}KB\"; if [ $REMAINING -lt 50 ]; then echo \"⚠️ Warning: Less than 50KB remaining in performance budget\"; fi; fi; else echo \"❌ No .next build directory found. Run build first.\"; fi; else echo \"ℹ️ Performance check skipped (not a build command or failed build)\"; fi'",
"timeout": 30
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash -c 'input=$(cat); FILE_PATH=$(echo \"$input\" | jq -r \".tool_input.file_path // empty\"); SUCCESS=$(echo \"$input\" | jq -r \".tool_response.success // false\"); if [ \"$SUCCESS\" = \"true\" ] && [[ \"$FILE_PATH\" =~ \\.(js|jsx|ts|tsx)$ ]] && [[ ! \"$FILE_PATH\" =~ node_modules ]]; then echo \"🔍 Performance Guard: Checking code changes in $FILE_PATH...\"; if [ -f \"$FILE_PATH\" ]; then FILE_SIZE=$(stat -f%z \"$FILE_PATH\" 2>/dev/null || stat -c%s \"$FILE_PATH\" 2>/dev/null); FILE_SIZE_KB=$((FILE_SIZE / 1024)); if [ $FILE_SIZE_KB -gt 100 ]; then echo \"⚠️ Large file detected: ${FILE_SIZE_KB}KB\"; echo \"💡 Consider splitting large components or lazy loading\"; fi; IMPORTS_COUNT=$(grep -c \"^import\" \"$FILE_PATH\" 2>/dev/null || echo \"0\"); if [ $IMPORTS_COUNT -gt 15 ]; then echo \"📦 Many imports detected: $IMPORTS_COUNT imports\"; echo \"💡 Consider consolidating imports or tree-shaking unused code\"; fi; if grep -q \"import.*\\*.*from\" \"$FILE_PATH\" 2>/dev/null; then echo \"🚨 Wildcard import detected in $FILE_PATH\"; echo \"💡 Use specific imports instead of wildcard imports for better tree-shaking\"; fi; if grep -q \"moment\" \"$FILE_PATH\" 2>/dev/null; then echo \"📅 Moment.js usage detected\"; echo \"💡 Consider using date-fns or native Date for smaller bundle size\"; fi; if grep -q \"lodash\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"lodash/\" \"$FILE_PATH\" 2>/dev/null; then echo \"🔧 Full Lodash import detected\"; echo \"💡 Use specific Lodash functions: import debounce from \\\"lodash/debounce\\\"\"; fi; echo \"✅ Performance check completed for $FILE_PATH\"; else echo \"❌ File $FILE_PATH not found\"; fi; else echo \"ℹ️ Performance check skipped (not a JavaScript/TypeScript file or failed operation)\"; fi'",
"timeout": 15
}
]
}
]
}
}
© davila7 · MIT · davila7/claude-code-templates