Skip to content
Astro as seo 4 min read

Performance Optimization

Astro ships zero JavaScript by default, which gives you a head start on performance that most frameworks have to claw back. But “fast by default” is not the same as “optimized” — real-world Core Web Vitals depend on how you handle images, hydration, fonts, prefetching, and caching. This page walks through the practical levers that move LCP, INP, and CLS in the right direction on a production Astro site.

Understand what you’re optimizing

Performance work is only useful if it targets the metrics that affect users and ranking. The three Core Web Vitals map cleanly to concrete Astro techniques.

MetricMeasuresBiggest Astro levers
LCP (Largest Contentful Paint)Time to render the largest visible elementImage optimization, font loading, server rendering
INP (Interaction to Next Paint)Responsiveness to user inputMinimizing and deferring JavaScript hydration
CLS (Cumulative Layout Shift)Visual stability during loadExplicit image/media dimensions, reserved space

Measure before and after every change. Use Lighthouse or PageSpeed Insights for lab data and your analytics/RUM for field data — lab numbers alone can mislead you.

Minimize and defer JavaScript

The single biggest performance advantage in Astro is the islands architecture: static HTML by default, with interactivity opted into per-component via client:* directives. Choosing the right directive controls when (and whether) a component’s JS executes.

---
import Counter from "../components/Counter.jsx";
import Newsletter from "../components/Newsletter.jsx";
import Carousel from "../components/Carousel.jsx";
---

<!-- Hydrate immediately — use sparingly for above-the-fold interactivity -->
<Counter client:load />

<!-- Hydrate when the browser is idle — good default for non-critical UI -->
<Newsletter client:idle />

<!-- Hydrate only when scrolled into view — ideal for below-the-fold widgets -->
<Carousel client:visible />

If a component never needs client-side interactivity, render it without any directive at all and it ships zero JS.

DirectiveHydrates whenUse for
client:loadPage loadsCritical, above-the-fold interactivity
client:idleMain thread is idleLow-priority widgets
client:visibleElement enters viewportBelow-the-fold components
client:mediaA media query matchesResponsive-only behavior
client:onlyClient only (skips SSR)Browser-only libraries

Reach for client:visible and client:idle by default. Reserve client:load for the rare element a user interacts with within the first second.

Optimize images

Unoptimized images are the most common LCP killer. Astro’s built-in <Image /> and <Picture /> components from astro:assets automatically convert to modern formats, generate responsive sizes, and enforce dimensions to prevent layout shift.

---
import { Image } from "astro:assets";
import hero from "../assets/hero.jpg";
---

<Image
  src={hero}
  alt="Product dashboard overview"
  width={1200}
  height={630}
  format="avif"
  quality={80}
  loading="eager"
  fetchpriority="high"
/>

For your LCP image, set loading="eager" and fetchpriority="high" so it is not lazy-loaded. For everything below the fold, the default loading="lazy" is correct. The width and height attributes are required for local images and eliminate CLS by reserving layout space.

To serve multiple formats with fallbacks, use <Picture />.

---
import { Picture } from "astro:assets";
import banner from "../assets/banner.jpg";
---

<Picture
  src={banner}
  alt="Conference banner"
  formats={["avif", "webp"]}
  fallbackFormat="jpeg"
  width={1600}
  height={500}
/>

Prefetch upcoming navigations

Astro can prefetch pages before a user clicks, making navigation feel instant. Enable it in your config and control behavior per-link.

// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  prefetch: {
    prefetchAll: true,
    defaultStrategy: "hover",
  },
});

Override the strategy on individual links with the data-astro-prefetch attribute.

<a href="/pricing" data-astro-prefetch="viewport">Pricing</a>
<a href="/docs" data-astro-prefetch="hover">Docs</a>
<a href="/contact" data-astro-prefetch="tap">Contact</a>
StrategyTriggers prefetch whenBest for
hoverPointer hovers the linkDesktop, most links
tapPointer/finger starts a tapMobile, data-conscious users
viewportLink scrolls into viewHigh-confidence next clicks
loadPage finishes loadingCritical destinations

Leverage caching and prerendering

Static output is the fastest thing you can serve. With Astro’s hybrid rendering you can prerender most pages and reserve SSR for genuinely dynamic ones.

---
// A page that should be built once at build time
export const prerender = true;
---

For server-rendered or on-demand pages, set explicit cache headers so a CDN can serve them without round-tripping to your origin.

// src/pages/feed.json.ts
import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  const data = await getLatestPosts();
  return new Response(JSON.stringify(data), {
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": "public, max-age=300, s-maxage=600, stale-while-revalidate=86400",
    },
  });
};

Verify your headers from the command line.

curl -sI https://example.com/feed.json | grep -i cache-control

Output:

cache-control: public, max-age=300, s-maxage=600, stale-while-revalidate=86400

The stale-while-revalidate directive lets the CDN serve a slightly stale response instantly while it refreshes in the background — fast for users, fresh over time.

Best Practices

  • Default to no client:* directive; only hydrate components that genuinely need interactivity, and prefer client:visible/client:idle.
  • Always render images through astro:assets, set explicit width/height, and mark the LCP image with fetchpriority="high".
  • Prerender every page you can and use Cache-Control with s-maxage plus stale-while-revalidate for the rest.
  • Enable prefetching and tune the strategy per link (hover on desktop, tap on mobile).
  • Self-host fonts with font-display: swap and preload the critical font file to protect LCP and CLS.
  • Audit the JS payload regularly with astro build output and Lighthouse; a regression in shipped bytes is a regression in INP.
  • Test with field data (RUM), not just lab scores — real devices and networks reveal what synthetic tests miss.
Last updated June 14, 2026
Was this helpful?