Home / Articles / Performance

Performance

Core Web Vitals in 2026: how we hit 100/100

The font-loading, image, and edge tactics behind sites that score perfect without a performance team.

By Hagumi Lab· Jan 2026· 7 MIN READ ·Leer en español →
A single minimalist analog gauge in cream and matte black with a teal needle resting at maximum, isolated on dark negative space.

TL;DR — Perfect Core Web Vitals on a static-first site come down to four boring disciplines: ship less JavaScript, self-host a subset of your fonts with a sane font-display, give every image explicit dimensions and lazy-load below the fold, and let a CDN serve pre-rendered HTML from the edge. Do those and 100/100 stops being luck.

We build marketing sites and product pages for clients who don’t have a performance team — and shouldn’t need one. A green Lighthouse score isn’t the goal in itself; it’s a proxy for a site that feels instant to a real person on a mid-range phone over spotty LTE. The good news: the tactics that move the needle are well understood and largely set-and-forget. Here’s the playbook we run on every static-first build.

LCP: the largest thing should arrive first

Largest Contentful Paint is almost always either a hero image or a heading rendered in a web font. Both are fixable.

For the hero image: serve it in a modern format (AVIF with a WebP fallback), size it for the actual rendered width, and preload it so the browser fetches it before it finishes parsing CSS. Don’t lazy-load anything above the fold — lazy-loading your LCP element is the single most common self-inflicted wound we see.

For fonts: self-host. Pulling fonts from a third-party origin adds a DNS lookup, a TLS handshake, and a connection you don’t control. Subset the font to the glyphs you actually use (Latin, maybe Latin-Extended), serve woff2, and preload the one or two weights that render above the fold.

<link rel="preload" href="/fonts/inter-subset.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-subset.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
}

font-display: swap shows fallback text immediately and swaps in the web font when it loads — no invisible-text delay counting against LCP.

CLS: nothing should move after it paints

Cumulative Layout Shift is caused by content that arrives late and shoves everything else around. Two fixes cover ~90% of cases.

First, every image and embed gets explicit width and height attributes (or a CSS aspect-ratio). The browser reserves the box before the pixels arrive, so text below it never jumps.

<img src="/hero.avif" width="1200" height="630" alt="..." loading="eager" fetchpriority="high">

Second, the font swap shouldn’t shift layout. Pick a fallback whose metrics are close to your web font, or tune them with size-adjust and ascent-override in the @font-face. The swap from fallback to web font should be nearly invisible — not a visible reflow.

A site that never moves after it paints feels trustworthy. Layout shift is the digital equivalent of a waiter bumping your table — small, but you notice every time.

INP and JavaScript: the less you ship, the faster you respond

Interaction to Next Paint measures how quickly the page reacts when someone taps or types. The enemy is a fat main thread choking on JavaScript it didn’t need to download.

Static-first wins here by default — there’s simply less JS to parse and execute. Our rules:

  1. Ship HTML, not a framework, wherever possible. A static page with sprinkles of vanilla JS beats a hydrated SPA for a content site every time.
  2. Defer or lazy-load non-critical scripts. Analytics, chat widgets, and embeds load after interaction or on idle — never blocking first paint.
  3. Avoid layout-thrashing on scroll. Use IntersectionObserver, not scroll-event listeners that read layout on every frame.
  4. Keep third parties on a leash. Every tag manager, A/B tool, and tracker is someone else’s JavaScript running on your main thread. Audit them quarterly and cut what isn’t earning its weight.

Edge and caching: serve the answer from nearby

Pre-rendered HTML cached at a CDN edge is the cheapest performance win there is. A request from a user in Miami should be answered from a node in Miami, not a round-trip to an origin server somewhere else.

The tactics:

  • Pre-render at build time so the edge serves a finished document, not one assembled per request.
  • Set long cache lifetimes on static assets (Cache-Control: immutable) and fingerprint filenames so a deploy invalidates only what changed.
  • Compress with Brotli and enable HTTP/2 or HTTP/3 — most CDNs do this for you.
  • Put images on the CDN too, ideally with on-the-fly format negotiation so AVIF goes to browsers that support it.

The takeaway

Performance is a budget you defend, not a fix you ship once. A site that scores 100/100 at launch will quietly erode as someone adds a marketing pixel, a heavier hero image, or one more font weight. Set a budget — a JS size ceiling, an image-weight limit, a Lighthouse floor in CI — and treat regressions as bugs. The hard part was never hitting the score. It’s staying there.

Written by Hagumi Lab

The engineering & R&D notebook of Hagumi Studio. We write what we learn building and self-hosting the tools behind our work.

HAGUMISTUDIO.COM · X · RSS

Stay in the loop

Get the Lab notes in your inbox.

One considered email a month — what we built, what broke, and what we’d do differently. No fluff.

No spam. Unsubscribe anytime · delivered by our self-hosted Listmonk.