Zero JavaScript by Default
Most modern frameworks ship a JavaScript runtime to the browser for every page, then “hydrate” the entire DOM so it becomes interactive. Astro flips that model: by default it renders your components to plain HTML on the server (or at build time) and sends zero client-side JavaScript. You add interactivity only where you actually need it, one component at a time. This is the single design choice that makes Astro sites fast, accessible, and cheap to host.
What “zero JS by default” actually means
When you write an Astro component, the code between the --- fences (the component script) runs only on the server during rendering. The browser never sees it. The output is static HTML and CSS — no framework bundle, no virtual DOM, no hydration step.
---
// src/pages/index.astro
// This runs at build time (or on the server). It is NOT sent to the browser.
const products = await fetch("https://api.example.com/products").then((r) => r.json());
const now = new Date().toLocaleDateString("en-US");
---
<html lang="en">
<head>
<title>Catalog</title>
</head>
<body>
<h1>Products as of {now}</h1>
<ul>
{products.map((p) => <li>{p.name} — ${p.price}</li>)}
</ul>
</body>
</html>
The fetch call, the date formatting, and the .map() all execute on the server. The user downloads only the resulting markup. If you inspect the network tab, you will see HTML and CSS — and not a single .js file.
Why this matters: JavaScript is the most expensive resource a browser can load. It must be downloaded, parsed, compiled, and executed before a page becomes interactive. By shipping none of it, Astro pages reach interactivity instantly and score near-perfect on Core Web Vitals out of the box.
How Astro compares to a typical SPA
| Aspect | Single-page app (default) | Astro (default) |
|---|---|---|
| HTML source | Rendered by JS in the browser | Rendered to static HTML on the server |
| Client JS shipped | Whole framework + app bundle | None |
| Time to interactive | After full hydration | Immediate (it’s just HTML) |
| Interactivity | Everything is interactive | Only opted-in components |
| SEO / no-JS users | Often degraded | Fully functional |
Opting in to interactivity
Static HTML is perfect until you need a counter, a carousel, or a search box. That’s where Astro’s islands come in. You import a UI-framework component (React, Vue, Svelte, Solid, Preact) and attach a client:* directive to tell Astro to hydrate just that component in the browser.
---
// src/pages/dashboard.astro
import Counter from "../components/Counter.jsx";
import StaticHeader from "../components/StaticHeader.astro";
---
<StaticHeader /> <!-- pure HTML, zero JS -->
<Counter client:load /> <!-- becomes an interactive island -->
Only Counter and its dependencies are bundled and sent to the browser. The header, the layout, and everything else stay as static HTML. This is the opposite of all-or-nothing hydration — you pay for JavaScript only where you spend it.
The client directives
Each directive controls when the island hydrates, letting you defer or even skip JS work.
| Directive | Hydrates when | Use for |
|---|---|---|
client:load | Immediately on page load | Critical above-the-fold widgets |
client:idle | Browser is idle | Low-priority interactivity |
client:visible | Component scrolls into view | Below-the-fold components |
client:media | A CSS media query matches | Mobile-only or desktop-only UI |
client:only | Skips server render entirely | Browser-only components |
---
import Carousel from "../components/Carousel.svelte";
import SearchBox from "../components/SearchBox.vue";
---
<Carousel client:visible />
<SearchBox client:media="(min-width: 768px)" />
Without a client:* directive, a framework component renders to HTML on the server and ships no JavaScript — exactly like an .astro component.
Verifying it yourself
Build a project and inspect the output to confirm what gets shipped.
npm run build
npm run preview
A page with no islands produces HTML and CSS only:
Output:
dist/index.html 4.2 kB
dist/_astro/index.css 1.8 kB
# no .js emitted for this route
Add a single client:load island and rebuild, and you’ll see a small hydration script appear — scoped to that island, not the whole page.
When you genuinely need site-wide JS
Sometimes you want a tiny vanilla script (analytics, a theme toggle). Astro supports inline <script> tags, which it bundles and optimizes automatically — still without pulling in a UI framework runtime.
<button id="theme-toggle">Toggle theme</button>
<script>
const btn = document.getElementById("theme-toggle");
btn?.addEventListener("click", () => {
document.documentElement.classList.toggle("dark");
});
</script>
This is plain JavaScript processed by Astro’s bundler — a few hundred bytes, not a framework.
Best practices
- Default to
.astrocomponents and static markup; reach for a UI-framework island only when a piece of UI truly needs client interactivity. - Prefer
client:visibleorclient:idleoverclient:loadfor anything below the fold to defer JavaScript work. - Keep islands small and focused — hydrate the interactive widget, not the entire page section around it.
- Do server-side data fetching in the component script so payloads stay as static HTML instead of client-side fetches.
- Use
client:mediato avoid shipping JavaScript to devices that will never display the component. - Audit your build output regularly to confirm routes ship the minimum JavaScript you expect.