Benefits of Performance Budgets in CI for Marketing Sites
Nobody files a bug for a multi-megabyte hero image. Someone swaps in the new campaign photo, the page still looks great on office Wi-Fi, and three weeks later the paid landing page is slow with no obvious culprit.
Performance budgets in CI close that gap. A budget is a limit on page weight or load timing, and Lighthouse CI checks it on every pull request, failing the build when a page crosses the line. Below is a working config, then five benefits a marketing site gets from it, and an honest note on when it isn't worth the trouble.
Lighthouse CI assertions: a budget config you can copy
Lighthouse CI loads your pages in headless Chrome and checks the results against assertions you define. Install it with npm install -g @lhci/cli@0.16.x, then put a lighthouserc.js at the repo root:
module.exports = {
ci: {
collect: {
startServerCommand: 'npm run start',
url: [
'http://localhost:3000/',
'http://localhost:3000/landing/spring-sale',
'http://localhost:3000/pricing',
],
numberOfRuns: 3,
},
assert: {
assertions: {
'largest-contentful-paint': ['error', { maxNumericValue: 2500, aggregationMethod: 'median-run' }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1, aggregationMethod: 'median-run' }],
'total-blocking-time': ['warn', { maxNumericValue: 300, aggregationMethod: 'median-run' }],
'resource-summary:image:size': ['error', { maxNumericValue: 400000 }],
'resource-summary:script:size': ['error', { maxNumericValue: 200000 }],
'resource-summary:third-party:count': ['warn', { maxNumericValue: 8 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
Three runs per URL, with median-run picking the most representative one, keeps a single slow run from failing a pull request. An error assertion makes lhci exit non-zero, while warn prints but passes. Byte limits are in bytes, so 400000 is roughly 400 KB. Treat these numbers as placeholders: measure a current good build and add some headroom.
Then run it on every pull request:
name: Performance budget
on: pull_request
jobs:
lhci:
name: Lighthouse CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Use Node.js 22.x
uses: actions/setup-node@v6
with:
node-version: 22.x
- name: Install and build
run: |
npm install
npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli@0.16.x
lhci autorun
lhci autorun runs collect, assert and upload in one command. One catch for campaign work: temporary-public-storage reports are visible to anyone with the link and deleted after a few days, so check you're comfortable with that before pointing it at an unannounced page.
1. A page speed regression fails the pull request
Without a gate, a slowdown is found by whoever notices first, usually a media buyer staring at a cost-per-click report. With the config above, the pull request that adds a new carousel library goes red with the failing audit named, while the change is still fresh in the author's head. Finding the culprit among one pull request beats finding it among a month of them.
2. Oversized images and tag scripts get caught before launch
resource-summary:image:size and resource-summary:script:size cap what the page downloads. resource-summary:third-party:count caps requests to other people's domains: chat widgets, heatmaps, ad pixels. "Can we add one more pixel?" is an easy yes until the count assertion turns it into a visible tradeoff in the review.
3. Core Web Vitals in CI protect landing page quality
Google's published thresholds call LCP under 2.5 s, CLS under 0.1 and INP under 200 ms "good", judged on real visits. A lab run never clicks anything, so it can't measure INP. Total blocking time is the nearest lab proxy, which is why the config warns on TBT instead.
This matters for paid traffic too. Google Ads rates landing page experience as one of three Quality Score components, and its help pages list improving load speed among the fixes when that rating is Average or Below average. Those thresholds describe field data and your CI run is a different measurement, so use them as a starting line and calibrate against your own baseline.
4. A number ends the design-versus-speed argument
Designers want the full-bleed video, developers want the page to load, and without a number it's taste against taste. With a budget the conversation gets concrete: the hero is 180 KB over the image limit, so compress it, swap it, or raise the limit. Raising a budget is allowed. It just has to happen in a pull request both sides can see, instead of as slow drift.
5. Different page types get different budgets
A paid landing page and a blog archive shouldn't share limits. Use assertMatrix instead of the flat assertions block to give each URL pattern its own rules (add a blog post URL to collect.url so the second pattern has something to match):
assert: {
assertMatrix: [
{
matchingUrlPattern: '.*/landing/.*',
assertions: {
'largest-contentful-paint': ['error', { maxNumericValue: 2000, aggregationMethod: 'median-run' }],
'resource-summary:third-party:count': ['error', { maxNumericValue: 5 }],
},
},
{
matchingUrlPattern: '.*/blog/.*',
assertions: {
'largest-contentful-paint': ['warn', { maxNumericValue: 3500, aggregationMethod: 'median-run' }],
},
},
],
},
The strict rules go where ad spend lands, and the loose ones keep the content team from fighting the gate.
When performance budgets in CI are not worth it
- The site rarely changes. If it ships a handful of times a year, a quarterly manual check does the job and a gate is more machinery than the problem deserves.
- CI only sees what you deploy. Tags published from a tag manager's own interface never pass through a pull request. Point a scheduled run at the live URLs, since the
urllist accepts remote addresses. - Lab runs are noisy. Start every assertion at
warnfor a couple of weeks, then promote toerroronce you trust the numbers. A gate that cries wolf gets deleted.
The same habit covers AI features on a marketing site: a chat assistant or personalization script is just another third-party request, and a budget is how you learn what it costs before your visitors do.