PerformancePerformance budgets

100%

Performance budgets.

"Keep it fast" loses every argument with a feature, because it never says how fast or at whose expense. A budget is the same wish written as a number the build can check: this page may ship 150 KB of script, this route may take 2.5 seconds to show its main photo on the phone we design for. Once the number exists, a pull request that breaks it fails like a broken test, and the conversation moves from whether speed matters to what the new feature should replace.

Intermediate32 minUpdated 2 Oct 2026

Builds on Core Web Vitals as requirements and Designing for the device you don't own.

The idea.

A speed requirement only protects users if something checks it before release. A budget is that check, written down in units a build can measure.

Cobbleway sells bike parts at shop.cobbleway.example. Its riders shop from the trail head and the bus stop, so the team designs for a mid-range Android phone on a patchy 4G signal, and it has promised that the main photo on a product page appears within 2.5 seconds for three visits in four. That promise is the requirement. On its own it changes nothing, because no single change ever breaks it: a reviews widget adds a little, a new icon set a little more, and a year later the page takes four seconds and nobody can point at the commit that did it.

A budget fixes that by deciding the limits up front and checking them on every change. The team works out how many bytes fit inside the 2.5 seconds on the phone it designs for, divides those bytes between the parts of the page and the people who own them, and wires a check into continuous integration. A change that would spend more than its share now fails in review, while it is still one diff with one author, instead of surfacing months later as a slow page with a hundred suspects.

The point is not the number itself; it is that the number exists before the feature does. With a budget, adding something heavy becomes a trade the team makes on purpose: shrink something, remove something, or do not ship the new thing in that form.

Cobbleway's product page, 150 KB of script

Product page JSlimit 150 KB
  1. framework, width 45, Framework runtime, value 45, UI library and router; platform team
  2. shared, width 35, Shared app code, value 35, header, basket drawer, search box, data layer; web core team
  3. route, width 30, Product route chunk, value 30, gallery, variant picker, fitment checker; catalogue team
  4. 3rd party, width 25, Third-party tags, value 25, analytics and reviews widget; marketing owns this line
  5. headroom, width 15, Unspent headroom, value 15, kept unallocated for fixes and surprises
  • 135 KB shipped today, from 0 to 135
  • Framework runtime
  • Shared app code
  • Product route chunk
  • Third-party tags
  • Unspent headroom
  • Over budget
framework
UI library and router; platform team
shared
header, basket drawer, search box, data layer; web core team
route
gallery, variant picker, fitment checker; catalogue team
3rd party
analytics and reviews widget; marketing owns this line
headroom
kept unallocated for fixes and surprises
Start

As it starts. 3 steps follow.

Widths are KB of compressed JavaScript. Each slice has an owner. Step through to see a pull request that adds a carousel library to the product route: the route chunk grows past what is left and the page goes over.

The same year, with and without a budget

Without one
Every change is small and reasonable. Nobody owns the total, so it grows a few KB a sprint until a dashboard turns red long after the causes merged. Fixing it becomes a project that competes with features.
With one
The total has an owner and each slice a limit. The change that would break it fails in review with its author attached, and the fix is a trade inside one pull request: what does this replace?
Was this section helpful?

How it works.

Users feel timings, but a build can only cheaply count things. So a budget usually starts as a timing, gets converted into bytes on the phone and network you design for, and is then checked as bytes on every change and as timings on the routes that matter.

Three kinds of budget

What a budget can limit

KindLimitsCobbleway exampleChecked
Quality (what users feel)A user-centred metric at a percentile of real visits: the Core Web Vitals, or a rule-based score such as Lighthouse performanceLCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 at p75 on mobileIn the field, after release; a score can also be asserted in the lab
Milestone timingWhen a moment in the load happens on one fixed lab profile: LCP, First Contentful Paint, Total Blocking Timelab LCP ≤ 2.5 s, TBT ≤ 200 ms on Slow 4G with a 4x slower CPUIn CI, against a preview build, a few runs per route
QuantityHow much the page asks for: compressed bytes by type, number of requests, number of third-party scripts, size of one chunk≤ 150 KB of JS per page, ≤ 30 KB per route chunk, ≤ 3 third-party scriptsOn every build, in seconds, with no browser at all

The three kinds sit in a chain. The quality target is the promise to users, and only field data from real visits can tell you whether you kept it (what each vital means and why p75 is covered in Core Web Vitals as requirements). A milestone timing is the same idea measured on one repeatable lab profile, so it can run before release. A quantity is the cheapest proxy of all: counting the bytes in a build takes seconds and gives the same answer every time, so it can run on every commit.

Each step down the chain is faster to check and further from what users feel. That is why a working budget has more than one kind: quantity limits catch most regressions instantly, timing limits in the lab catch the ones bytes miss (a script that is small but slow to run, a photo requested too late), and the field target tells you whether the lab profile still resembles your users.

Working backwards from 2.5 seconds

To turn a timing into bytes, fix the network you design for and subtract everything that happens before the critical bytes can flow, plus what happens after they arrive. Cobbleway uses Lighthouse's default mobile profile, Slow 4G, as the lab stand-in for its rider on patchy 4G (choosing that profile is the subject of Designing for the device you don't own). At 1.6 megabits a second the pipe moves about 200 KB a second, and every round trip costs 150 ms.

How many bytes fit before the main photo

Assumptions
LCP target
2,500 msweb.dev "good" threshold, held at p75
Round trip on the lab profile
150 msLighthouse Slow 4G
Download throughput
200 KB/s1.6 Mbps ÷ 8, rounded
Server time to start the HTML
100 msassumption; cached product data
HTML document
20 KBassumption; compressed
Reserve for style, decode and paint after the last critical byte
250 msassumption
Other critical bytes: CSS 16, hero photo 70, one font 24
110 KBassumption; each has its own owner
Working
  1. Connection: DNS, TCP and TLS 1.3, one round trip each3 × rtt450 msfrom Round trip on the lab profile
  2. First byte of HTMLconnect + rtt + server = 450 + 150 + 100700 msfrom Connection: DNS, TCP and TLS 1.3, one round trip each, Round trip on the lab profile and Server time to start the HTML
  3. HTML fully arrivedttfb + html ÷ bw = 700 + 100800 msfrom First byte of HTML, HTML document and Download throughput
  4. Subresource bytes start flowing (one more round trip to request them)html-done + rtt950 msfrom HTML fully arrived and Round trip on the lab profile
  5. Critical bytes must land bylcp − render = 2,500 − 2502,250 msfrom LCP target and Reserve for style, decode and paint after the last critical byte
  6. Time window for critical bytes2,250 − 9501,300 msfrom Critical bytes must land by and Subresource bytes start flowing (one more round trip to request them)
  7. Bytes that fit in the window1.3 s × 200 KB/s260 KBfrom Time window for critical bytes and Download throughput
  8. Left for compressed JavaScriptbytes − other = 260 − 110150 KBfrom Bytes that fit in the window and Other critical bytes: CSS 16, hero photo 70, one font 24
What it means
  • Nearly half the 2.5 s is gone on overheads: 950 ms of round trips and HTML before the first CSS byte, and 250 ms of rendering at the end. Only 1.3 s is left for every critical byte, which is why the byte budget is so much smaller than it feels on office Wi-Fi.
  • Every input is a lever. A server that answers in 300 ms instead of 100 costs 40 KB of script; a 140 KB hero photo instead of 70 costs 70 KB. The budget is one total that the page's parts trade against.
  • This is a lab calculation for a typical visit on the reference profile, not the field p75. Real visits vary around it, so Cobbleway keeps 15 KB of the 150 unallocated as headroom.

The product page load on Slow 4G, against the 2.5 s line

Scenario 1 of 3: As described.

Timeline as a list

The product page load on Slow 4G, against the 2.5 s line: 7 lanes, from 0 ms to 3,200 ms.

  1. 0–450 ms · Connection (DNS, TCP, TLS) · 3 round trips
  2. 450–800 ms · HTML · 20 KB
  3. 700 ms · HTML · first byte
  4. 950–1,030 ms · CSS (16 KB) · 80 ms
  5. 950–2,250 ms · all lanes · window: 1.3 s window for critical bytes
  6. 1,030–1,705 ms · JavaScript · 135 KB · 675 ms
  7. 1,705–1,825 ms · Font (24 KB) · 120 ms
  8. 1,825–2,175 ms · Hero photo (70 KB) · 350 ms
  9. 2,175–2,425 ms · Style, decode, paint · 250 ms
  10. 2,425 ms · Style, decode, paint · LCP 2.43 s (ok, ok)
  11. 2,500 ms · all lanes · LCP target 2.5 s (deadline)
Simplified: the browser interleaves these downloads, but they share one 200 KB/s pipe, so their total is what moves the photo. Bars are drawn one after another in the order they finish. Switch tabs to see the page at its limit and with the carousel pull request.

Which bytes, and the second bill

"150 KB of JavaScript" is ambiguous until you say which 150. The window above is about the network, so the budget counts compressed bytes, the ones that cross the wire. But tools disagree on what they count by default, and a limit copied from one tool into another can be off by a factor of three or four, because minified script usually shrinks to a fraction of its size under Brotli. Write the unit next to every limit.

Bytes on the wire are also only the first bill for script. After it arrives, the browser decompresses it, parses and compiles it, and runs it, mostly on the main thread that also handles taps; the V8 team names download and execution as the dominant costs, and a median phone runs the same code several times slower than a high-end one. Why a kilobyte of script costs far more than a kilobyte of photo is taught in Bundle size and what it costs; for a budget the consequence is simple: script gets its own byte limit, and a timing limit on main-thread work backs it up.

What each number measures

NumberWhat it countsWho reports it by defaultUse it for
Transfer sizeBytes received over the network for a response, compressed, including headersLighthouse resource-summary, DevTools Network panelByte budgets derived from a network window
Compressed file sizeThe built file run through Brotli or gzip, measured without a browsersize-limit (Brotli unless told otherwise)Fast per-chunk checks on every build
Emitted sizeThe minified file as the bundler writes it, before compressionwebpack performance hints (250,000 bytes per asset and per entry point unless set)A rough guard; convert before comparing with a wire budget
Main-thread timeMilliseconds the CPU spends parsing, compiling and running script during loadLighthouse Total Blocking Time and bootup-time, on a throttled CPUThe second bill that bytes alone do not show

Cobbleway's second bill, on the reference phone

Script work per compressed KB
~5 ms
Cobbleway's own reading from a trace of today's bundle on its reference phone; an illustrative figure, not a general rate
At the 150 KB limit
~750 ms
of main-thread work during load, spread over several tasks
Lab limit on Total Blocking Time
200 ms
the part of that work in tasks long enough to block a tap

Splitting the total

A single page-wide number tells you when you are over, not who should fix it. So the total is divided along the lines the code is already divided: the framework runtime every page loads, shared application code (header, basket, search), one chunk per route, and third-party tags. Each slice gets a limit and an owning team, and the slices plus a deliberate headroom add up to the total. A route that loads less shared code may spend more on its own chunk; the total per page is what the network window allows.

Third-party scripts belong inside the budget, not beside it. Analytics, a reviews widget or a chat bubble cross the same pipe and run on the same main thread as your own code; leaving them out makes the budget look healthy while the page is not. Because another company writes them, their slice is usually a count plus a byte ceiling, with marketing or whoever adds them as the owner.

Two levels per limit

A warning before the wall

LevelCobbleway product page JSWhat happens
Warning≥ 140 KBThe check passes but comments on the pull request. The owning team plans a trim before it becomes urgent
Error> 150 KBThe check fails and the pull request cannot merge until something is traded or an owner approves an exception
Was this section helpful?

In practice.

Cobbleway's budget as the team actually runs it: a limit per route and per slice, two checks in CI, and the pull request that tested it. Then how to say all of this in an interview.

Where the page's bytes come from

One product page, four owners

135 KB of script in total. The gallery, rotor picker and fitment checker are the catalogue team's 30 KB route chunk; the header and basket are shared code; the star rating comes from a third-party reviews widget.

As shipped. Store “Cobbleway”, showing the product view. Cart button “Basket” with badge 1 (its accessible name says “Cart, 1 item”, not the number alone). Product (showing) “Ridgeline hydraulic disc brake set” by “Front and rear, mineral oil”: rating 4.5 (“89 reviews”); price £149.00 (was £172.00, struck through) “Save £23”. Gallery: image 1 of 4 (“Brake caliper and lever, side view”); thumbnails beside it. Rotor size (tiles, a radio group): 160 mm, 180 mm (selected), 203 mm. Quantity 1 (stepper “Quantity”). Buttons: “Add to basket”. Delivery: “Free delivery Thursday”. Folded sections: Fits these bikes, Specs, Reviews.

  1. Route chunk: gallery and zoom (catalogue team)
  2. Route chunk: rotor picker and fitment check
  3. Third-party: reviews widget (marketing)
  4. Shared app code: header and search (web core)
  5. Shared app code: basket store and drawer

Cobbleway's budget per route (lab, Slow 4G, 4x CPU)

RouteJS per pageRoute chunkThird-partyLab LCP / TBTWhy it differs
/≤ 140 KB≤ 20 KB≤ 2 scripts, 20 KB2.5 s / 200 msLarge promo photo as the LCP element takes more of the window
/c/:category≤ 150 KB≤ 30 KB≤ 2 scripts, 20 KB2.5 s / 200 msFilters and the product grid; thumbnails are small
/p/:product≤ 150 KB≤ 42 KB≤ 3 scripts, 25 KB2.5 s / 200 msRoute chunk raised from 30 by rebalancing headroom after PR #2207
/basket≤ 130 KB≤ 20 KB≤ 1 script, 10 KB2.5 s / 150 msTight TBT: quantity steppers must answer at once
/checkout≤ 130 KB≤ 20 KBpayment SDK only, 30 KB2.5 s / 150 msNo marketing tags; the payment provider's script is the only third party

Enforcing it in CI

Two checks, at two speeds. The first runs size-limit against the built chunks on every push: no browser, a few seconds, and it names the chunk that grew. The second deploys a preview, loads each key route with Lighthouse CI on the mobile profile several times, and asserts on transfer bytes, the number of third-party requests and the lab timings. Byte checks catch most regressions early and cheaply; the timing check catches what bytes cannot, such as a small script that runs for a long time, or a photo that is now requested late.

Lighthouse 12 removed its own budget.json, so limits that used to live there now go into Lighthouse CI assertions. An error-level assertion makes the run exit non-zero and fails the status check; warn-level ones are printed and let the build through, which is how Cobbleway implements its two levels. How many runs to take, which run an assertion judges, and how to keep a shared runner from producing false alarms belong to Lab data, field data and releases.

Cobbleway's budget, as configuration

[
  { "name": "framework", "path": "dist/js/framework-*.js", "limit": "45 kB" },
  { "name": "shared app", "path": "dist/js/app-*.js", "limit": "35 kB" },
  { "name": "route: product", "path": "dist/js/route-product-*.js", "limit": "42 kB" },
  { "name": "route: category", "path": "dist/js/route-category-*.js", "limit": "30 kB" },
  { "name": "route: checkout", "path": "dist/js/route-checkout-*.js", "limit": "20 kB" },
  {
    "name": "product page, first-party total",
    "path": ["dist/js/framework-*.js", "dist/js/app-*.js", "dist/js/route-product-*.js"],
    "limit": "125 kB"
  }
]
{
  "ci": {
    "collect": {
      "url": [
        "https://preview.cobbleway.example/p/ridgeline-hydraulic-brake-set",
        "https://preview.cobbleway.example/checkout"
      ],
      "numberOfRuns": 5
    },
    "assert": {
      "assertMatrix": [
        {
          "matchingUrlPattern": ".*/p/.*",
          "assertions": {
            "resource-summary:script:size": ["error", { "maxNumericValue": 150000 }],
            "resource-summary:third-party:count": ["error", { "maxNumericValue": 8 }],
            "largest-contentful-paint": ["error", { "maxNumericValue": 2500, "aggregationMethod": "median" }],
            "total-blocking-time": ["error", { "maxNumericValue": 200, "aggregationMethod": "median" }],
            "categories:performance": ["warn", { "minScore": 0.9, "aggregationMethod": "median" }]
          }
        },
        {
          "matchingUrlPattern": ".*/checkout$",
          "assertions": {
            "resource-summary:script:size": ["error", { "maxNumericValue": 130000 }],
            "resource-summary:third-party:count": ["error", { "maxNumericValue": 4 }],
            "total-blocking-time": ["error", { "maxNumericValue": 150, "aggregationMethod": "median" }]
          }
        }
      ]
    }
  }
}

A few details make this configuration honest. The size-limit total covers only first-party chunks, because third-party scripts are not in the build; Lighthouse sees them on the loaded page, so the page-wide script total and the third-party requests are asserted there. That count covers every request Lighthouse attributes to a third party, not only script files: Cobbleway's three product-page tags make eight requests between them on a typical load (scripts, a stylesheet, beacons), so the assertion caps eight requests, and checkout's payment SDK makes four. Lighthouse CI wants bytes, so 150 KB becomes 150,000 and 130 KB becomes 130,000. That matches size-limit, which reads "kB" as 1,000 bytes ("KiB" would be 1,024); pick one convention and write it in the file. And the warn level on the overall score is deliberate: a score blends several metrics and moves for reasons nobody can act on, so it informs rather than blocks.

What happens to pull request #2207

What happens to pull request #2207, as an ordered list of steps:
What happens to pull request 220713 steps between Catalogue team, CI build, size-limit, Lighthouse CI (preview), Budget owner (web core). The steps are listed as text after the diagram.Budget owner (web core)Lighthouse CI (preview)size-limitCI buildCatalogue teamoptions: optimise, remove, or don't addpush: add carousel to product route1built chunks2route: product 58 kB of 30 · FAIL (seconds)3preview URLs, 5 runs each4script 163 KB of 150 · LCP median 2.57 s · FAIL5push: drop zoom widget, defer swipe code6route: product 42 kB of 30 · FAIL; first-party total 122 of 125 · PASS7script 147 KB of 150 · LCP median 2.49 s · PASS8request: move 12 KB of headroom into the product route9approved: route limit 30 → 42 kB, page total stays 15010push: .size-limit.json updated in the same PR11all chunks PASS · ready to merge12
  1. Catalogue team → CI build: push: add carousel to product route
  2. CI build → size-limit: built chunks
  3. size-limit → Catalogue team (reply): route: product 58 kB of 30 · FAIL (seconds)
  4. CI build → Lighthouse CI (preview): preview URLs, 5 runs each
  5. Lighthouse CI (preview) → Catalogue team (reply): script 163 KB of 150 · LCP median 2.57 s · FAIL
  6. Note over Catalogue team: options: optimise, remove, or don't add
  7. Catalogue team → CI build: push: drop zoom widget, defer swipe code
  8. size-limit → Catalogue team (reply): route: product 42 kB of 30 · FAIL; first-party total 122 of 125 · PASS
  9. Lighthouse CI (preview) → Catalogue team (reply): script 147 KB of 150 · LCP median 2.49 s · PASS
  10. Catalogue team → Budget owner (web core): request: move 12 KB of headroom into the product route
  11. Budget owner (web core) → Catalogue team (reply): approved: route limit 30 → 42 kB, page total stays 150
  12. Catalogue team → size-limit: push: .size-limit.json updated in the same PR
  13. size-limit → Catalogue team (reply): all chunks PASS · ready to merge
01
PR #2207 is 13 KB over: what the team does
Chosen:Trade inside the PR, then rebalance headroom into the route
  • Pro:The page total stays at 150 KB, so the LCP promise is untouched
  • Pro:The cost is paid by the feature that caused it: the zoom widget it replaces goes, and swipe code waits for a swipe
  • Pro:The headroom move is visible, reviewed and owned
Downside we accept:
  • Con:Only 3 KB of headroom left for the next surprise
  • Con:Deferred swipe code means the first swipe waits for a small download
Ruled out:Raise the page total to 165 KB

The 2.5 s derivation no longer holds; the lab LCP is about 2.57 s; Sets the precedent that every approved feature raises the limit

Ruled out:Block the feature until a later redesign

Leaves a real product improvement on the table; budgets are meant to force a trade, not to freeze the page

Saying it in an interview

Q1
RequirementsThe interviewer asks for non-functional requirements of a product page. What do you say about performance?
I'd state the promise, the device and the gate in one breath: LCP within 2.5 s and INP within 200 ms at p75 on mobile, designed for a mid-range Android on a slow 4G profile, and a budget that CI enforces, roughly 150 KB of compressed script per page, split between framework, shared code, the route and third-party tags. Then I'd move on; the number is there so later design choices can be checked against it.
Q2
DerivingWhere does a number like 150 KB come from?
From the timing, backwards. On a 150 ms, 1.6 Mbps profile, connection and HTML take most of a second and rendering needs a few hundred milliseconds at the end, so only about 1.3 s is left for critical bytes, around 260 KB. Take away CSS, the hero image and a font and roughly 150 KB is left for script. I'd say the inputs out loud so the interviewer can challenge them.
Q3
Design choiceThe interviewer suggests a rich 3D product viewer. How do you respond?
Check it against the budget before agreeing. It cannot sit on the first load, so it loads when the shopper taps 'View in 3D', with a static image holding its place; its code gets its own lazy chunk with its own limit, and the first-load total stays where it was.
Q4
EnforcementHow would you make sure the page stays fast after launch?
Two gates before merge: a per-chunk size check on every build, and Lighthouse CI on key routes against a preview with error-level assertions on script bytes, third-party count, LCP and TBT. After release, field p75 from real users per device class is the source of truth; if field and lab drift apart, the lab profile or the budget gets revisited.

Where the systems' flows spend a budget

FlowWhat competes for the budgetHow the budget shapes it
amazon/product-detail-pageGallery, variant picker, reviews, recommendations and third-party tagsBuy box and gallery inside the first-load limit; reviews and recommendations as lazy chunks with their own limits; tags counted on the page
amazon/product-listingFilters, a grid of thumbnails, sort and pagination codeMany small images share the window with script, so the byte budget is split between images and JS, not set for JS alone
airbnb/listing-pagePhoto tour, map, date picker, booking cardMap and date picker load on intent; the booking card's script is in the route chunk because it must answer quickly
netflix/home-rowsRow components, artwork, autoplay previewsA tight first-load limit for the shell and first rows; preview playback code outside it
facebook/news-feedFeed renderer, composer, reactions, adsComposer and rich reactions as lazy chunks; ad scripts as a third-party slice with a count limit
Was this section helpful?

Trade-offs.

A budget is a social contract as much as a config file. Set it too tight and people route around it; never revisit it and it quietly stops meaning anything.

The life of one budget line

The life of one budget line. 6 states, 10 transitions. The table below lists them.
The life of one budget line6 states, 10 transitions. The table below lists them.

team signs off / add error-level check

merge leaves › 10 KB slack / lower limit

size crosses warn level / comment on PR

a PR trims the slice

PR exceeds the limit / fail status check

PR exceeds the limit / fail status check

PR removes or defers bytes

owner approves [expiry set] / raise limit

expiry date passes / restore limit

route deleted / remove check

Proposed

Enforced

Near the limit

Breached

Temporary exception

Retired

3 steps. The carousel pull request: one change jumps from well inside the limit straight past it, with no warning on the way.

One limit, such as the product route chunk. Notice the two ways out of a breach (trade or a time-boxed exception) and the self-loop that ratchets the limit down after a win.

Transitions of The life of one budget line
From → ToEventGuardActionActor
Proposed → Enforcedteam signs offadd error-level checkweb core
Enforced → Enforcedmerge leaves > 10 KB slacklower limitbudget owner
Enforced → Near the limitsize crosses warn levelcomment on PRCI
Near the limit → Enforceda PR trims the sliceowning team
Near the limit → BreachedPR exceeds the limitfail status checkCI
Enforced → BreachedPR exceeds the limitfail status checkCI
Breached → EnforcedPR removes or defers bytesPR author
Breached → Temporary exceptionowner approvesexpiry setraise limitbudget owner
Temporary exception → Enforcedexpiry date passesrestore limitCI
Enforced → Retiredroute deletedremove check
Proposedstart
Derived from the timing target; not yet enforced
Enforced
Error-level check on every build
Near the limit
Warn level crossed; still merging
Breachederror
A pull request fails the check
Temporary exception
Raised limit with an owner and an expiry date
Retiredend
The route or chunk no longer exists

The same saving, with and without a ratchet

  • Fixed limit
  • No ratchet
  • Ratcheted limit
  • With ratchet
The same saving, with and without a ratchetWithout a ratchet the 19 KB saved in release 3 is spent again by release 11; with one, the lowered limit keeps the page near 130 KB.11012013014015016024681012polyfill and icon-set trimlimit lowered to 135Fixed limitNo ratchetRatcheted limitWith ratchetCompressed JS per page (KB)ReleaseThe same saving, with and without a ratchetWithout a ratchet the 19 KB saved in release 3 is spent again by release 11; with one, the lowered limit keeps the page near 130 KB.11012013014015016024681012polyfill and icon-set trimlimit lowered to 135Fixed limitNo ratchetRatcheted li…With ratchetCompressed JS per page (KB)Release
Illustrative: two versions of Cobbleway's product page over twelve releases, both trimmed by 19 KB in release 3. Only one team lowered its limit afterwards.
Data
ReleaseFixed limit (KB)No ratchet (KB)Ratcheted limit (KB)With ratchet (KB)
1150147150147
2150146150146
3150127150127
4150131135129
5150135135131
6150138135132
7150141135129
8150143135131
9150145135132
10150147135130
11150148135132
12150149135131
  • At 3: polyfill and icon-set trim
  • At 4: limit lowered to 135
01
What the merge gate checks
Chosen:Bytes on every build, lab timings on key routes, field p75 after release
  • Pro:Byte checks are instant and deterministic, so they can block without false alarms
  • Pro:Lab timings catch slow-but-small code and late-requested images
  • Pro:The field tells you whether the lab profile still matches your users
Downside we accept:
  • Con:Three systems to keep running and in agreement
  • Con:Byte limits are proxies that must be re-derived when the network profile or page shape changes
Ruled out:Bytes only

Blind to expensive code: a 5 KB script that runs for 300 ms passes; Blind to ordering, such as a hero photo discovered late

Ruled out:Lab timings only

Noisy from run to run, so tight limits cause flaky failures; Slow, and the failure says what got worse but not which chunk caused it

How budgets go wrong

FailureImpactDetectionMitigationMeanwhile
Limits set so tight that ordinary work failsTeams learn to bypass the check, mark code as lazy that loads immediately anyway, or disable the jobExceptions requested every sprint; checks skipped or muted in CI historyStart from today's measured size plus a small margin and ratchet down, rather than starting from an ideal numberThe page is no worse than before the budget existed
Exceptions that never expireThe limit drifts upward one approval at a time until it no longer protects the 2.5 s promiseCompare the current limit with the derived one at each quarterly reviewEvery raise carries an owner and an expiry date in the config, and the check fails when the date passesUsers get slower pages with no single change to blame
Third-party tags added outside the buildA tag manager injects a new script in production; no pull request ever sees itA scheduled Lighthouse CI run against production asserting the third-party request count and script totalGive marketing a third-party slice with a count limit and an allowlist in the tag managerThe page pays for the tag until the scheduled run flags it
Budget measured in the wrong unitA wire budget copied into a tool that counts uncompressed bytes is several times too strict, or the reverse lets bloat throughA size that passes one tool and fails anotherWrite the unit and the compression next to every limit, and derive each tool's number from one sourceEither needless failures or a gate that does not bite
Only the home page is budgetedProduct and checkout pages, where money is made, grow uncheckedField p75 by route shows the unbudgeted routes worstBudget every route template that matters to revenue, not just the landing pageRegressions found by users instead of CI
This topic
Kinds of budget and how they relate
Deriving a byte budget from a timing on a reference profile
Splitting it across routes, owners and third parties
Enforcing it in CI and keeping it meaningful (levels, exceptions, ratcheting)
Elsewhere
What each vital measures and why p75Core Web Vitals as requirements
Choosing the reference device and networkDesigning for the device you don't own
Why script bytes cost more than image bytes, and how to shrink bundlesBundle size and what it costs; Code splitting
Lab run variance, aggregation and comparing releases in the fieldLab data, field data and releases
Percentile mathsLatency percentiles
Was this section helpful?
Related
How fast is fast enough
Read next