Journal · Feb 10, 2026

The 100-point budget

Lighthouse 100 isn't a trophy — it's a budget you spend. Where the points actually go in 2026, and how we structure a Next.js site so the score survives contact with the marketing team.

Every few months someone sends us a screenshot of four green 100s and asks how long it took. The honest answer is that hitting 100 on launch day is the easy part. Any competent team can do it with a fresh codebase and nothing on the page. The hard part is still being at 100 eighteen months later, after the analytics tags, the chat widget, the hero video, and the CMS full of unoptimized uploads.

That's why we stopped treating Lighthouse as a score to chase and started treating it as a budget to spend. The distinction changes everything about how you build.

A score is a snapshot. A budget is a policy.

A score describes one moment. A budget describes a policy: here is how much performance headroom this site has, here is what each feature costs against it, and here is who has to say yes before we spend more.

When you frame it that way, the conversation with a client stops being "why did the score drop?" and becomes "the chat widget costs roughly 8 points of mobile performance — is it worth that?" Sometimes it is. A support widget that closes deals can absolutely be worth 8 points. But it should be a decision, made once, on the record — not an accident discovered in a quarterly audit.

The budget mindset also forces an uncomfortable admission: a site at 100 has spent nothing yet. It has no fat to trim later. Every future addition is a withdrawal, so the launch-day architecture needs to leave room for the withdrawals you already know are coming.

Where the points actually go in 2026

The line items have shifted over the years. Render-blocking jQuery bundles are mostly gone. Here's what actually drains the budget on the sites we're asked to rescue now.

Unsized media. Still, in 2026. An img without width and height (or a container without aspect-ratio) forces layout when the bytes arrive, and CLS eats it. The fix costs nothing at build time and is nearly impossible to retrofit through a CMS after a thousand uploads. Every image on our sites goes through next/image or gets explicit dimensions — no exceptions, enforced by lint.

Third-party scripts. The single biggest line item on most audits we run. Tag managers are the sneaky part: the manager itself is small, but it's a door that marketing can push arbitrary JavaScript through without a deploy. We load anything non-essential with next/script on the lazyOnload strategy, route what we can through a first-party proxy, and put a hard cap in writing: total third-party JavaScript stays under 150 KB compressed, and anything above that needs a named owner and a reason.

Blocking fonts. Web fonts still block first paint when they're loaded from a third-party origin with default settings. Self-hosting through next/font removes the extra connection, subsets the files, and inlines the @font-face declarations with sensible fallback metrics:

import { Space_Grotesk } from "next/font/google";

const spaceGrotesk = Space_Grotesk({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-space-grotesk",
});

That's the whole intervention. Fonts stop appearing in the waterfall as a blocking request, and the layout-shift penalty from fallback swapping drops to nearly zero because Next.js calculates size-adjusted fallbacks for you.

Hydration overweight. The modern one. Server components render on the server for free; the moment a file says "use client", everything it imports ships to the browser and executes there. The failure mode we see constantly: one interactive tab strip marked as a client component at the page level, dragging a charting library and forty components into the bundle with it. The rule is boring and absolute — client components sit at the leaves, take serializable props, and import as little as possible. Interactivity is an island, not a continent.

Spending on purpose

A real budget has line items decided up front. Ours, for a typical marketing site on mid-tier mobile hardware:

  • Total JavaScript: under 200 KB compressed on first load, including third parties.
  • LCP element: an image or heading that's discoverable in the initial HTML — never something that waits for JavaScript. Priority-loaded, properly sized, no carousel.
  • Third parties: the 150 KB cap above, loaded lazy, audited quarterly.
  • Fonts: two families, subset, self-hosted. The third family is a design conversation, not a CSS edit.

Notice these are input budgets, not output scores. You can't ship a pull request that "decreases LCP" — you can ship one that adds 60 KB of JavaScript, and an input budget catches it at review time instead of in the field data three weeks later.

Holding the line after launch

The mechanism that makes the budget real is enforcement you don't have to remember to run.

{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.95 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 204800 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.05 }]
      }
    }
  }
}

Lighthouse CI runs on every pull request against the built preview. If a change blows the script budget, the build fails and the conversation happens before merge — with the person who made the change, while they still have context. That's a fundamentally different dynamic than an audit that assigns blame months later.

Two more habits that matter as much as the tooling. First, we watch field data (CrUX and real-user monitoring), not just lab runs — a 100 in the lab with a p75 LCP of four seconds in the field means the lab is testing the wrong page or the wrong network. Second, every third-party script gets an expiry date. When the pixel for last spring's campaign is still loading in November, nobody remembers why. Ours get reviewed or removed on a schedule, like credentials.

The part nobody wants to hear

None of this is clever. There's no secret plugin, no obscure header, no trick. Holding a 100 is almost entirely the discipline of refusing small conveniences: the un-lazied embed, the quick tag-manager addition, the client component that was faster to write. Each costs two or three points. The site that's slow in two years got there three points at a time, with nobody deciding anything.

Treat the score as a budget, give every point a named owner, enforce it in CI, and the 100 stops being a launch-week photo op. It becomes the default state of the system — which is the only version of it that's worth anything.