Performance Budgeting for Static Sites — Core Web Vitals Guide
In this tutorial, you'll learn about Performance Budgeting for Static Sites. We cover key concepts, practical examples, and best practices.
Performance budgeting sets hard limits on page weight, load time, and resource count — preventing regressions by failing builds that exceed thresholds and keeping static sites fast as they grow with new content and features.
What You'll Learn
Why It Matters
A single oversized image or unoptimized script can double your page load time, hurting Lighthouse scores, Core Web Vitals ratings, and search rankings. Without a performance budget, sites slowly degrade as teams add features, images, and scripts. Enforcing budgets in CI catches regressions before they reach production, protecting your user experience and SEO performance.
Real-World Use
A documentation site sets a 500KB JavaScript budget and a 1MB total page weight limit. When a developer adds a new syntax highlighting library, the CI build fails because the budget is exceeded. A marketing site enforces a 2.5-second Largest Contentful Paint (LCP) budget and blocks any pull request that drops below it.
Performance Budget Architecture
flowchart LR
A[New Feature] --> B[Build]
B --> C[Measure Metrics]
C --> D{Budget Check}
D -->|Pass| E[Deploy]
D -->|Fail| F[Block Merge]
F --> G[Developer Fixes]
G --> A
E --> H[Monitor Live]
H --> I{Degradation?}
I -->|Yes| J[Alert Team]
I -->|No| K[Maintain Budget]
style D fill:#f90,color:#fff
style F fill:#f44,color:#fff
style K fill:#090,color:#fff
What Is a Performance Budget?
A performance budget is a set of thresholds for key metrics that your site must not exceed. Breaking the budget means either optimizing the new addition or reconsidering its inclusion.
Common Budget Categories
| Category | Example Budget | Why It Matters |
|---|---|---|
| Total page weight | < 1MB | Slower downloads on mobile networks |
| JavaScript budget | < 300KB parsed | Parsing time delays interactivity |
| Image budget | < 500KB per page | Images are the largest contributor to page weight |
| HTTP requests | < 25 requests | Connection overhead on HTTP/1.1 |
| LCP | < 2.5 seconds | Largest Contentful Paint (Core Web Vital) |
| FID / TBT | < 100ms / < 50ms | First Input Delay (Core Web Vital) |
| CLS | < 0.1 | Cumulative Layout Shift (Core Web Vital) |
| Time to Interactive | < 3.5 seconds | When the page becomes usable |
Core Web Vitals Targets
Google considers these metrics for search ranking:
# Performance budget thresholds (Google's recommended targets)
metrics:
lcp:
good: 2.5s # Largest Contentful Paint
poor: 4.0s
fid:
good: 100ms # First Input Delay
poor: 300ms
cls:
good: 0.1 # Cumulative Layout Shift
poor: 0.25
inp:
good: 200ms # Interaction to Next Paint (2024+)
poor: 500ms
Enforcing Budgets with Lighthouse CI
Installation and Configuration
npm install -g @lhci/cli
// lighthouserc.json — Lighthouse CI configuration with budgets
{
"ci": {
"collect": {
"numberOfRuns": 3,
"staticDistDir": "./public",
"settings": {
"onlyCategories": ["performance", "accessibility", "seo"]
}
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 50 }],
"max-server-response-time": ["error", { "maxNumericValue": 600 }],
"uses-responsive-images": ["error", { "minScore": 1 }],
"modern-image-formats": ["error", { "minScore": 1 }],
"offscreen-images": ["error", { "minScore": 1 }],
"unminified-css": ["error", { "minScore": 1 }],
"unminified-javascript": ["error", { "minScore": 1 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
Expected behavior: Lighthouse CI builds the site, runs Lighthouse audits 3 times, and asserts that each metric meets the defined thresholds. If any assertion fails, Lighthouse CI exits with a non-zero code, causing the CI pipeline to fail.
GitHub Actions Integration
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.128.0'
extended: true
- name: Build site
run: hugo --gc --minify
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
Budget File with webpack
For sites using webpack-based SSGs (Next.js, Astro):
// performance-budget.js — webpack performance budgets
module.exports = {
configureWebpack: {
performance: {
hints: 'error',
maxEntrypointSize: 300000, // 300KB
maxAssetSize: 200000, // 200KB
assetFilter: function(assetFilename) {
return assetFilename.endsWith('.js') || assetFilename.endsWith('.css');
}
}
}
};
Tracking Budgets with bundlesize
// package.json — bundlesize budget configuration
{
"bundlesize": [
{ "path": "./public/**/*.js", "maxSize": "200 kB" },
{ "path": "./public/**/*.css", "maxSize": "100 kB" },
{ "path": "./public/**/main.*.js", "maxSize": "50 kB" }
],
"scripts": {
"test:size": "bundlesize"
}
}
Tools and Platforms Comparison
| Tool | Type | What It Measures | CI Integration | Free Tier |
|---|---|---|---|---|
| Lighthouse CI | Local audit | LCP, CLS, TBT, score | GitHub Actions, CircleCI | Yes |
| WebPageTest | Cloud test | Full waterfall | API, custom | Limited |
| bundlesize | File size | Asset byte sizes | npm script | Yes |
| SiteSpeed.io | Local + Cloud | Full metrics, video | Docker, plugins | Yes |
| Cloudflare Observatory | Edge CDN | Core Web Vitals from RUM | Dashboard | Yes (with CF) |
| Pagespeed Insights API | Google API | Lab + field data | REST API | Yes |
Optimization Strategies to Meet Budgets
Image Optimization
Images are the largest contributor to page weight. Use responsive images with modern formats like WebP:
{{/* Hugo — Responsive image with WebP */}}
{{ $image := resources.Get "images/hero.jpg" }}
{{ $webp := $image | resources.Fingerprint "md5" }}
{{ $image320 := $image.Resize "320x webp" }}
{{ $image640 := $image.Resize "640x webp" }}
{{ $image1280 := $image.Resize "1280x webp" }}
<picture>
<source srcset="{{ $image320.RelPermalink }} 320w,
{{ $image640.RelPermalink }} 640w,
{{ $image1280.RelPermalink }} 1280w"
sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 1280px"
type="image/webp">
<img src="{{ $image.RelPermalink }}"
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
alt="Hero image">
</picture>
CSS and JavaScript Optimization
# hugo.toml — Asset optimization
[build]
writeStats = true
[params]
[params.assets]
cssMinify = true
jsMinify = true
Font Optimization
/* Load only the characters you need */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF; /* Latin only */
font-display: swap;
}
Common Errors
1. Budget Too Strict (False Positives)
Setting budgets too aggressively (e.g., < 100KB total) causes constant build failures for legitimate additions. Start with Google's recommended thresholds, then tighten gradually based on real user monitoring data.
2. Budget Too Loose (No Benefit)
A 5MB budget on a static site provides no guardrails. The budget should be tight enough that a single unoptimized image or script addition would break it. Review budgets quarterly as site performance improves.
3. Not Differentiating by Page Type
A blog post and a dashboard page have different performance profiles. Set page-type-specific budgets using Lighthouse CI's --budget-file option, or segment by URL patterns.
4. Ignoring Third-Party Script Impact
Third-party scripts (analytics, widgets, fonts) often exceed the budget silently. Include them in your measurements and set specific budgets for third-party content. Use Resource Timing API to track them programmatically.
5. Not Monitoring Real User Data
Lab data (Lighthouse) and field data (Chrome User Experience Report) can differ. A page that passes Lighthouse budgets might still have poor real-world Core Web Vitals. Monitor both and set budgets for field data using the CrUX API.
Practice Questions
1. What are the three Core Web Vitals metrics and their recommended thresholds?
LCP (< 2.5s), FID (< 100ms), and CLS (< 0.1). Starting in 2024, INP replaces FID with a threshold of < 200ms.
2. How does Lighthouse CI enforce a performance budget?
Lighthouse CI runs Lighthouse audits and compares each metric against defined thresholds using assertions. If any assertion fails (e.g., LCP > 2.5s), Lighthouse CI exits with an error, which blocks the CI pipeline.
3. What is the difference between lab data and field data?
Lab data is collected in a controlled environment (Lighthouse, WebPageTest) with consistent network and device conditions. Field data comes from real users (Chrome UX Report) and reflects actual device capabilities and network conditions.
4. Why is image optimization the highest-leverage performance improvement?
Images account for 50-70% of total page weight on most sites. Converting to WebP/AVIF, serving responsive sizes, and lazy-loading offscreen images can reduce page weight by 60-80% with a single change.
5. Challenge: Set up performance budgets for a Hugo site using Lighthouse CI. Configure budgets for LCP (< 2.5s), CLS (< 0.1), TBT (< 50ms), total page weight (< 1MB), and JavaScript (< 300KB). Integrate into a GitHub Actions workflow that blocks pull requests exceeding any budget.
Mini Project: Performance Budget Dashboard
Create a performance budget system for a static site:
- Define budget thresholds in a
lighthouserc.jsonfile - Integrate Lighthouse CI into your GitHub Actions pipeline
- Set up budges for both lab data (Lighthouse) and field data (CrUX API)
- Add a GitHub status check that shows pass/fail for each metric
- Create a performance dashboard page that displays current metrics against budgets using the CrUX API
Budget configuration template:
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 50 }],
"total-byte-weight": ["error", { "maxNumericValue": 1000000 }],
"uses-responsive-images": ["error", { "minScore": 1 }]
}
}
}
}
Test the system by adding a 2MB unoptimized image to a page, pushing a branch, and verifying that the CI pipeline fails. Then optimize the image and confirm the pipeline passes.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro