We've all been there. You build a beautiful application, test it locally, everything feels snappy and responsive. Then you deploy it to production, check it on your phone over a 4G connection, and... it crawls. Users bounce. Your client isn't happy. Google penalizes your rankings.
After years of building with everything from Flask and FastAPI backends to Next.js, React, and Astro frontends, I've learned that understanding Core Web Vitals isn't just about appeasing Google's algorithms - it's about delivering genuinely exceptional user experiences. And honestly? It's become one of the most important skills in modern web development.
Core Web Vitals are Google's attempt to quantify user experience through measurable metrics. Think of them as the vital signs of your web application - like a doctor checking your pulse, blood pressure, and temperature. These metrics capture the most critical aspects of how users perceive performance.
The three main Core Web Vitals are:
Largest Contentful Paint (LCP) - Measures loading performance. Specifically, when the largest visible content element renders on the screen.
Interaction to Next Paint (INP) - Measures responsiveness. How quickly your site responds to user interactions throughout the entire page lifecycle. (This recently replaced First Input Delay)
Cumulative Layout Shift (CLS) - Measures visual stability. How much your content unexpectedly moves around while loading.
But the story doesn't end there. To truly understand performance, we also need to look at:
Let's dive deep into each one.
FCP marks when users first see something meaningful on their screens. For a site like tashif.codes
, this might be the header text, the navigation bar, or the beginning of a hero section.
Google's guideline is sub-1.8 seconds, but if you want to truly impress, aim for under 1 second.
On the backend, you want to send critical content as quickly as possible:
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import HTMLResponse
import asyncio
app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/", response_class=HTMLResponse)
async def home():
# Send the shell immediately with critical CSS inlined
html_shell = """
<!DOCTYPE html>
<html>
<head>
<style>
/* Critical CSS inlined */
.hero { font-size: 2rem; padding: 2rem; }
</style>
</head>
<body>
<div class="hero">Welcome to tashif.codes</div>
<!-- Defer non-critical resources -->
<link rel="stylesheet" href="/styles.css" media="print"
onload="this.media='all'">
</body>
</html>
"""
return html_shell
The key? Prioritize above-the-fold content and inline critical CSS. Everything else can wait.
LCP is usually the hero image, a large text block, or a video thumbnail. On tashif.codes
, if your portfolio hero image takes 4 seconds to load, that's your LCP - and you're failing Google's 2.5-second threshold.
Next.js makes LCP optimization almost trivial with its Image component:
import Image from "next/image";
export default function Hero() {
return (
<section className="hero">
<Image
src="/portfolio-hero.webp"
alt="Tashif's Portfolio"
width={1200}
height={600}
priority // This is crucial for LCP!
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
sizes="100vw"
/>
<h1>Welcome to tashif.codes</h1>
</section>
);
}
The priority
prop tells Next.js to preload this image. The blur placeholder gives users something to see immediately.
Astro takes a different angle with its image optimization:
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.webp';
---
<section class="hero">
<Image
src={heroImage}
alt="Portfolio Hero"
width={1200}
height={600}
loading="eager"
decoding="async"
quality={85}
/>
<h1>Welcome to tashif.codes</h1>
</section>
<style>
.hero {
/* Using CSS containment for better rendering */
contain: layout style paint;
}
</style>
Astro automatically optimizes images at build time, converting them to modern formats and generating multiple sizes.
INP is the newest Core Web Vital, and it's tough. It measures the latency of every user interaction throughout the page's entire lifecycle - not just the first one like the old FID metric.
Think about it: users don't just interact once. They click, type, scroll, hover. INP captures all of that.
import { useCallback, useMemo, startTransition } from "react";
function ProjectGallery({ projects }) {
const [filter, setFilter] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
// Memoize expensive computations
const filteredProjects = useMemo(() => {
return projects.filter((p) => {
const matchesFilter = filter === "all" || p.category === filter;
const matchesSearch = p.title.toLowerCase().includes(searchTerm);
return matchesFilter && matchesSearch;
});
}, [projects, filter, searchTerm]);
// Use startTransition for non-urgent updates
const handleFilter = useCallback((newFilter) => {
startTransition(() => {
setFilter(newFilter);
});
}, []);
// Debounce search to avoid blocking
const debouncedSearch = useMemo(
() => debounce((term) => setSearchTerm(term), 300),
[]
);
return (
<div>
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search projects..."
/>
<div className="filters">
{["all", "web", "mobile", "design"].map((cat) => (
<button key={cat} onClick={() => handleFilter(cat)}>
{cat}
</button>
))}
</div>
<div className="grid">
{filteredProjects.map((project) => (
<ProjectCard key={project.id} {...project} />
))}
</div>
</div>
);
}
The key techniques:
CLS is perhaps the most user-facing metric. You know that moment when you're about to click a button and an ad loads, shifting everything down, and you end up clicking something else? That's CLS, and it's infuriating.
/* Reserve space for images using aspect ratio */
.image-container {
aspect-ratio: 16 / 9;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Reserve space for dynamic content */
.dynamic-content {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
/* Always specify dimensions */
img,
video {
width: 100%;
height: auto;
}
/* Prevent font loading shifts */
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
/* Use size-adjust to prevent layout shift */
size-adjust: 100%;
}
The golden rules for preventing CLS:
font-display: swap
wisely// next.config.js
module.exports = {
images: {
domains: ["tashif.codes"],
formats: ["image/webp", "image/avif"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
},
experimental: {
optimizeCss: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
swcMinify: true,
};
Next.js gives you:
next/font
Astro's "island architecture" is brilliant for Core Web Vitals:
---
// Only ship JavaScript for interactive components
import HeavyChart from '../components/HeavyChart';
import ContactForm from '../components/ContactForm';
---
<Layout title="Dashboard">
<!-- Static content - zero JavaScript -->
<header>
<h1>Performance Dashboard</h1>
</header>
<!-- Hydrate only when visible -->
<HeavyChart client:visible />
<!-- Hydrate when browser is idle -->
<ContactForm client:idle />
<!-- Hydrate on user interaction -->
<InteractiveDemo client:click />
</Layout>
You get zero JavaScript by default, only hydrating interactive components when needed. It's a Core Web Vitals dream.
from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import StreamingResponse
import asyncio
app = FastAPI()
@app.get("/api/portfolio")
async def portfolio_data(background_tasks: BackgroundTasks):
# Stream data progressively
async def generate():
# Critical data first
critical = await get_critical_projects()
yield f"data: {json.dumps({'critical': critical})}\n\n"
# Additional data in background
additional = await get_additional_projects()
yield f"data: {json.dumps({'additional': additional})}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
# Implement caching
from functools import lru_cache
@lru_cache(maxsize=128)
async def get_cached_projects(category: str):
return await db.fetch_projects(category)
You can't optimize what you don't measure. Here's how I monitor tashif.codes
:
// web-vitals.js
import { getCLS, getFID, getFCP, getLCP, getTTFB, onINP } from "web-vitals";
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// Use sendBeacon if available
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/vitals", body);
} else {
fetch("/api/vitals", { method: "POST", body, keepalive: true });
}
}
// Measure everything
getCLS(sendToAnalytics);
onINP(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Tools I use:
Let's talk numbers. Google's research shows:
For tashif.codes
or any portfolio/business site, poor Core Web Vitals mean:
Performance isn't just technical - it's a business imperative.
Core Web Vitals represent the future of web development - where user experience is quantified, measured, and optimized. As full-stack developers, we have unique leverage: we control both the frontend rendering and backend delivery.
The frameworks we choose matter. Next.js gives us optimization out of the box. Astro gives us zero-JS by default. FastAPI gives us async performance. But frameworks alone aren't enough - we need to understand the fundamentals and apply them thoughtfully.
Remember: every millisecond matters. Every layout shift frustrates. Every smooth interaction delights. In the attention economy, performance is the ultimate differentiator.
Now go make your sites blazingly fast. Your users (and your business) will thank you.