
Core Web Vitals with Astro: Complete Optimization Guide (2026)
Key Takeaways
- Core Web Vitals passing requires LCP ≤2.5s, INP ≤200ms, CLS ≤0.1—yet only 33% of websites pass all three metrics in 2026
- Astro ships 40-90% less JavaScript than React frameworks, achieving 1.6x higher mobile pass rates than Next.js (47% vs 29.3%)
- INP replaced FID in March 2024—measure overall responsiveness, not just first input
- 72% of sites pass INP, 75% pass CLS, and 58% pass LCP—LCP remains the biggest bottleneck
- Zero JavaScript by default gives Astro significant performance advantages, but strategic optimization is still required
Why Core Web Vitals Matter for Astro Sites
Core Web Vitals remain a permanent ranking factor in Google’s search algorithm for 2026. With only one-third of websites passing all three metrics, optimizing performance isn’t optional—it’s competitive advantage.
Astro’s architecture provides a head start: zero JavaScript by default means no main-thread blocking from hydration. Real-world data shows Astro achieves 47% mobile pass rates compared to Next.js’s 29.3%—a 1.6x improvement. This comes from shipping 40-90% less JavaScript while maintaining full interactivity through island architecture.
However, default Astro isn’t automatic perfection. The March 2026 Core Web Vitals update tightened evaluation criteria, making site-wide passing more challenging. Strategic optimization across LCP, INP, and CLS is required to consistently achieve “good” ratings across 75% of visits.
Understanding the Three Metrics (2026 Thresholds)
LCP: Largest Contentful Paint ≤ 2.5 seconds
LCP measures the initial loading speed—specifically, when the largest content element becomes visible. This could be a hero image, heading, or block of text. Only 58% of websites currently pass this metric, making it the most common Core Web Vitals failure point.
For Astro sites, LCP typically depends on:
- Hero image optimization and delivery
- Font loading strategies
- Server response times
- CDN performance
The threshold remains unchanged in 2026: ≤2.5 seconds for “good,” 2.5-4.0 seconds “needs improvement,” and >4.0 seconds “poor.” With tighter enforcement, even marginal delays now impact rankings.
INP: Interaction to Next Paint ≤ 200 milliseconds
INP replaced First Input Delay (FID) in March 2024 as the responsiveness metric. Unlike FID, which only measured the first interaction, INP observes all user interactions throughout the page lifecycle—clicks, taps, and keyboard inputs.
INP measures the delay between user input and visual feedback. 72% of sites pass this metric, suggesting most applications handle responsiveness adequately. The threshold: ≤200ms for “good,” 200-500ms “needs improvement,” and >500ms “poor.”
For Astro sites, INP performance primarily depends on:
- JavaScript execution time (minimal with Astro by default)
- Event handler efficiency
- Third-party script impact
- Long task blocking
CLS: Cumulative Layout Shift < 0.1
CLS measures visual stability—specifically, how much content shifts unexpectedly during page load. 75% of websites pass this metric, making it the most commonly achieved Core Web Vital.
The threshold: < 0.1 for “good,” 0.1-0.25 “needs improvement,” and > 0.25 “poor.” Each layout shift event is scored based on impact fraction and distance fraction, then summed for the total CLS score.
Common Astro CLS issues include:
- Images without explicit dimensions
- Font swaps causing reflow
- Dynamic content insertion above the fold
- Skeleton loaders without reserved space
Astro’s Built-in Performance Advantages
Astro’s architecture fundamentally addresses Core Web Vitals requirements through server-first rendering and zero JavaScript by default. Unlike React frameworks that hydrate entire applications, Astro ships only necessary JavaScript for interactive components.
This approach delivers measurable results:
- 40-90% less JavaScript shipped compared to equivalent React applications
- 47% mobile pass rate versus 29.3% for Next.js (1.6x improvement)
- Double Nuxt’s pass rate (24.9%) in real-world testing
- Near-perfect scores achievable with proper optimization
The island architecture pattern allows selective interactivity—only components requiring JavaScript pay the hydration cost. Everything else renders as static HTML, eliminating main-thread blocking from framework initialization.
However, Astro doesn’t automatically guarantee passing Core Web Vitals. Strategic optimization is required for images, fonts, third-party scripts, and dynamic content. The following sections provide implementation-specific guidance for each metric.
LCP Optimization with Astro
Image Priority and FetchPriority
Astro’s <Picture /> component with the priority prop signals that an image is above-the-fold and should load eagerly. formats is a <Picture /> prop (not <Image />, which takes a single format):
---
import { Picture } from 'astro:assets';
import heroImage from '../images/hero.jpg';
---
<Picture
src={heroImage}
widths={[800, 1200, 1600]}
sizes="(max-width: 768px) 100vw, 1200px"
formats={['avif', 'webp']}
priority
alt="Hero image showing Core Web Vitals metrics dashboard"
/>Resize params, not dimension declarations. On local imports, Astro reads width/height from the file at build time and emits them to the <img> automatically—you never need to pass width/height to declare the original size. Passing width={1200} resizes the output (height auto-computed to preserve aspect ratio); passing both width and height together forces a target box that can distort the image if its ratio differs. The pattern above uses widths (plural) instead: Astro generates a srcset with one file per entry, and the browser picks the smallest that satisfies sizes. Heights are derived from the source’s aspect ratio for each width—no manual height math.
The priority prop (added in astro@5.10.0) sets three attributes on the <img> for above-the-fold images:
loading="eager"(replaces the defaultloading="lazy")decoding="sync"fetchpriority="high"
priority already sets fetchpriority="high", so adding it again is redundant—set individual attributes only when you want to override what priority does. No preload <link> is generated.
Avoid over-using priority. The Astro Cloudinary guide warns that excessive prioritization delays other critical resources. Only apply to 1-2 elements that are genuinely LCP candidates.
Image Format Selection and Compression
Modern formats significantly reduce file size while maintaining quality:
<Picture
src={heroImage}
widths={[800, 1200, 1600]}
sizes="(max-width: 768px) 100vw, 1200px"
formats={['avif', 'webp']}
quality={85}
alt="Hero image"
/>The last entry in formats becomes a <source>; <Picture /> also emits a final <img> fallback in the original format (.jpg for a JPG source), so don’t list 'jpg' in formats—it’s redundant.
Format recommendations:
- AVIF for best compression (browser support: 93% as of 2026)
- WebP as fallback (near-universal support)
- JPEG final
<img>fallback is automatic from the source file - PNG only when transparency is required
Target < 100KB per image for LCP elements. Use Astro’s built-in sharp integration for automatic format conversion and compression without additional build tools.
Resource Hints for CDNs
Add preconnect hints for image CDNs in your layout head:
---
// src/layouts/Layout.astro
---
<html>
<head>
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>
<link rel="dns-prefetch" href="https://images.unsplash.com">
<!-- Other head elements -->
</head>
<body>
<slot />
</body>
</html>This establishes early connections to image origins, reducing TCP/TLS handshake delays before image requests begin.
Font Optimization Strategy
Astro 6+ ships a built-in Font API that handles font-display, subsetting, and preload links for you—no hand-written @font-face blocks. Register fonts in astro.config.mjs, then render them with <Font /> in your layout head:
// astro.config.mjs
import { defineConfig, fontProviders } from 'astro/config';
export default defineConfig({
fonts: [
{
provider: fontProviders.local(),
name: 'Inter',
cssVariable: '--font-inter',
options: {
variants: [
{ weight: '400', style: 'normal', src: ['./src/assets/fonts/inter-regular.woff2'] },
],
},
},
],
});---
// src/layouts/Layout.astro
import { Font } from 'astro:assets';
---
<html>
<head>
<!-- Outputs the @font-face <style> and optional <link rel="preload">.
Preload only the LCP font—every preload competes with the LCP
image for bandwidth. -->
<Font cssVariable="--font-inter" preload />
</head>
<body>
<slot />
</body>
</html>Astro defaults registered fonts to font-display: swap, so text renders immediately in a fallback and swaps once the web font arrives—no invisible text blocking LCP.
INP Optimization Strategies
Minimizing JavaScript Execution
Astro’s zero-JS default gives you significant INP advantages. Astro components (.astro) render to static HTML with no client runtime—interactivity comes from UI framework islands (.jsx, .svelte, .vue) that you import and hydrate on demand. Astro frontmatter is TypeScript only (no JSX)—write JSX in a separate framework file:
---
// Static Astro component — zero JS shipped
import Header from '../components/Header.astro';
// Interactive island — only this ships JS
import SearchForm from '../components/SearchForm.jsx';
---
<Header />
<SearchForm client:idle />client:* directives only work on imported framework components—not on plain HTML elements and not on .astro components.
Client directive priorities (per Astro docs):
client:load— High. Hydrate immediately on page load. Use for above-the-fold elements that must be interactive ASAP.client:idle— Medium. Hydrate once the browser firesrequestIdleCallbackafter initial load.client:visible— Low. Hydrate when the element enters the viewport (IntersectionObserver).client:media="(max-width: 50em)"— Low. Hydrate when a CSS media query matches. Requires a query string.
All directives ship the same JS for a given component—they differ only in when hydration fires, not how much JS. Default to no directive; add one only where the component must be interactive.
Event Handler Optimization
Efficient event handling prevents INP degradation:
<script>
// Passive event listeners for scroll/touch
document.addEventListener('scroll', handleScroll, { passive: true });
// Debounce user inputs
const debounce = (fn, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
};
searchInput.addEventListener('input', debounce(handleSearch, 300));
// Use requestAnimationFrame for visual updates
function updateDisplay() {
requestAnimationFrame(() => {
// Visual updates here
});
}
</script>Passive listeners promise not to call preventDefault(), allowing browsers to optimize scrolling behavior. Debouncing limits expensive operations during rapid user input.
Third-Party Script Management
Third-party scripts are the most common INP killers:
---
// In your layout or component
---
<!-- External script: is:inline is REQUIRED for a remote src. Without it,
Astro bundles the tag as a module and silently drops defer. -->
<script
is:inline
defer
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
></script>
<!-- Processed Astro script (bundled, type=module, deferred by default) -->
<script>
window.addEventListener('load', () => {
// Initialize analytics after Core Web Vitals measurement window
});
</script>Third-party script priorities:
- Defer everything unless explicitly blocking critical functionality
- Lazy load embeds (YouTube, social widgets) until user interaction
- Remove unused scripts—audit with Chrome DevTools Coverage tab
- Self-host analytics when possible (control over delivery)
For YouTube embeds, use lite-youtube-embed to defer heavy script loading until user click:
<LiteYouTube
id="videoId"
title="Video title"
/>CLS Fixes for Astro Projects
Image Dimensions and Aspect Ratios
Local imports (import blogImage from './blog.jpg') carry their dimensions in metadata—Astro emits width/height to the <img> automatically, no manual declaration needed. For remote URLs, pass width/height or set inferSize. Either way the rendered tag has explicit dimensions, which reserves layout space and prevents CLS during load:
---
import { Picture } from 'astro:assets';
import blogImage from '../images/blog.jpg';
---
<Picture
src={blogImage}
widths={[400, 800, 1200]}
sizes="(max-width: 768px) 100vw, 768px"
formats={['avif', 'webp']}
alt="Blog post thumbnail"
/>For responsive images, use aspect-ratio CSS as backup:
.responsive-image-container {
aspect-ratio: 16 / 9;
width: 100%;
}
.responsive-image-container img {
width: 100%;
height: auto;
object-fit: cover;
}For remote images (src="https://..."), you must pass width/height or set inferSize so Astro fetches the dims—without either, the rendered <img> has no dimensions and the page CLS-spikes during load. For local imports (import myImage from './x.png') Astro reads the file metadata at build time and emits width/height automatically; nothing to pass. <Image /> renders a single <img> with one src; pass widths/densities for a srcset, or use <Picture /> with formats for multiple formats.
Dynamic Content Slots
Skeleton loaders must reserve space to prevent CLS:
---
const data = await fetch('/api/data').then(r => r.json());
---
<div class="skeleton-container" style="min-height: 400px;">
{data ? (
<div>{/* Actual content */}</div>
) : (
<div class="skeleton">
{/* Loading placeholder */}
</div>
)}
</div>Always set explicit min-height on skeleton loaders matching the expected content height. This prevents collapse when loading completes.
Avoid DOM Insertions Above Fold
Content inserted via JavaScript after initial render causes layout shift:
<script is:inline>
// AVOID: This causes CLS
document.body.insertBefore(banner, document.body.firstChild);
// PREFER: Insert at end of body or use reserved space
const reservedSpace = document.getElementById('banner-placeholder');
reservedSpace.appendChild(banner);
</script>Strategy: Reserve space in HTML for dynamic content, then populate in-place rather than inserting above existing content.
Measuring and Monitoring
PageSpeed Insights Setup
Google’s PageSpeed Insights provides both lab data (simulated load) and field data (real Chrome users):
- Run at
pagespeed.web.devwith your site URL - Check Core Web Vitals Assessment section for pass/fail
- Review Metric breakdowns for specific failure causes
- Examine Opportunities for targeted optimizations
Interpret lab vs field data: Lab data is consistent for testing, but field data represents actual user experience with real devices and networks. Prioritize fixing field data failures first.
CrUX Data Integration
Chrome UX Report (CrUX) provides 25-week historical performance data:
// Access via PageSpeed Insights API
const response = await fetch(
`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&strategy=mobile`
);
const data = await response.json();
// Extract CrUX metrics
const lcp = data.loadingExperience.metrics.LARGEST_CONTENTFUL_PAINT;
const inp = data.loadingExperience.metrics.INTERACTION_TO_NEXT_PAINT;
const cls = data.loadingExperience.metrics.CUMULATIVE_LAYOUT_SHIFT;Track trends weekly to catch regressions before they impact rankings. Sudden metric drops often indicate deployment issues or third-party script changes.
Want this handled end to end? My technical SEO services for Astro and static sites include a full Core Web Vitals and indexability audit.
Common Astro Performance Pitfalls
Based on PageSpeedFix analysis of Astro sites:
Over-using Priority on Images
- Symptom
Multiple images flagged priority- Cause
- Marking multiple images as `priority` dilutes impact and delays other resources.
- Fix
- Only use `priority` on the genuine LCP element (typically one hero image).
Missing Image Dimensions
- Symptom
CLS spike during image load- Cause
- Omitting width/height causes layout shift during image load.
- Fix
- Always include explicit dimensions on `<Image />` and `<Picture />` components.
Unoptimized Third-Party Scripts
- Symptom
TBT / main-thread blocking- Cause
- Marketing scripts, chat widgets, and analytics blocking main thread.
- Fix
- Defer all non-critical scripts, lazy-load until user interaction.
Large LCP Elements
- Symptom
LCP > 2.5s on hero- Cause
- High-resolution hero images (>500KB) delaying initial render.
- Fix
- Compress to < 100KB, use modern formats (AVIF/WebP), consider lower initial quality.
Missing Resource Hints
- Symptom
Slow connect on CDN/font origin- Cause
- No preconnect/dns-prefetch for image CDNs or font origins.
- Fix
- Add `<link rel="preconnect">` for critical third-party origins.
FAQ
What replaced FID in Core Web Vitals?
INP (Interaction to Next Paint) replaced FID in March 2024. INP measures overall responsiveness through all page interactions rather than just first input delay. This captures the complete user experience, especially for single-page applications where users interact repeatedly. 72% of sites currently pass INP, making it the most commonly achieved metric.
Why does Astro perform better on Core Web Vitals?
Astro ships 40-90% less JavaScript than React frameworks by default through zero-JS architecture and island-based interactivity. This eliminates main-thread blocking from framework hydration, resulting in 1.6x higher mobile pass rates (47% vs 29.3% for Next.js). Server-first rendering sends complete HTML rather than skeleton screens waiting for client-side JavaScript.
What are the 2026 Core Web Vitals thresholds?
LCP ≤2.5 seconds for initial load speed, INP ≤200ms for responsiveness, CLS < 0.1 for visual stability. All three metrics require “good” ratings for 75% of visits to pass assessment. Google tightened enforcement in March 2026 with site-wide evaluation rather than page-by-page. These thresholds remain unchanged from previous years—algorithm evolution focuses on measurement methodology, not lowering standards.
How do I optimize images for LCP in Astro?
Use the priority prop on the LCP image element, set explicit width/height dimensions, enable modern formats (AVIF/WebP), and add fetchpriority="high" for browser scheduling. Avoid over-prioritizing multiple images as this delays other critical resources. Target < 100KB file size for LCP elements and add preconnect hints for image CDNs to establish early connections.
What causes poor CLS scores on Astro sites?
Missing image dimensions without width/height attributes, font swaps without reserved space using font-display or ascent-override, dynamic DOM insertions above the fold pushing content down, and skeleton loaders without min-height matching expected content. Always reserve layout space for dynamic content and use aspect-ratio CSS for responsive images as backup strategy.
Further Reading
- Google Core Web Vitals Documentation
- Astro Image Optimization Guide
- Core Web Vitals 2026 Guide
- Framework Pass Rate Study
- Astro vs Next.js 2026
Related articles

Building a Documentation Site with Astro Starlight and Custom Expressive Code
Learn how to create a stunning documentation site using Astro Starlight and add custom expressive code blocks to enhance developer experience.

Integrating Mailchimp Subscription Form with Astro
Add a Mailchimp subscription form to your Astro site using a JSONP submit handler. Step-by-step Mailchimp setup, an HTML form, and the JSONP JavaScript are included.

Implementing Dark Mode in Astro 4.0 with ViewTransitions
Discover integrating and using Dark Mode in your Astro 4.0 website with ViewTransitions enabled.
