ThanhDev
  • Articles
  • Categories
  • Tags
  • About

ThanhDev

Frontend engineering notes on React, Next.js, TypeScript and software architecture.

  • Articles
  • Categories
  • Tags
  • About
  • GitHub
  • LinkedIn
  • Website
  • Email

© 2026 ThanhDev. Built with Next.js, Prisma and PostgreSQL.

  1. Home
  2. Articles
  3. Measuring and Fixing Core Web Vitals
Architecture

Measuring and Fixing Core Web Vitals

LCP, CLS and INP explained through the fixes that actually move them, not the definitions you can already look up.

T

ThanhDev

Jul 4, 2026•2 min read•594 views

Lighthouse gives you a number. Field data tells you whether real users agree. Optimize for the second one.

LCP — how fast the main thing appears

The Largest Contentful Paint element is usually a hero image or a heading. Find it in DevTools before guessing at fixes.

The wins, in the order they usually pay off:

  1. Size the image. Always give width and height, or use fill with a sized parent.
  2. Do not lazy-load it. priority on the hero, lazy on everything below the fold.
  3. Serve modern formats. AVIF or WebP, at the size actually rendered.
  4. Preconnect to the image host if it is not your own origin.
tsx
<Image
  src={post.coverImage}
  alt={post.coverImageAlt}
  width={1200}
  height={630}
  priority
  sizes="(max-width: 768px) 100vw, 768px"
/>

CLS — how much the page jumps

Layout shift is almost always something arriving late without a reserved space: an image without dimensions, a font swap, an injected banner.

css
/* Reserve the space before the aspect ratio is known */
.cover { aspect-ratio: 16 / 9; }

For fonts, next/font handles the hard part — it self-hosts, preloads and computes a size-adjusted fallback so the swap does not reflow the page.

INP — how fast the page answers

Interaction to Next Paint replaced FID, and it is stricter: it measures the whole interaction, not just the delay before handling starts.

The usual culprits:

  • A long synchronous handler blocking the main thread.
  • Rendering an entire list on every keystroke.
  • Too much JavaScript hydrating at once.
tsx
// Keep the input responsive, let the expensive render lag behind
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(deferredQuery), [deferredQuery]);

The structural fix is smaller client bundles. Every component that does not need to be interactive and stays on the server is JavaScript that never competes for the main thread.

Measure in the field

ts
export function reportWebVitals(metric: NextWebVitalsMetric) {
  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));
}

A lab score on your laptop over fibre is not the experience you are shipping. Real devices on real networks are the only data that settles an argument.

  • #Performance
  • #Next.js
Share
PreviousWhat Five Years of Frontend Taught MeNextFrontend Architecture: Layers That Earn Their Keep

Related articles

  • Architecture

    PostgreSQL Indexes Every Developer Should Know

    B-tree, partial, composite and GIN — what each one is for, and how to tell whether the planner is using yours.

    • #PostgreSQL
    • #Performance
    Aug 8, 2026•2 min read•914 views
  • Architecture

    Frontend Architecture: Layers That Earn Their Keep

    Repositories, services and actions are useful right up to the point where they become ceremony. Where I draw the line.

    • #TypeScript
    • #Prisma
    • #Next.js
    Jul 18, 2026•2 min read•401 views
  • Architecture

    Designing a Prisma Schema That Scales

    Relations, indexes and cascade rules are decisions you make once and live with for years. Here is how I approach them.

    • #Prisma
    • #PostgreSQL
    • #TypeScript
    Aug 15, 2026•2 min read•793 views