Web Performance: A Practical Guide to Core Web Vitals
Why Core Web Vitals Matter
Google's Core Web Vitals aren't just abstract metrics — they directly impact your search rankings, user engagement, and conversion rates. A study by Deloitte found that a 0.1-second improvement in mobile site speed increased conversion rates by 8.4% for retail sites and 10.1% for travel sites.
At Rosecraft Studios, every project ships with a Lighthouse Performance score of 95 or higher. Here's how we consistently achieve that.
The Three Metrics That Matter
Largest Contentful Paint (LCP)
Target: under 2.5 seconds
LCP measures how long it takes for the largest visible element (usually a hero image or heading) to render. It's the metric users "feel" most directly.
// Bad: Unoptimized hero image blocks LCP
<img src="/images/hero.png" alt="Hero" />
// Good: Optimized with next/image, priority loading
import Image from 'next/image';
<Image
src="/images/hero.webp"
alt="Hero section showcasing our engineering work"
width={1200}
height={630}
priority
sizes="100vw"
/>
Key strategies:
- Use
priorityon the LCP element — Tells Next.js to preload the image - Serve modern formats — AVIF is 50% smaller than JPEG, WebP is 30% smaller
- Right-size images — Use
sizesprop so the browser downloads the correct variant - Minimize server response time — RSC + edge caching gets TTFB under 200ms
Interaction to Next Paint (INP)
Target: under 200 milliseconds
INP replaced First Input Delay (FID) in March 2024. It measures the latency of all interactions throughout the page's lifecycle, not just the first one.
The biggest INP killers:
- Long JavaScript tasks — Any task over 50ms blocks the main thread
- Hydration — Client-side React hydration re-renders the entire component tree
- Event handlers doing too much — Click handlers that trigger cascading state updates
// Bad: Heavy computation in click handler
function handleFilter(tag: string) {
const filtered = allPosts
.filter((p) => p.tags.includes(tag))
.sort((a, b) => b.date - a.date)
.map((p) => ({ ...p, excerpt: generateExcerpt(p.content) }));
setFilteredPosts(filtered);
}
// Good: Memoize expensive computations, defer non-critical work
const filteredPosts = useMemo(
() => allPosts.filter((p) => activeTag === 'all' || p.tags.includes(activeTag)),
[allPosts, activeTag],
);
Cumulative Layout Shift (CLS)
Target: under 0.1
CLS measures visual stability. Every time an element shifts position after rendering, it contributes to the CLS score. Common offenders:
- Images without dimensions — The browser doesn't know how much space to reserve
- Fonts loading late — Text reflows when custom fonts replace system fonts
- Dynamic content injection — Ads, embeds, or lazy-loaded content pushing elements around
// Bad: Image causes layout shift
<img src="/photo.jpg" alt="Team photo" />
// Good: Explicit dimensions prevent shift
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="Team photo"
width={800}
height={600}
className="rounded-lg"
/>
For fonts, we self-host with next/font to eliminate Flash of Unstyled Text (FOUT):
import { Poppins, Inter } from 'next/font/google';
const poppins = Poppins({
subsets: ['latin'],
weight: ['600', '700', '800'],
variable: '--font-heading',
display: 'swap',
});
Our Performance Checklist
Every page we ship passes this checklist:
Images
- All images use
next/imagewith explicitwidthandheight - Hero/LCP image has
priorityflag - Below-fold images use
loading="lazy"(default in Next.js) - Format priority: AVIF, WebP, then PNG/JPEG fallback
- No image exceeds 200 KB after optimization
JavaScript
- Default to Server Components (zero client JS)
"use client"only for interactive components- Code-split heavy components with
dynamic()imports - No unused dependencies in the bundle
- Tree-shaking verified with
@next/bundle-analyzer
CSS
- Tailwind CSS purges unused utilities at build time
- No render-blocking external stylesheets
- Critical CSS inlined by Next.js automatically
- Animations use
transformandopacityonly (GPU-composited)
Fonts
- Self-hosted via
next/fontfor zero layout shift display: swapfor progressive rendering- Only load weights actually used (not the full family)
- Preload the primary heading font
Measuring in Production
Lab data (Lighthouse) is useful for development but doesn't capture real user experience. For production monitoring, we use:
// Report Web Vitals to your analytics
export function reportWebVitals(metric: NextWebVitalsMetric) {
if (metric.label === 'web-vital') {
analytics.track('Web Vital', {
name: metric.name,
value: Math.round(metric.value),
rating: metric.rating,
});
}
}
Field data from Chrome User Experience Report (CrUX) is what Google actually uses for ranking signals. Monitor it monthly through Google Search Console.
Results From Our Projects
| Project | LCP | INP | CLS | Lighthouse |
|---|---|---|---|---|
| SaaS Dashboard | 1.1s | 89ms | 0.02 | 98 |
| Marketing Site | 0.8s | 45ms | 0.00 | 100 |
| E-commerce | 1.4s | 120ms | 0.03 | 96 |
| Content Platform | 1.2s | 67ms | 0.01 | 97 |
Every project exceeds Google's "Good" thresholds. The common thread: React Server Components for minimal JS, next/image for optimized media, and next/font for stable typography.
Is your site's performance holding back your search rankings? Schedule a performance audit and we'll identify exactly what's slowing you down and how to fix it.
