All posts SEO & Marketing

Core Web Vitals: How Speed Became an SEO Problem

Core Web Vitals really asks three simple questions: when did the page show up, did it stay put after it appeared, and did it answer when you touched it. Here are the typical mistakes that break these three metrics, and the concrete fixes I applied on the sites I shipped.

DT
Demir Taşdemir Mobile App & Web Developer
— min read

Last year a client told me, “the site has everything, but we’re not moving up on Google.” The content wasn’t bad and the headings were where they should be. I opened it on my phone: first a blank white screen, then the logo, then the cover image dropped in and every bit of text below it jumped down at once. I tried to tap the menu, and the button did nothing for a while. The problem wasn’t the content; it was the way the page pulled itself together.

Core Web Vitals measures exactly that: through the user’s eyes, when did the page appear, did it stay put after it appeared, did it respond when touched. In this post I’ll go through the three metrics, the typical mistakes that break them, and what I changed on the sites I shipped.

Three metrics, three separate questions

Core Web Vitals consists of three measurements, and none of them stands in for another:

  • LCP (Largest Contentful Paint) — When was the largest content element in the viewport painted? The “good” threshold is 2.5 seconds.
  • CLS (Cumulative Layout Shift) — How much did content move around while the page loaded? It’s a unitless score; the upper bound considered good is 0.1.
  • INP (Interaction to Next Paint) — After the user clicks or taps, how long until the first visual response on screen? The good threshold is 200 milliseconds. INP replaced FID in March 2024.

The second part is what matters: these values don’t come from a single visit but from field data generated by real users, and they’re evaluated at the 75th percentile. So three out of every four visitors need to clear the threshold. The data Chrome collects accumulates over a rolling 28-day window; that’s what the Core Web Vitals report in Search Console shows you.

Lab and field are not the same thing

Scoring 100 in Lighthouse is not the same as looking “good” in field data. Lighthouse measures once, on your own machine, over your own connection. Field data also includes real visits from old Android phones on weak mobile connections. I always look at both: lab to find the problem, field to see whether it actually got fixed.

LCP is usually an image problem

The LCP element is typically the cover image or the first large heading block. Splitting the duration into four parts makes it easier to see where you’re losing time: the server’s time to first byte, the browser’s delay in discovering that resource, the resource’s download time, and the render delay after it has been downloaded.

In practice, the mistakes I see most often are in the discovery step:

  • Putting loading="lazy" on the cover image. Lazy loading only makes sense for images below the fold; putting it on the topmost image means you’re delaying LCP with your own hands.
  • Setting the hero image as a background-image in CSS. The browser’s preload scanner can’t see that URL while reading the HTML; it has to download and parse the CSS first.
  • Adding the image to the page later with JavaScript. Same problem, one layer more delayed.
  • Large render-blocking CSS and synchronous script files sitting at the very top of the page.

To get the cover image downloaded early and with priority, you need to leave a hint for the browser:

<link rel="preload" as="image"
      href="/assets/kapak-1200.webp"
      imagesrcset="/assets/kapak-800.webp 800w, /assets/kapak-1600.webp 1600w"
      imagesizes="100vw">

<img src="/assets/kapak-1200.webp"
     srcset="/assets/kapak-800.webp 800w, /assets/kapak-1600.webp 1600w"
     sizes="100vw"
     width="1600" height="900"
     fetchpriority="high" decoding="async"
     alt="Ofis girişinde güvenlik danışma bankosu">

Sizing images correctly

There’s a detail in the example above that does two jobs at once: the width and height attributes. They don’t set the image’s on-screen size; CSS still handles that with max-width: 100%; height: auto. What they do is tell the browser the aspect ratio. The browser reserves an empty box of the right height before the image has even downloaded, so text doesn’t shift down when the image arrives. That one-line addition solves the bulk of CLS problems.

Other things I watch on the sizing side:

  • Serve at the real size. Shrinking a 4000-pixel-wide photo down to 600 pixels with CSS doesn’t shrink the file; the phone still downloads every one of those bytes.
  • Modern formats. WebP gives a noticeably smaller file than JPEG in most scenarios; AVIF compresses even better but is slow to encode. I make WebP the default for photos and use SVG for logos and icons.
  • Reserve space for embeds. Boxes that arrive late — maps, videos, ad slots — need an aspect-ratio or min-height in CSS.

CLS: the ground sliding out from under you

Layout shift is the thing users get most annoyed by and developers notice least. The reason is simple: we open the site on a fast connection with a warm cache, so everything arrives instantly. The shifting shows up on slow connections.

The causes are almost always the same:

  • Images and iframes with no declared dimensions.
  • An announcement bar or cookie banner that drops in at the top of the page later. Once those blocks enter the flow, they push everything below them down. I render them as a separate layer with position: fixed; they never touch the page flow.
  • Font swapping. Text drawn in the fallback font reflows when the real font arrives, because the line widths differ.
  • Animating properties like top, left and height. Using transform and opacity instead both prevents the shift and runs more smoothly.

Fonts: the sneakiest source of shift

Font loading on its own can affect both LCP and CLS. Pulling from a third-party font service adds the cost of a DNS lookup and a TLS handshake for a new domain. On the sites I ship, I serve fonts from my own server as woff2.

@font-face {
  font-family: "Inter";
  src: url("/assets/inter-var.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0100-024F, U+0130-0131, U+015E-015F, U+011E-011F;
}

/* Bring the fallback font's metrics closer to the real one */
@font-face {
  font-family: "Inter Yedek";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
}

Three details here matter. font-display: swap makes the text appear immediately in the fallback font, so the user isn’t staring at a blank screen. With unicode-range I declare the range covering the characters Turkish needs and keep the file small — ı, ş, ğ and capital İ live in the Latin Extended-A block, so sticking to the basic Latin range isn’t enough. The third is bringing the fallback font’s metrics closer to the real one with size-adjust and ascent-override: because line widths stay nearly identical when the font arrives, there’s no visible jump.

Critical CSS: not loading everything up front

CSS blocks rendering by default. The browser won’t paint a single pixel until it has downloaded and parsed the entire stylesheet. If you have one big stil.css file, the page’s first screen is also waiting on the styles for the footer and the contact form.

The fix is to inline the rules that paint the first screen into the HTML and load the rest without blocking:

<style>
  /* above the fold only: typography, top menu, hero */
  :root{--metin:#111}
  body{margin:0;font:16px/1.6 "Inter","Inter Yedek",sans-serif;color:var(--metin)}
  .ust-menu{display:flex;align-items:center;height:64px}
</style>

<link rel="stylesheet" href="/css/stil.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/stil.css"></noscript>

The media="print" trick tells the browser “this file isn’t needed for the screen right now”; it downloads without blocking and kicks in via onload. But don’t overdo it: the more CSS you inline, the more the HTML bloats, and that part never gets cached. I generally try not to go beyond a few kilobytes.

If you hand-write critical CSS, don’t forget to maintain it

When you change the design and forget to update the inline block, the first screen looks wrong for a moment and then corrects itself — meaning you’ve produced a layout shift with your own hands. That’s why I only inline rules that change very rarely.

INP: the silence after the click

LCP and CLS are about loading; INP is about what happens after the page is open. In the browser, JavaScript runs on a single main thread. While that thread is busy, your tap waits in the queue and the screen can’t update. That’s why any task over 50 milliseconds counts as a “long task.”

The typical things that break INP: doing heavy work on every single fire of a scroll or typing event, building a very large DOM tree in one go, and stuffing the page with third-party scripts that have nothing to do with its function. My approach is this: give the user visual feedback first, defer the heavy work. When a button is pressed I add the class and update the UI first, then start the calculation or the network request on the following frame. On long lists I use content-visibility: auto to postpone the rendering cost of the sections that aren’t visible.

What I changed on the sites I shipped

The fixes I applied on allianceguvenlik.com, drdoganuysal.com and my own site demirtasdemir.com were largely the same list:

ProblemWhat I didMetric affected
Cover photo was a raw JPEG at a single sizeConverted it to WebP, gave it a two-size srcsetLCP
loading="lazy" on every imageRemoved it from the above-the-fold image, added fetchpriority="high"LCP
No dimension attributes on imagesAdded width and height to all of themCLS
Fonts loaded from a third-party serviceMoved them to my own server, woff2 and preloadLCP, CLS
Cookie banner inside the page flowMoved it to a fixed-position layerCLS
Icon fontSwitched to inline SVGLCP, CLS

On WordPress projects, stripping out plugin CSS and slider files that load on every page but are never used on that page lifts a serious amount of weight all by itself. There’s also a classic mistake: leaving two plugins that do the same job both active.

I’m not going to put a percentage here and claim “it improved by this much.” Field data moves in a 28-day window, and during the same period both the content and the backlinks were changing; crediting a single number to these fixes wouldn’t be honest. What I can say is this: the “needs improvement” warnings in Search Console disappeared, and pages now open on mobile without jumping around.

One last bit of balance: speed alone won’t carry bad content up the rankings. Google doesn’t present page experience as a substitute for content either. But it is a layer that makes the difference between two results of similar strength and, more importantly, keeps the user from abandoning the site. That’s why I read Core Web Vitals not as an SEO score, but as a measure of the user’s patience.

  • SEO
  • Core Web Vitals
  • Performance
  • Web Development
  • CSS
Share: LinkedIn X WhatsApp
DT

Demir Taşdemir

Mobile App & Web Developer

I have been building software since 2018. I have published 11 apps on the App Store and Google Play; right now I am working on 6 mobile apps, 1 e-commerce platform and 1 desktop game.

Your site’s speed may be holding your rankings back

I can review your current site against Core Web Vitals and work out which fixes to make first. If you’re building a new site, I develop it with these metrics in mind from the start. Just write to me from the contact page.