Astro performance starts high because Astro sends HTML and CSS and no JavaScript unless a component opts in. Most Astro sites that end up slow lose their speed in four places: unoptimised images, islands that hydrate too early, third-party scripts, and web fonts. Fix those, measure with real-user data, and an Astro site stays fast as it grows.
This guide goes through each one in Astro 7, with the config and component code to fix it, and ends with a checklist you can run before every launch.
Key Takeaways
- Use
<Image />and<Picture />fromastro:assetsfor every local image. Setpriorityon the largest above-the-fold image.- Every
client:*directive adds JavaScript. Preferclient:visibleorclient:idle, and use plain<script>tags for small interactions.prefetch: trueinastro.config.mjsmakes link navigation feel instant at almost no cost.- Self-host fonts with the Fonts API and preload only the one or two files used above the fold.
- Judge results by field data (Chrome UX Report, your analytics), not a single Lighthouse run.
Why Astro starts fast
A typical single-page-app framework sends a JavaScript bundle that has to download, parse and execute before the page becomes interactive. Astro renders components to HTML on the server and only ships JavaScript for components marked as islands. A content page with no islands is just HTML and CSS.
That default protects the metrics Google uses in its Core Web Vitals:
- Largest Contentful Paint (LCP), how quickly the main content appears. Target: 2.5 seconds or less.
- Interaction to Next Paint (INP), how quickly the page responds to input. Target: 200 ms or less.
- Cumulative Layout Shift (CLS), how much the layout jumps. Target: 0.1 or less.
Astro helps all three, but it can’t stop you from adding a 2 MB hero image or hydrating a carousel on load. The rest of this guide covers how to avoid those mistakes.
Images: the biggest win
Images are usually the heaviest thing on a content page and the most common LCP element. Astro’s astro:assets module resizes, converts and sets dimensions for you.
The Image component
---
import { Image } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<Image src={hero} alt="Dashboard showing weekly traffic" width={1200} priority />
For a local import, Astro knows the original size, so it:
- outputs a WebP (by default) at the requested width,
- writes
widthandheightattributes, which prevents layout shift, - adds
loading="lazy"anddecoding="async", unless you setpriority.
priority switches the image to loading="eager", decoding="sync" and fetchpriority="high". Use it on the one image most likely to be the LCP element, usually the hero. Everything else should stay lazy.
Images in public/ are not processed. Move images you want optimised into src/ (for example src/assets/) and import them.
Responsive images and layout
A single 1200px image is wasteful on a phone. Set a layout and Astro generates a srcset and sizes for you:
<Image src={hero} alt="..." layout="constrained" width={1200} height={630} />
layout |
Behaviour |
|---|---|
constrained |
Scales down to fit its container, never above width |
full-width |
Always fills the container width |
fixed |
Keeps the given size; generates 1x/2x densities |
none |
No responsive behaviour |
Set a project default in config so you don’t repeat it:
// astro.config.mjs
export default defineConfig({
image: { layout: 'constrained', responsiveStyles: true },
});
responsiveStyles adds a small amount of CSS so images resize correctly without your own rules. The responsive images tip shows custom widths and sizes for grids.
The Picture component for AVIF
<Picture /> outputs several formats and lets the browser pick:
---
import { Picture } from 'astro:assets';
---
<Picture src={hero} formats={['avif', 'webp']} alt="..." layout="full-width" />
AVIF is usually smaller than WebP at similar quality, but slower to encode, so builds take longer with many images.
Remote images
To process images from a CMS or CDN, allow the domain:
image: { domains: ['images.example-cms.com'] }
Remote images need width and height (or inferSize) so Astro can reserve space. If your CMS has its own image CDN, using its resizing parameters directly is often simpler. The Astro images guide covers image services and remote patterns.
Hydration budget: choosing client directives
Each island ships its framework runtime (once per framework) plus its own code. Two React islands share the React runtime; a React island plus a Svelte island ship both.
Rules that keep JavaScript low:
- Start with no directive. A framework component without
client:*renders to HTML and ships nothing. - Use
client:visiblefor anything below the fold. Comment widgets, carousels, maps and pricing calculators rarely need to load before the reader reaches them. - Use
client:idlefor important but not urgent widgets, like a search box in the header. - Reserve
client:loadfor controls the user may touch immediately. - Replace small islands with a
<script>tag. A menu toggle doesn’t need React.
<SearchBox client:idle />
<Testimonials client:visible={{ rootMargin: '200px' }} />
rootMargin starts loading a little before the island scrolls into view, so it’s ready when the reader arrives. More on that in lazy-loading islands.
To see what each island costs, inspect the bundle. The bundle size tip shows how to generate a visual treemap of your build.
Prefetch links
Prefetching downloads the next page in the background before the click. Enable it globally:
// astro.config.mjs
export default defineConfig({
prefetch: true,
});
Then mark links:
<a href="/pricing/" data-astro-prefetch>Pricing</a>
<a href="/blog/" data-astro-prefetch="viewport">Blog</a>
Strategies are hover (the default), tap, viewport and load. Set prefetch: { prefetchAll: true } to prefetch every link on hover without adding attributes. Prefetch is enabled automatically on pages using <ClientRouter />. The prefetch tip covers which strategy to use where.
View transitions
<ClientRouter /> from astro:transitions turns page navigations into animated transitions and keeps elements persistent across pages:
---
import { ClientRouter } from 'astro:transitions';
---
<head>
<ClientRouter />
</head>
It adds a small client-side router, so it is a trade-off: smoother navigation for some JavaScript on every page. For many sites, native cross-document view transitions in CSS (@view-transition { navigation: auto; }) give a similar effect in supporting browsers with no JavaScript at all. The view transitions guide explains both.
Fonts
Fonts often delay text rendering and cause layout shift when the fallback swaps for the web font. The Fonts API handles the hard parts:
// astro.config.mjs
import { defineConfig, fontProviders } from 'astro/config';
export default defineConfig({
fonts: [{
provider: fontProviders.google(),
name: 'Inter',
cssVariable: '--font-inter',
weights: [400, 700],
}],
});
---
import { Font } from 'astro:assets';
---
<Font cssVariable="--font-inter" preload />
Astro self-hosts the files, generates fallback font metrics to reduce layout shift, and preloads them. Request only the weights and subsets you use; each one is another file. The styling guide has the full Fonts API setup.
CSS delivery
Astro bundles CSS per page and inlines small stylesheets automatically. Two settings are worth knowing:
build.inlineStylesheets:'auto'(default) inlines small files,'always'inlines everything (fewest requests),'never'links everything (best caching across pages).- Scoped component styles only load on pages that use the component.
The usual CSS problem on Astro sites is a large global stylesheet: an icon font or a full CSS framework imported in the base layout. Audit what the layout imports first.
Third-party scripts
Analytics, chat widgets, A/B testing and tag managers are the usual cause of poor INP on otherwise fast Astro sites. In order of preference:
- Drop it, or replace it with a lighter alternative. The privacy-friendly analytics tip compares options that load under 2 KB.
- Load it late. A
<script>withdeferor loaded after user interaction. - Move it off the main thread with the Partytown integration (
npx astro add partytown) andtype="text/partytown". This works for many analytics tags, but not for scripts that need synchronous DOM access.
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>
Server-side speed
For static sites, the server is a CDN and Time to First Byte is rarely the problem. For on-demand pages:
- Cache responses with
Cache-Controlheaders where content allows. - Move the dynamic part into a server island (
server:defer) and keep the page itself static and cached. - Avoid sequential
awaitchains in frontmatter; usePromise.all()for independent fetches.
The Astro SSR guide goes deeper on server islands.
Measuring: lab data vs field data
Lighthouse and PageSpeed Insights lab scores simulate a mid-range phone on a throttled network. They are useful for catching regressions, but they are not what Google uses for ranking. Field data from real users (the Chrome UX Report, shown at the top of PageSpeed Insights when your site has enough traffic) is.
A practical routine:
- Run Lighthouse on the home page, a content page and the heaviest page before launch.
- After launch, watch field Core Web Vitals in Search Console or PageSpeed Insights.
- Investigate any page group that falls out of “Good”, starting with its LCP element.
Lab scores swing a few points between runs on the same page, so compare trends, not single numbers.
Komal themes are static Astro 7 sites with zero JavaScript by default, so the defaults in this guide already apply. Browse Astro themes to start from a fast baseline.
Worked example: diagnosing a slow page
A portfolio home page scores poorly on mobile, with LCP around five seconds in the lab. The process for any slow Astro page goes like this.
1. Find the LCP element. In Chrome DevTools, open the Performance panel, record a reload, and look at the LCP marker. Here it’s the hero image.
2. Check how it’s served. The Network tab shows the hero is a 2.4 MB JPEG from public/images/hero.jpg, rendered with a plain <img>. Files in public/ aren’t processed, so every visitor downloads the full original.
3. Fix it. Move the file to src/assets/, import it, and render it with <Image />:
---
import { Image } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<Image src={hero} alt="Selected work, 2026" layout="full-width" priority />
The browser now receives a WebP sized for its viewport, with fetchpriority="high".
4. Check what else blocks rendering. The waterfall shows two font files from a third-party CDN loading before text appears, and a 90 KB React bundle for a testimonials slider far down the page. Moving fonts to the Fonts API puts them on your own domain with preload hints; changing the slider from client:load to client:visible takes its JavaScript out of the initial load.
5. Measure again. Run Lighthouse three times and compare the median, not the best run. Then watch field data over the following weeks.
On Astro sites, the slow parts are almost always images in public/, eager islands and third-party resources, in roughly that order.
Common performance mistakes
- Optimising the wrong image.
priorityon a logo while the hero stays lazy. Put it on the image Lighthouse reports as LCP. priorityeverywhere. If every image is high priority, none is.- Layout shift from late content. Embeds, ads and islands that grow after hydration push content down. Reserve space with
aspect-ratioormin-height. - Prefetching everything with
load. It looks fast in testing and burns your visitors’ data. - Huge inline SVGs. A 200 KB illustration inlined into every page’s HTML can’t be cached separately. Reference it as an image unless you need to style its internals.
- Assuming the CDN is caching. Check response headers for cache hits after deploy, especially on on-demand pages.
Performance checklist
Before launch:
- All local images use
<Image />or<Picture />fromastro:assets - The LCP image has
priority; nothing else does - A default
image.layoutis set, or images have explicitwidths/sizes - No island uses
client:loadwithout a reason - Small interactions use
<script>instead of a framework island -
prefetchis enabled - Fonts are self-hosted, limited to the weights in use, and preloaded
- Third-party scripts are removed, deferred or moved to Partytown
- Lighthouse run on three representative pages
- Field Core Web Vitals checked after launch
For the build side (how long astro build takes and how to speed it up in CI), see faster Astro builds in CI. Performance also feeds into search, which the Astro SEO guide covers.
FAQ
Is Astro faster than React frameworks?
For content sites, Astro usually ships far less JavaScript because components render to HTML and only islands hydrate. Actual speed depends on images, fonts and third-party scripts, which affect every framework equally.
How do I optimise images in Astro?
Import local images from src/ and render them with <Image /> or <Picture /> from astro:assets. Astro resizes, converts to WebP or AVIF, sets dimensions and lazy-loads them.
What does priority do on the Astro Image component?
It marks the image as high priority: eager loading, synchronous decoding and fetchpriority="high". Use it only on the main above-the-fold image.
Does Astro prefetch links automatically?
Only on pages that use <ClientRouter />. Otherwise set prefetch: true in astro.config.mjs and add data-astro-prefetch to links, or enable prefetchAll.
Why is my Astro site slow on mobile?
The most common causes are large unoptimised images in public/, islands hydrating with client:load, heavy third-party scripts, and many font files. Check each in that order.