Astro ships zero JavaScript by default. A typical Astro site loads 40% faster than its React equivalent while sending 90% less JavaScript to the browser. The framework's server-first architecture and islands pattern make it, in their words, "nearly impossible to build a slow website."
And yet developers manage to build slow Astro sites anyway.
The problem isn't Astro—it's patterns that made sense in React or Next.js but actively work against Astro's performance model. Slapping client:load on every component. Using React when Preact would work. Skipping the built-in image optimization.
This guide covers the five mistakes that most commonly slow Astro sites down, how to measure Astro's unique MPA performance characteristics, and how to choose the right hydration directive for each component.
Why Astro is fast by default
Astro's performance advantage comes from four architectural decisions:
HTML-first rendering. Astro components produce static HTML at build time. No JavaScript runtime ships to the browser unless you explicitly add it. Compare this to React, where even a static page includes 40KB+ of framework code.
Islands architecture. Instead of hydrating the entire page (like traditional SPAs), Astro hydrates only the interactive components you designate. A 20-component page might ship JavaScript for just 2 of them.
Server-side by default. Computational work happens at build time on your server, not on your visitors' phones. This is especially significant for users on slower devices.
Automatic optimizations. Astro inlines small scripts, bundles TypeScript automatically, and deduplicates imports without configuration.
The result: Astro sites consistently achieve perfect Core Web Vitals scores where Next.js or Create React App would struggle. But this only works if you don't fight the framework.
The 5 mistakes that slow Astro down
1. Overusing client:load
The most common mistake is treating client:load as the default hydration directive.
---
// Bad: Every component loads JavaScript immediately
import Navbar from '../components/Navbar';
import HeroSection from '../components/HeroSection';
import FeatureCards from '../components/FeatureCards';
import Newsletter from '../components/Newsletter';
import Footer from '../components/Footer';
---
<Navbar client:load />
<HeroSection client:load />
<FeatureCards client:load />
<Newsletter client:load />
<Footer client:load />
This creates "performance no different from a React SPA—completely wasting Astro's architectural advantages." You've shipped all the JavaScript upfront, defeating the purpose of islands.
Fix: Question every directive. Most components don't need client-side JavaScript at all:
---
// Good: Only interactive components get hydrated
import Navbar from '../components/Navbar';
import HeroSection from '../components/HeroSection';
import FeatureCards from '../components/FeatureCards';
import Newsletter from '../components/Newsletter';
import Footer from '../components/Footer';
---
<!-- Static HTML, no JavaScript -->
<Navbar />
<HeroSection />
<FeatureCards />
<!-- Only the form needs interactivity, load when visible -->
<Newsletter client:visible />
<!-- Static footer -->
<Footer />
2. Heavy framework islands
When you do need interactivity, your framework choice matters enormously:
| Framework | Bundle Size (gzipped) |
|---|---|
| Preact | ~3 KB |
| Solid.js | ~7 KB |
| React + ReactDOM | ~40-45 KB |
Choosing React over Preact for an island adds 37-42KB. For a page with three interactive components, that's potentially 120KB of unnecessary JavaScript.
---
// Bad: React for a simple counter
import Counter from '../components/Counter.tsx';
---
<Counter client:visible />
---
// Good: Preact for the same functionality
import Counter from '../components/Counter.preact.tsx';
---
<Counter client:visible />
Preact offers a near-identical React API. Unless you're using React-specific features like concurrent rendering, switch to Preact and save the bytes.
3. Skipping astro:assets for images
Using raw <img> tags instead of Astro's <Image /> component leaves performance on the table:
---
// Bad: No optimization, potential CLS, no lazy loading
---
<img src="/images/hero.jpg" alt="Hero image" />
---
// Good: Automatic AVIF/WebP, inferred dimensions, lazy loading
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<Image src={heroImage} alt="Hero image" />
Astro's Image component:
- Converts images to modern formats (AVIF, WebP) via Sharp
- Infers width and height to prevent CLS
- Enables lazy loading by default
- Generates responsive
srcsetattributes
For art direction across breakpoints, use <Picture />:
---
import { Picture } from 'astro:assets';
import heroDesktop from '../assets/hero-desktop.jpg';
---
<Picture
src={heroDesktop}
formats={['avif', 'webp']}
alt="Hero image"
widths={[400, 800, 1200]}
/>
Important: Images in src/ get optimized. Images in public/ are copied as-is. For optimization, always import from src/.
4. Font loading mistakes
Web fonts can block rendering and cause layout shifts if loaded incorrectly. Common mistakes:
---
// Bad: Fonts loaded via CSS @import in component
---
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
</style>
This creates a render-blocking request. The browser can't paint text until the font loads.
Fix: Preload critical fonts and use font-display:
<!-- In your layout head -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="preload"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
as="style"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
/>
Or better, self-host fonts to eliminate external requests entirely:
---
// fonts.css
---
<style is:global>
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
</style>
5. Third-party scripts blocking render
Analytics, chat widgets, and tracking pixels can undo all your Astro optimizations. A single chat widget can add 500KB of JavaScript and block the main thread for over a second.
---
// Bad: Scripts in head block rendering
---
<head>
<script src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script src="https://widget.intercom.io/widget/APP_ID"></script>
</head>
Fix: Defer non-critical scripts and use facades:
---
// Good: Defer analytics, lazy-load chat widgets
---
<head>
<!-- Defer analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
</head>
<body>
<!-- Load chat widget only when user wants it -->
<button id="chat-trigger">Need help?</button>
<script>
document.getElementById('chat-trigger').addEventListener('click', () => {
// Load Intercom only on interaction
const script = document.createElement('script');
script.src = 'https://widget.intercom.io/widget/APP_ID';
document.body.appendChild(script);
});
</script>
</body>
For a deep dive on this topic, see our guide on third-party scripts and Core Web Vitals.
Measuring Astro performance
Astro's MPA (Multi-Page Application) architecture requires different measurement approaches than SPAs.
What to measure
LCP (Largest Contentful Paint): Astro excels here because HTML arrives pre-rendered. No waiting for JavaScript to execute before content appears. Real-world migrations from React SPAs to Astro have seen LCP drop from 4 seconds to 1.5 seconds.
INP (Interaction to Next Paint): With fewer scripts executing, user interactions face less main thread competition. Astro sites typically have excellent INP unless you've overloaded islands with heavy frameworks.
CLS (Cumulative Layout Shift): Astro's Image component prevents most CLS issues automatically. Watch for shifts from:
- Fonts loading without
font-display: swap - View Transitions with improperly handled scripts
- Third-party embeds loading late
Tools for Astro
Lighthouse: Works normally. Run it in incognito to avoid extension interference.
Chrome DevTools Performance tab: Profile page loads to identify JavaScript bottlenecks. In an optimized Astro site, you should see minimal JavaScript execution.
Real User Monitoring (RUM): Tools like Vercel Analytics or Cloudflare Web Analytics show how real users experience your site. Lab tests don't capture real-world device and network variance.
For LCP optimization techniques, prioritize above-the-fold content and preload hero images.
View Transitions and CLS
Astro's View Transitions enable smooth page-to-page animations without building an SPA. They work in two modes:
MPA mode (default): Uses browser-native View Transitions API for cross-document animations. No additional JavaScript.
SPA mode (<ClientRouter />): Full client-side routing with state persistence. Requires the router component.
Built-in CLS protections
Astro's View Transitions include several CLS safeguards:
- Stylesheet preservation: Prevents FOUC (Flash of Unstyled Content) during navigation
- Automatic scroll restoration: Returns users to their scroll position on back navigation
- Reduced motion support: Automatically respects
prefers-reduced-motion
Potential CLS pitfalls
Inline scripts may re-execute during navigation, potentially causing layout changes. Handle this with lifecycle events:
<script>
// Bad: Runs on every navigation, may cause shifts
document.querySelector('.sidebar').style.width = '250px';
</script>
<script>
// Good: Controlled initialization with lifecycle events
document.addEventListener('astro:page-load', () => {
document.querySelector('.sidebar').style.width = '250px';
});
</script>
Note: Bundled module scripts (those processed by Astro) execute only once per session, not on every navigation.
Choosing hydration directives
Astro's directive system determines when (and if) components ship JavaScript. Here's a decision framework:
| Directive | When JS Loads | Use For |
|---|---|---|
| (none) | Never | Static content, no interactivity needed |
client:load | Immediately | Critical UI that must work instantly (dropdowns, mobile nav) |
client:idle | When browser idle | Non-critical interactivity (newsletter forms, carousels) |
client:visible | When scrolled into view | Below-fold components (comments, related posts) |
client:media | When media query matches | Responsive interactivity (mobile-only features) |
client:only | Client-only render | Components that can't be server-rendered (using window/document) |
Decision flowchart
- Does this component need JavaScript at all? If it's just displaying content, use no directive.
- Is it above the fold and immediately interactive? Use
client:load, but question whether it truly needs to be instant. - Is it above the fold but not immediately needed? Use
client:idleto defer until the browser has idle time. - Is it below the fold? Use
client:visible. No reason to load JavaScript for content users haven't scrolled to yet. - Does it depend on viewport size? Use
client:mediawith the appropriate query.
---
import MobileMenu from '../components/MobileMenu';
import SearchBar from '../components/SearchBar';
import Comments from '../components/Comments';
import Newsletter from '../components/Newsletter';
---
<!-- Must work instantly on mobile -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- Critical for user experience -->
<SearchBar client:load />
<!-- Users scroll to these; load when visible -->
<Comments client:visible />
<Newsletter client:visible />
Conclusion
Astro's zero-JavaScript default gives you a performance advantage that most frameworks require significant optimization to achieve. The key is not fighting that advantage by:
- Using hydration directives sparingly and choosing the right one
- Selecting lightweight frameworks (Preact, Solid) over React for islands
- Using built-in image optimization instead of raw
<img>tags - Loading fonts and third-party scripts without blocking render
For sites that need more interactivity, Astro's islands architecture lets you add JavaScript surgically—paying the performance cost only where you need it.
Related guides:
- How to Improve LCP — Techniques for faster largest contentful paint
- Fix Cumulative Layout Shift — Eliminate visual instability
- Third-Party Scripts and Core Web Vitals — Control external script impact
Need help identifying what's slowing down your Astro site? Run it through PageSpeedFix for a detailed Core Web Vitals analysis and specific fixes.