Why the performance budget goes in the contract
A performance target that lives in a ticket loses to a marketing tag by sprint 3. Anyone with a reason can close a ticket. Put the number in the contract instead, and whoever wants to spend it has to come and ask.
A budget is a number, not a wish
Most performance targets fail because they are wishes. "The site should feel fast" cannot be breached, so it cannot be enforced. A budget works only when it names quantities a machine can check on its own.
So it specifies bytes first. A total transfer size for the landing page and for a typical content page, split by type: HTML, CSS, JavaScript, images, fonts, third-party. That split looks like bookkeeping until the remedy clause, where each line gets an owner. Then requests, which cost you even when they are multiplexed: header frames, cache lookups, main-thread processing. Each extra origin pays its own DNS, TCP and TLS setup.
Then one timing metric, written as a full sentence with its conditions. Largest Contentful Paint under 2.5 seconds, on a mid-range Android phone, on a throttled 4G connection, cold cache, on the staging URL. All of that is arguable at signing time and none of it afterwards. A number measured on a MacBook Pro on office fibre is a compliment you pay yourself.
Treat that metric differently from the other 2. Bytes and requests are exact, so a single run can gate a deploy. A lab timing number moves between runs on a build that changed nothing. Name a run count and a statistic: the median of 5 runs inside a stated tolerance band. Otherwise the first false alarm is why somebody switches the check off.
Where the number comes from
A budget nobody can derive is a hole in the contract, so work backwards from the timing metric. A slow 4G profile in the common lab tools sits around 1.6 Mbit/s with a 150ms round trip, which is 200KB per second. Nothing moves until the connection opens, so subtract DNS, TCP, TLS and the server's response time. Call it half a second.
That leaves about 2 seconds of transfer under a 2.5 second target, or roughly 400KB. It covers the whole critical path: HTML, render-blocking CSS, any font that blocks text, and the hero image. Everything below the fold spends from a second, looser figure.
Every breach arrives through a convenient door
Nobody ships a 6MB page on purpose. It arrives in pieces, each reasonable on the day, each approved by somebody not looking at the total.
Marketing tags are the usual first breach. An analytics script goes in, then a tag manager to make the next one easier. The tag manager is the problem: it turns adding a third-party script into a task that never touches the repository or the review. The container grows for a year without a single commit to the site.
The same shape repeats elsewhere. A CMS image is heavy because the upload path has no gate, and the uploader never saw the number. A type family grows because adding a weight is a design decision that never reaches an engineer. Name the door in each case, because the weight is only the symptom.
So one check is not enough. A per-deploy check on staging covers what the repository controls: bundles, images, request count, font files. It cannot see a container that grows without a commit. A second check runs on a schedule against production, daily or weekly, on the same device and profile. It catches tag containers, CMS uploads and injected widgets.
Both cadences belong in the contract. A budget checked quarterly can sit breached for 3 months before anyone looks.
The remedy clause names an owner for each line
The contract decides, in advance and in calm conditions, who pays for the argument later. Without a clause, marketing wants a tag, the page gets slower, and the client asks the studio why. The answer arrives after the complaint, so it sounds like an excuse.
The clause turns on who breached, and that is rarely self-evident. One deploy can carry studio work, client developers, a CMS upload and a new tag. This is where the split by type earns its keep. JavaScript and CSS belong to the studio, the third-party line to whoever adds tags, the media line to whoever uploads.
The breached line names the party, so the remedy follows. A studio breach is fixed at no charge, capped at agreed hours, excluding anything the client signed off. A client breach is billable work. A third-party breach moves the script off the critical path by default, deferred or loaded on consent, rather than deleted. Each remedy carries a window of 5 working days, after which the change reverts.
The strongest objection is that the tag is often worth its cost. Sometimes it plainly is, and the clause does not claim speed always wins. It claims the trade gets made once, in writing, by a named person, with the price on it. That is what a written variation is for.
Agree the number before anything is drawn
A budget measured after launch is a post-mortem with a number in it. The expensive decisions are made at design approval, in a form that hides their cost.
A full-bleed photographic hero, a 4-weight type family, a carousel of 8 product shots. None of those looks like a budget line in a design review, and all of them are one. Ask for a cut afterwards and you are asking somebody to give up work they already sold internally.
The hard part is that a designer cannot price a comp. So publish rough figures beside the budget, from your own stack. Being wrong by a factor of 10 is what you are preventing.
- A full-bleed hero image, modern format, compressed: low hundreds of KB.
- Each extra font weight, subset to woff2: tens of KB.
- A map embed or chat widget: hundreds of KB of third-party JavaScript.
- An autoplaying background video: megabytes, competing with the content.
Priced like that, the number shapes the design while it is still cheap to change. A page with 400KB on the critical path picks one hero image over a slideshow, one variable font over a family. Those are good outcomes, reached by constraint rather than by taste.
It also changes who is in the room. The person who wants the tag manager has to ask before the site is built, the only moment when the answer is cheap. That conversation is uncomfortable exactly once, instead of every quarter for the life of the site.
