Minimal fast-loading web page

Frontend Performance Budgets: Ship Less JavaScript

How to set meaningful performance budgets for JavaScript bundles, enforce them in CI, and make trade-off decisions when budgets are exceeded.

performance-budgetjavascriptbundle-sizeci

The average web page in 2026 ships over 500 KB of compressed JavaScript. On mid-range mobile devices with 4G connections, that translates to several seconds of parse and execute time before the page becomes fully interactive. Performance budgets are the engineering discipline that keeps this under control.

This article is for frontend engineers and technical leads who want to set realistic budgets, enforce them in CI, and maintain them as features grow. It builds on the performance foundations covered in the web fundamentals path and complements the INP checklist and event handler fixes we published earlier.

The web development hub links to additional JavaScript resources if you need to review patterns for code organisation and module design.

What a performance budget actually is

A performance budget is a quantified limit on one or more metrics that the team agrees not to exceed. Budgets work because they turn abstract goals ("the site should be fast") into concrete, enforceable numbers.

Common budget targets:

  • Total JavaScript size (compressed): the bytes shipped to the browser
  • Main-thread blocking time: cumulative time the main thread is occupied during load
  • Largest Contentful Paint (LCP): time until the main content is visible
  • INP: responsiveness after load

Start with the metric that matters most for your users. For JavaScript-heavy applications, total JS size and main-thread blocking time are usually the right starting points.

Setting your initial budget

Baseline from field data

Pull your current p75 metrics from CrUX, your RUM provider, or Lighthouse CI. These are your starting numbers. A budget should be an improvement over baseline, not an aspirational fantasy.

Competitive benchmark

Measure 3–5 competitor or peer sites. Your budget should target the best performer in your category, not the average.

Device and network targets

Define the target device and connection. A reasonable 2026 baseline is a mid-range Android phone on a 4G connection (roughly 10 Mbps download, 50 ms RTT). Test on this profile, not on your development MacBook.

Recommended starting points

For a content-focused site in 2026:

  • Total JS (compressed): ≤ 150 KB
  • Total CSS (compressed): ≤ 50 KB
  • LCP: ≤ 2.5 s
  • INP: ≤ 200 ms (aim for ≤ 100 ms)
  • Total page weight: ≤ 800 KB

For a JavaScript-heavy application, the JS budget may be higher (200–300 KB), but the INP and LCP targets should remain the same.

Enforcing budgets in CI

A budget that is not enforced is just a suggestion. Integrate budget checks into your build pipeline so that every pull request is validated before merge.

Build-time bundle analysis

Use your bundler's built-in analysis (webpack's --json, Vite's rollup-plugin-visualizer, or similar) to output bundle sizes. Compare against your budget thresholds in a CI step.

Lighthouse CI

Lighthouse CI can run against a preview deployment and fail the build if any metric exceeds the budget. Configure it with a lighthouserc.json file that defines assertions for each metric.

Custom budget scripts

For teams that need fine-grained control, write a script that reads the build output, sums JavaScript and CSS sizes per route, and exits with a non-zero code if any route exceeds its budget.

Strategies for shipping less JavaScript

Audit third-party scripts

Third-party scripts (analytics, consent banners, chat widgets, ad tags) often account for 40–60% of total JS. Audit each one:

  • What does it cost in bytes and main-thread time?
  • Is it still actively used?
  • Can it be loaded later (after the page is interactive)?
  • Can it be replaced with a lighter alternative?

Remove anything that is no longer providing value. Defer everything that is not needed for the initial render.

Tree-shake and dead-code eliminate

Modern bundlers tree-shake unused exports, but only if your code uses ES module syntax. Check for:

  • CommonJS require() calls that defeat tree-shaking
  • Barrel files (index.ts that re-export everything) that pull in unused modules
  • CSS-in-JS libraries that generate runtime JavaScript for styles

Code-split by route

Only load the JavaScript needed for the current page. Use dynamic import() for routes, heavy components, and large libraries. Preload critical chunks that the user is likely to navigate to next.

Replace heavy libraries with smaller alternatives

Common swaps that save significant bytes:

  • Full moment.js → native Intl.DateTimeFormat or date-fns (tree-shakeable)
  • Full lodash → lodash-es with direct imports or native array methods
  • Heavy charting libraries → lightweight SVG-based alternatives for simple visualisations

Maintaining budgets over time

Budget reviews

Schedule a quarterly review. As features grow, budgets may need adjustment—but always trade one thing for another. Adding a new widget means removing or deferring something else.

Ownership

Assign budget ownership to a specific team or individual. Without clear ownership, budgets drift and eventually get ignored.

Visibility

Display current budget metrics on a dashboard that the whole team can see. Make regressions visible immediately, not buried in CI logs.

Trade-offs

  • Aggressive code splitting increases request count. More chunks mean more HTTP round trips. Use HTTP/2 or HTTP/3 multiplexing and preload hints to mitigate this.
  • Removing third-party scripts may impact business metrics. Get stakeholder buy-in before removing analytics or marketing tools. Present the performance cost in business terms (conversion rate, bounce rate).
  • Very tight budgets slow feature development. Set budgets that are ambitious but achievable. The goal is to make performance a first-class design constraint, not to block all progress.

Further reading on EBooks-Space