You inherit a frontend app whose main bundle is already several times larger than any budget you would sensibly set. How do you introduce bundle-size budgets without turning the build red on day one?
answer
- not worse than today
- down freely, up with review
- commit the baseline to the repo
- tolerance for build noise
- the ratchet needs a destination
basics
~20 sSet the initial baseline at today's measured size plus a small tolerance, so the check fails only on regression, then ratchet it downward automatically whenever a build comes in under. Keep a separate, user-derived target as the destination the ratchet is heading toward.
solid answer
~50 sStart by measuring, not by legislating. Commit today's size as the baseline plus a small tolerance for build noise, so the check passes immediately and fails only when someone makes things worse — that gets the gate in place with no negotiation. Then make it a *ratchet*: when a build comes in under the baseline, the baseline moves down to the new value automatically; it never moves up without an explicit, reviewed change to the committed file. That way every improvement is locked in and cannot silently erode. Separately, write down the real target — derived from a load-time goal on the device and network profile you care about — so the ratchet has a destination and people can see the gap. Keep the baseline in a committed file so every change to it shows up in a diff with an author and a reason.
code
bash · 7 linesBUDGET=$(cat .bundle-budget)
SIZE=$(brotli -q 11 -c dist/main.js | wc -c | tr -d ' ')
if [ "$SIZE" -gt "$BUDGET" ]; then
echo "main.js ${SIZE}B exceeds baseline ${BUDGET}B"
exit 1
fi
echo "main.js ${SIZE}B within baseline ${BUDGET}B"go deeper
Know that a size check can compare against a stored baseline rather than an ideal number, and that failing only on increases is what makes it adoptable on an existing codebase.
Explain the ratchet's asymmetry — down automatically, up only through a reviewed change to a committed file — and why a tolerance is needed for build-to-build noise.
Show the rollout sequence and the operational details: report deltas on pull requests, ratchet on the main branch only, fix build non-determinism instead of widening tolerance, and publish a user-derived target so the ratchet has somewhere to go.
Own the framing that makes the effort fundable: the gap between today's baseline and a target derived from a real device and load-time goal is a visible, trackable commitment, not a background chore.
## Why an aspirational number fails on day one The instinct is to pick the number the app *should* hit and enforce it. On a legacy codebase that makes the build red for weeks while nobody can ship, so the check gets disabled or set to warn — and a warning that fires on every build is noise nobody reads. The budget is dead before it has caught anything. The alternative accepts a weaker but immediately enforceable claim: **not worse than today**. That is enforceable from the first commit, it needs no negotiation, and it stops the bleeding while the real work happens. ## The ratchet A ratcheting baseline has one defining property: **it moves down freely and up only with review.** - Store the current baseline in a committed file (a small JSON or text file, per artifact). - On every build, measure and compare. Over baseline plus tolerance → fail. - Under baseline → the improvement becomes the new baseline, written back automatically (a bot commit on the main branch, or a step in the release job). The asymmetry is the whole design. Improvements get locked in immediately, so the twenty kilobytes someone shaved off cannot be quietly re-spent next sprint by someone else. Increases require editing a tracked file, which means a diff, an author, and a reviewer asking why. ```bash BUDGET=$(cat .bundle-budget) SIZE=$(brotli -q 11 -c dist/main.js | wc -c | tr -d ' ') if [ "$SIZE" -gt "$BUDGET" ]; then echo "main.js ${SIZE}B exceeds baseline ${BUDGET}B" exit 1 fi ``` Apply the ratchet on the main branch, not on feature branches: a branch build that happens to be small should not lower the number for everyone before it merges. ## Tolerance and noise A byte-exact comparison is too brittle. Real builds vary for reasons unrelated to anyone's change: a dependency's patch release, a compiler or minifier version bump, non-deterministic module ordering, changed content hashes appearing inside a manifest. Allow a small tolerance — a fixed number of bytes or a low single-digit percentage — chosen so ordinary noise passes and a real addition does not. If you find yourself widening the tolerance repeatedly, the problem is usually build non-determinism rather than the budget; pin dependency versions and lock the toolchain rather than loosening the gate until it stops meaning anything. A two-level setup helps here: warn on any increase at all, fail only past the tolerance. The warning surfaces small creep in review without blocking, and the failure catches the real regressions. ## Reporting the delta, not just the verdict The check is far more useful when it reports the **change** rather than a pass/fail: "main.js +12.4 KB brotli (+9%) versus baseline". A number attached to a specific change is actionable while the author still has the context; a red X three days later is not. If your CI can surface that on the pull request, the budget starts changing behaviour before it ever fails, because people see the cost of their own change. ## The ratchet needs a destination A ratchet alone only guarantees monotone non-worsening — it will happily sit at the inherited size forever if nobody actively improves anything. So pair it with a target that is *not* derived from the current code: - pick a device and network profile that represents your real users, - pick a load-time goal for the critical route, - work backwards to a byte figure the route may not exceed. That target is the destination; the ratchet is how you walk there without backsliding. Publishing both — "currently 410 KB, target 200 KB" — turns the gap into a visible, trackable piece of work rather than a vague wish, and gives you the argument for scheduling reduction work at all. ## Sequencing the rollout In practice: measure and report only for a week or two so people see the numbers; then turn on failure at today's size plus tolerance; then enable the downward ratchet; then publish the target and start closing the gap. Each step is uncontroversial on its own, which is why the sequence tends to survive contact with a team that has other priorities.
- Why should the baseline only be lowered automatically, never raised automatically?Automatic lowering locks in improvements so they cannot be silently re-spent. Automatic raising would make the baseline follow whatever shipped, which converts the gate into a size recorder — every regression becomes the new normal without anyone deciding it was acceptable. Raising has to leave a diff with an author and a reason.
- The size check keeps failing on unrelated pull requests because builds vary slightly. What do you do?Diagnose the variance before loosening anything. Unpinned dependency ranges, toolchain version drift, and non-deterministic module ordering are the usual causes, and pinning them is the real fix. Use a modest tolerance for irreducible noise, but a tolerance wide enough to swallow genuine regressions has stopped being a budget.
- How would you pick the eventual target number rather than just ratcheting forever?Derive it from users, not from the code: choose a representative device and connection, choose a load-time goal for the critical route, and work backwards to the bytes that fit. That gives a number you can defend in a product conversation, and it makes the gap between today's baseline and the target a schedulable piece of work.
saying these in an interview costs you the question
- Sets the aspirational number on day one and disables it when red
- Lets the baseline update automatically in both directions
- Keeps the baseline in CI settings rather than in the repo
- Widens the tolerance whenever the check gets noisy
- Ratchets forever with no user-derived target