September 7, 2025 7 min read by Mykola Samila

I spent a weekend optimizing this portfolio site — the one you're reading right now — from a mediocre Lighthouse score to consistent 95+ across all categories. Here's a frank breakdown of what made a real difference, ordered by impact.

What Core Web Vitals actually measure

Core Web Vitals are three metrics Google uses as part of its ranking signal since 2021. They measure real user experience, not synthetic benchmarks:

Google's ranking boost from good Core Web Vitals is real but modest — it's a tiebreaker, not a primary signal. The more important reason to care: these metrics directly correlate with whether users stay on your page.

LCP: the biggest win

LCP is almost always caused by one of two things: a large above-the-fold image loading slowly, or render-blocking resources delaying first paint. On my portfolio, it was the hero image.

1. Preload the LCP image

The browser doesn't know your hero image is critical until it parses the HTML and discovers the <img> tag. By then, it's already loaded stylesheets and fonts. A preload hint tells the browser immediately:

<!-- In <head>, before any stylesheets -->
<link
  rel="preload"
  as="image"
  href="images/hero-640.webp"
  imagesrcset="images/hero-640.webp 1280w, images/hero-768.webp 768w"
  imagesizes="50vw"
  fetchpriority="high"
/>

Add fetchpriority="high" to the <img> element itself as well. This dropped my LCP from 3.8s to 1.4s on its own.

2. Serve modern image formats

WebP is 25–34% smaller than JPEG at equivalent quality. AVIF is even smaller but has less browser support. Use WebP with a JPEG fallback:

<picture>
  <source
    type="image/webp"
    srcset="hero-640.webp 640w, hero-1280.webp 1280w"
    sizes="50vw"
  />
  <img
    src="hero-1280.jpg"
    alt="Hero image"
    width="1280"
    height="853"
    fetchpriority="high"
  />
</picture>

3. Always specify width and height on images

Without explicit dimensions, the browser reserves no space for the image. When it loads, the layout shifts. This hurts both CLS and LCP. With dimensions, the browser allocates space immediately.

CLS: easier than it looks

CLS is usually caused by content shifting as assets load. The fixes are mechanical:

My CLS was 0.18 — over the 0.1 threshold — entirely because of a Google Fonts load causing a font swap. Switching to font-display: swap and preloading the font brought it to 0.02.

<!-- Preconnect before any font CSS loads -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Load fonts asynchronously to avoid render blocking -->
<link
  href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;700&display=swap"
  rel="stylesheet"
  media="print"
  onload="this.onload=null;this.removeAttribute('media');"
/>

INP: the new kid

INP measures the delay between a user interaction (click, key press, tap) and the next visual update. Most static HTML sites have excellent INP naturally. JavaScript-heavy apps are where it suffers.

Common causes on React/SPA sites:

On this portfolio, the contact form had a Turnstile (CAPTCHA) widget that blocked interaction for ~200ms. Loading it deferred fixed it.

Fonts are performance killers

Google Fonts is convenient but adds two DNS lookups, two connections, and a network round-trip for the CSS file before any font bytes download. The asynchronous loading pattern eliminates the render-blocking behavior:

<!-- This pattern loads fonts without blocking render -->
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Raleway:wght@600;800&display=swap"
  media="print"
  onload="this.onload=null;this.removeAttribute('media');"
/>
<noscript>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?..." />
</noscript>

The media="print" trick makes the browser load the CSS without blocking render. The onload handler switches it to media="all" once loaded. Include a <noscript> fallback for browsers without JavaScript.

For maximum performance, self-host fonts using @font-face with local WOFF2 files. This eliminates the third-party DNS lookup entirely. Tools like google-webfonts-helper make this easy.

What didn't work

I also tried things that looked good in theory but didn't move the metrics:

Final scores

After all optimizations, running Lighthouse on a throttled mobile connection:

The 80% of the gains came from: preloading the hero image, serving WebP, setting image dimensions, and async-loading fonts. Everything else was marginal.

Want a performance audit on your site?

Get in touch

← All articles  ·  Portfolio