Performance & Core Web Vitals
Performance & Core Web Vitals
Section titled “Performance & Core Web Vitals”Since 2021, Core Web Vitals are official Google ranking signals. Since March 2024, INP (Interaction to Next Paint) replaced FID as the responsiveness metric. These three metrics measure real user experience and directly affect ranking.
The three metrics
Section titled “The three metrics”| Metric | What it measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | When the largest visible element renders | <2.5s | 2.5–4.0s | >4.0s |
| INP (Interaction to Next Paint) | Responsiveness to user input | <200ms | 200–500ms | >500ms |
| CLS (Cumulative Layout Shift) | Visual stability | <0.1 | 0.1–0.25 | >0.25 |
To be classified as “Good” in Search Console’s Core Web Vitals report, 75% of page visits must hit the “Good” threshold for all three metrics.
LCP — Largest Contentful Paint
Section titled “LCP — Largest Contentful Paint”LCP measures when the largest content element (image, video poster, or text block) becomes visible in the viewport.
What counts as the LCP element
Section titled “What counts as the LCP element”The candidates Google considers:
<img>elements<image>inside<svg><video>elements (poster image)- Background images via CSS
- Text blocks (block-level elements containing text)
The “largest” is measured by viewport area covered.
Optimization techniques
Section titled “Optimization techniques”Preload critical resources (the most impactful single optimization for LCP):
<head> <link rel="preload" as="image" href="/hero-image.webp" fetchpriority="high" imagesrcset="/hero-480.webp 480w, /hero-1200.webp 1200w" imagesizes="100vw"></head>Responsive images with srcset:
<img src="/hero-1200.webp" srcset="/hero-480.webp 480w, /hero-768.webp 768w, /hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 1200px" alt="..." fetchpriority="high" loading="eager" width="1200" height="630">fetchpriority="high" tells the browser to prioritize this image over
other resources.
Server-side rendering: for SSR’d pages, the LCP element is in the
initial HTML, eliminating the wait for JS hydration. SvelteKit’s default
SSR mode (no csr: false) accomplishes this.
Critical CSS inline:
<head> <style> /* Critical above-the-fold styles inlined */ .hero { height: 100vh; background: #0ea5e9; } .hero-title { font-size: 4rem; color: white; } </style> <link rel="stylesheet" href="/styles/main.css" media="print" onload="this.media='all'"></head>The non-critical CSS is loaded with media="print" (which doesn’t block
rendering), then promoted to media="all" once loaded.
Reduce server response time (TTFB):
LCP includes server response time. A 500ms TTFB plus 1500ms of frontend rendering equals 2000ms LCP. See Caching Strategies for backend optimization.
Self-host fonts:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>Google Fonts adds an HTTP roundtrip to fonts.googleapis.com before the font
file. Self-hosting eliminates this. Use font-display: swap to render text
immediately with fallback fonts, swapping to the custom font when loaded.
INP — Interaction to Next Paint
Section titled “INP — Interaction to Next Paint”INP measures the slowest interaction the user experiences during their session. Specifically, the time from input event (click, tap, key press) to the next paint that reflects the response.
INP replaced FID in March 2024 because FID measured only the first interaction (often before JS heavy work). INP captures the full user experience.
Optimization techniques
Section titled “Optimization techniques”Break up long JavaScript tasks:
Long tasks (>50ms) block the main thread and degrade INP. Identify them in Chrome DevTools → Performance panel (gray bars are long tasks).
Split with setTimeout(0) or requestIdleCallback():
// BAD: blocks main thread for 200msfunction processItems(items) { items.forEach(item => doExpensiveWork(item));}
// GOOD: yields to the browser between itemsasync function processItems(items) { for (const item of items) { doExpensiveWork(item); await new Promise(resolve => setTimeout(resolve, 0)); }}Batch state updates:
In Svelte 5, multiple state updates inside the same synchronous code path batch automatically. Avoid forcing premature reactivity.
Delay non-critical scripts:
<!-- Load analytics after main content is interactive --><script> if (document.readyState === 'complete') { loadAnalytics(); } else { window.addEventListener('load', loadAnalytics); }</script>Or use requestIdleCallback:
requestIdleCallback(() => { loadAnalytics();}, { timeout: 2000 });Defer hydration of non-critical components:
For SvelteKit, this means selective hydration. Components below the fold
can be prerender: true without csr for hero areas that need interactivity:
export const prerender = true;For per-component hydration delay, use intersectionObserver-based
hydration patterns.
Avoid heavy synchronous work in event handlers:
<!-- BAD --><button onclick={() => { const result = processLargeDataset(data); // takes 300ms updateUI(result);}}>Process</button>
<!-- GOOD --><button onclick={async () => { showLoadingState(); await yieldToMainThread(); const result = await processInChunks(data); updateUI(result);}}>Process</button>CLS — Cumulative Layout Shift
Section titled “CLS — Cumulative Layout Shift”CLS measures unexpected visual movement of page content. A high CLS means content jumps around as the page loads — frustrating for users (they tap the wrong button) and a ranking signal for Google.
Optimization techniques
Section titled “Optimization techniques”Always set width and height on images and videos:
<img src="..." alt="..." width="1200" height="630">Even with responsive CSS (width: 100%; height: auto), the width and
height attributes tell the browser the aspect ratio so it reserves
the correct space before the image loads.
For modern responsive images, use aspect-ratio CSS:
img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover;}Reserve space for ads, embeds, iframes:
.ad-container { min-height: 280px; /* matches typical ad slot */}
.youtube-embed { aspect-ratio: 16 / 9;}Font loading: prevent FOIT and reduce FOUT shift:
@font-face { font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2'); font-display: swap; /* font-display: optional; — even better but slower font appearance */}Combine with size-adjust, ascent-override, descent-override CSS
properties to match fallback font metrics to the custom font, eliminating
shift on swap:
@font-face { font-family: 'Inter Fallback'; src: local('Arial'); size-adjust: 107%; ascent-override: 90%; descent-override: 22.5%;}
body { font-family: 'Inter', 'Inter Fallback', sans-serif;}Avoid injecting content above existing content:
Banner pop-ups, cookie notices that push content down, late-loading hero images that resize — all are CLS-rontó.
If you must show a banner, use a fixed overlay that doesn’t displace content:
.cookie-banner { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; /* This doesn't shift layout */}CSS animations should use transform/opacity, not width/height:
/* BAD: causes layout shifts */.menu-open { width: 300px; }
/* GOOD: composited animation, no shift */.menu-open { transform: translateX(0); }Measuring
Section titled “Measuring”Field data (real users)
Section titled “Field data (real users)”This is what Google uses for ranking:
- Search Console → Core Web Vitals report — 28-day rolling data, grouped by page-pattern
- Chrome User Experience Report (CrUX) — the underlying dataset Google uses, accessible via API
- PageSpeed Insights — combines lab and field data
Lab data (synthetic)
Section titled “Lab data (synthetic)”For development:
- Chrome DevTools → Performance panel — record a session, see metrics
- Chrome DevTools → Lighthouse panel — run an audit
- WebPageTest.org — detailed waterfall analysis
- Lighthouse CI — automated audits in your CI pipeline
Field data is what counts for ranking, but lab data is faster to iterate on during development.
Common pitfalls
Section titled “Common pitfalls”Optimizing only for desktop
Section titled “Optimizing only for desktop”Mobile typically has worse network and CPU. Test on mobile profiles in DevTools (3G throttling, 4x CPU slowdown) to match real conditions.
Optimizing for landing pages only
Section titled “Optimizing for landing pages only”Google measures CWV across the entire site. A fast homepage doesn’t help if blog posts are slow.
Chasing lab scores while ignoring field data
Section titled “Chasing lab scores while ignoring field data”Lighthouse scores can show “100” while real users experience slow loads. Always cross-reference with Search Console’s field data.
Adding code “to fix” CWV that breaks other things
Section titled “Adding code “to fix” CWV that breaks other things”E.g., aggressively delaying script execution can hide important UI states or break tracking. Measure user-facing UX, not just metrics.
Performance budget
Section titled “Performance budget”Set targets per page type:
| Page type | LCP target | INP target | CLS target | Total JS budget |
|---|---|---|---|---|
| Marketing landing | <1.5s | <100ms | <0.05 | <100kb |
| Blog post | <2.0s | <150ms | <0.1 | <150kb |
| App/dashboard | <2.5s | <200ms | <0.1 | <500kb |
Enforce in CI:
- name: Lighthouse CI uses: treosh/lighthouse-ci-action@v10 with: urls: | https://staging.example.com/ https://staging.example.com/blog/post budgetPath: ./lighthouse-budget.json runs: 3Cross-references
Section titled “Cross-references”- Server-Side Rendering — SSR’s role in LCP
- Image Optimization — image pipeline affecting LCP
- Caching Strategies — TTFB optimization
- Accessibility = SEO — accessibility correlates with performance