Enforcing performance budgets in Webpack CI: failing the build before users pay for it
performance.maxAssetSize is the least of it. A real production optimization checklist - persistent caching, deterministic hashing, and a CI gate that fails on bundle-size regressions.
A performance budget that only exists as a number someone remembers in a design review gets violated within a month. A performance budget enforced in CI, that fails the build on regression, survives team turnover and deadline pressure. Here's how we wire that up around a Webpack production build, and the config that gets us there.
Step 1: performance.maxAssetSize, and why it's not enough alone
module.exports = {
performance: {
maxAssetSize: 250_000, // bytes
maxEntrypointSize: 400_000,
hints: 'error', // fail the build, don't just warn
},
};This catches absolute size violations - a single asset or entrypoint exceeding a hard ceiling - but it doesn't catch the more common failure: a bundle that was already close to budget growing 15% from a single dependency add, still under the ceiling, that quietly erases a quarter's worth of optimization work one PR at a time.
Step 2: a CI job that diffs bundle size against main
# .github/workflows/bundle-size.yml
- name: Build and compare bundle size
run: |
npx webpack --json > stats.json
npx bundlesize --config bundlesize.config.json{
"files": [
{ "path": "./dist/main.*.js", "maxSize": "180 kB" },
{ "path": "./dist/vendor.*.js", "maxSize": "220 kB" }
]
}Tools like `bundlesize` or `size-limit` post a PR comment with the delta against the base branch and fail the check if a named chunk crosses its budget - the review conversation becomes 'this PR added 40KB, is that intentional' instead of nobody noticing until a user complains about load time months later.
Step 3: persistent caching for faster CI builds, not just dev
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: { config: [__filename] },
},
};Webpack 5's filesystem cache persists the compiled module graph to disk between builds. Combined with a CI cache action that restores `node_modules/.cache/webpack` between runs, this turns a cold 90-second production build into a warm 20-second one on PRs that only touch a handful of files - the budget check runs faster, which means it gets skipped less often under deadline pressure.
Step 4: deterministic module IDs for long-term caching
module.exports = {
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
},
};Without deterministic IDs, an unrelated change elsewhere in the dependency graph can shift Webpack's internal numeric module IDs, which changes chunk hashes, which invalidates every user's browser cache for chunks that didn't actually change - a performance regression that never shows up in bundle-size CI because the bytes are identical, only the cache-busting hash changed. `moduleIds: 'deterministic'` hashes based on module path instead of build order, so unrelated changes stop cascading into unrelated cache invalidation.
A performance budget without a CI gate is a suggestion. Ship the gate, or the budget number is decorative.
Written by Appesto Engineering.