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.
| Metric | Measures | Biggest Astro levers |
|---|---|---|
| LCP (Largest Contentful Paint) | Time to render the largest visible element | Image optimization, font loading, server rendering |
| INP (Interaction to Next Paint) | Responsiveness to user input | Minimizing and deferring JavaScript hydration |
| CLS (Cumulative Layout Shift) | Visual stability during load | Explicit 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.
| Directive | Hydrates when | Use for |
|---|---|---|
client:load | Page loads | Critical, above-the-fold interactivity |
client:idle | Main thread is idle | Low-priority widgets |
client:visible | Element enters viewport | Below-the-fold components |
client:media | A media query matches | Responsive-only behavior |
client:only | Client only (skips SSR) | Browser-only libraries |
Reach for
client:visibleandclient:idleby default. Reserveclient:loadfor 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>
| Strategy | Triggers prefetch when | Best for |
|---|---|---|
hover | Pointer hovers the link | Desktop, most links |
tap | Pointer/finger starts a tap | Mobile, data-conscious users |
viewport | Link scrolls into view | High-confidence next clicks |
load | Page finishes loading | Critical 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 preferclient:visible/client:idle. - Always render images through
astro:assets, set explicitwidth/height, and mark the LCP image withfetchpriority="high". - Prerender every page you can and use
Cache-Controlwiths-maxageplusstale-while-revalidatefor the rest. - Enable prefetching and tune the strategy per link (
hoveron desktop,tapon mobile). - Self-host fonts with
font-display: swapand preload the critical font file to protect LCP and CLS. - Audit the JS payload regularly with
astro buildoutput 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.