Astro Zero-JS Pages: How I Hit 100 Lighthouse Performance
client:load vs client:visible, islands architecture in practice, and removing every unnecessary hydration point.
Tested on: Astro 5.7, Lighthouse 12 Site: 28-page docs blog. Score: 100 Performance, 100 Accessibility.
Default Astro is already mostly zero-JS
A .astro page ships HTML only. JS appears when you add client:* directives.
Audit — find accidental hydration
grep -r "client:" src/
My offenders:
<Header client:load />— mobile menu on every page<Search client:load />— on pages without search box visible- Starlight theme toggle
client:loadon static docs
Fix Header — only hydrate when needed
Before:
---
import Header from '../components/Header.svelte';
---
<Header client:load />
After — mobile menu only:
---
import MobileMenu from '../components/MobileMenu.svelte';
---
<header>
<nav class="desktop-nav"><!-- pure HTML links --></nav>
<MobileMenu client:media="(max-width: 768px)" />
</header>
client:media hydrates only on matching viewport. Desktop users: 0 KB menu JS.
Blog post pages — pure static
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((p) => ({ params: { slug: p.slug }, props: { post: p } }));
}
const { post } = Astro.props;
---
<Layout title={post.data.title}>
<article set:html={await post.render()} />
</Layout>
No client:* anywhere. Zero JS per post.
Error — Starlight forcing JS
@astrojs/starlight adds client:load to theme selector by default
Override in astro.config.mjs if you don't need toggle:
starlight({
components: {
ThemeSelect: './src/components/Empty.astro'
}
})
Empty.astro: --- --- (renders nothing).
Measure
npm run build
# Check dist/_astro/ chunk count
ls dist/_astro/*.js | wc -l
My blog listing page: 0 JS files. Contact page with form: 1 chunk (12 KB).
Lighthouse proof
Performance: 100
FCP: 0.6s
LCP: 0.9s
TBT: 0ms
Total JS: 0 KB (content pages), 12 KB (form page)
Islands aren't magic. They're discipline. Default to no client:*, add only with evidence.