Astro 6 in 2026: what it is and why it ships so little JS
What Astro is and why it ships so little JS: Islands, the Content Layer API, what changed in Astro 6, and the real Next.js to Astro migration on my portfolio. →
Software engineer · Málaga

On this page
Updated May 2026: now covers Astro 6. I originally wrote this for Astro 5 in late 2025. I’ve since upgraded this site to Astro 6 (stable since 10 March 2026), and the headline pitch is unchanged: zero JS by default, Islands, and Core Web Vitals you can actually pass. The Content Layer API and
client:*hydration directives below are exactly the same in v6. The visible deltas are mostly under the hood: Node^22.12.0 || ^24.0.0minimum, Zod v4, and the v5 legacy grace periods are over. If you want the upgrade-from-Astro-5 angle instead of an intro, see my Astro 6 migration guide, and for the next jump, the Astro 7 upgrade.
My portfolio used to ship a heavy JavaScript bundle to render what is mostly text. On Astro the homepage ships little more than the dark mode toggle’s JavaScript. I’m not quoting before-and-after scores here.
Same content. Same design. This post is the why and the how.
What Astro is, and why version 6 matters
Astro is a modern web framework designed for building fast, content-focused websites: blogs, portfolios, marketing sites. Its key feature is the “zero JavaScript by default” architecture, which ships only HTML and CSS to the browser and loads JavaScript only for the interactive components that need it.
The main new features in Astro 5 (all carried into 6) focus on performance and flexibility:
- Content Layer API: unifies content loading from multiple sources (local files, headless CMS) with build-time data validation and type-safety through its new loader system.
- Improved Islands Architecture: allows for more granular component hydration with directives like
client:visible, drastically reducing the amount of JavaScript shipped to the client. - Faster builds: Astro says a new caching system and parallel processing cut build times. I have not timed my own build before and after.
- Integrated asset optimization: the
<Image />component now handles image optimization, automatically generating multiple sizes and formats.
Why I migrated my portfolio to Astro 5
After trying Next.js, Gatsby, and other frameworks, Astro 5 won me over with its radical approach: zero JavaScript by default. Not as a marketing line, as the actual architecture.
The problem Astro solves
Most modern frameworks ship all JavaScript code to the browser, even for static content. The result:
- JavaScript bundles bigger than a simple page needs
- Time to Interactive (TTI) that runs high on mobile
- JavaScript execution time that blocks the main thread
Astro flips the paradigm: HTML first, JavaScript only when necessary.
Content Layer API: the feature that earns the version number
Astro 5’s biggest innovation is the Content Layer API. Previously, Content Collections only supported local files. Now you can load content from any source.
Practical example: blog with headless CMS
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
// Local content (Markdown)
const blog = defineCollection({
loader: glob({
pattern: "**/*.{md,mdx}",
base: "./src/content/blog"
}),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
author: z.string().default("Your Name"),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
What the new loader system buys you
1. Real type-safety
Before, TypeScript trusted that your files followed the schema. Trusted. Now, with Zod validation:
// FAILS: this throws at BUILD TIME, not runtime
---
title: 123 # Error: Expected string, received number
pubDate: "invalid-date" # Error: Invalid date
---
2. Build performance
In theory the build should be faster; I have not timed my build before and after, so I won’t give you a figure. Why should it be faster?
- Smart caching: only recompiles modified files
- Parallel processing: handles multiple sources simultaneously
- Optimized tree-shaking: eliminates unused code from Zod validations
3. Unlimited flexibility
You can combine multiple sources:
// Future: Load from external API
import { strapiLoader } from '@astrojs/strapi-loader';
const externalBlog = defineCollection({
loader: strapiLoader({
url: 'https://api.example.com',
collection: 'posts'
}),
schema: z.object({...}),
});
Islands: hydrate the button, not the page
This is the concept that sets Astro apart from other frameworks.
The traditional problem
With Next.js or Nuxt, if you have one interactive button, the entire page gets hydrated:
// Next.js: Ships the ENTIRE bundle to the browser
export default function Page() {
return (
<div>
<Header /> {/* Static, but gets hydrated */}
<Article /> {/* Static, but gets hydrated */}
<InteractiveButton /> {/* Needs hydration */}
<Footer /> {/* Static, but gets hydrated */}
</div>
);
}
The Astro version
---
import Header from './Header.astro';
import Article from './Article.astro';
import InteractiveButton from './InteractiveButton.svelte';
import Footer from './Footer.astro';
---
<Header />
<Article />
<!-- Only this component gets hydrated -->
<InteractiveButton client:visible />
<Footer />
Real result: my homepage went from a full client bundle (Next.js) to just the dark mode toggle’s worth of JS.
Hydration directives: when to use each one
| Directive | Ideal use | JS Bundle |
|---|---|---|
client:load | Critical above-the-fold components | Loads immediately |
client:idle | Non-critical widgets | Waits for requestIdleCallback |
client:visible | Below-the-fold components | Loads when entering viewport |
client:media | Responsive components | Conditional per media query |
client:only | Embedded SPAs | Browser only |
Real example: image gallery
---
import ImageGallery from '@components/ImageGallery.react';
---
<!-- Only loads when user scrolls here -->
<ImageGallery
client:visible
images={galleryImages}
/>
Migrating from another framework
From Next.js
The challenges you’ll actually hit:
- API Routes: Astro doesn’t have
/pages/api. Use endpoints:
// src/pages/api/newsletter.ts
export async function POST({ request }) {
const data = await request.json();
// Your logic here
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
}
- Dynamic routes: Change
[id].jsto[id].astro, usegetStaticPaths:
---
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { id: post.id },
props: { post }
}));
}
---
- Data fetching: No
getServerSideProps. In Astro everything runs at build time:
---
// This executes on the server during build
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---
<div>{data.title}</div>
What changed on my portfolio
This is what I saw on this one site, with no figures because I did not keep the earlier measurements.
Before (Next.js 14): the JavaScript bundle hydrated pages that are mostly text.
After (Astro 5): little more than the dark mode toggle’s JavaScript.
Your mileage will vary with your content and components: run Lighthouse before and after on yours.
Where Astro wins, and where it will fight you
The honest version of the pitch: Astro is built for content. Step outside that and you’re swimming upstream.
Where it shines:
- Blogs and portfolios
- Marketing sites
- Technical documentation
- E-commerce with static catalogs
- High-performance landing pages
Where I wouldn’t use it:
- Dashboards with real-time updates
- SPAs with heavy shared state
- Apps requiring dynamic SSR (use Astro with SSR adapter)
- Apps like Gmail or Figma
If your app lives in the second list, picking Astro for the Lighthouse score is choosing the wrong tool for a good reason. Don’t.
Production tips
1. Image optimization
---
import { Image } from 'astro:assets';
import heroImage from '@assets/hero.jpg';
---
<!-- Automatically generates multiple sizes -->
<Image
src={heroImage}
alt="Hero"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 800px"
loading="eager"
/>
2. Prefetching for instant navigation
<script>
// Prefetch on link hover
document.querySelectorAll('a[href^="/"]').forEach(link => {
link.addEventListener('mouseenter', () => {
const href = link.getAttribute('href');
const prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = href;
document.head.appendChild(prefetchLink);
});
});
</script>
3. Dark mode without FOUC (Flash of Unstyled Content)
<script is:inline>
// Runs before render to avoid flash
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.classList.add(theme);
</script>
What stayed with me after using it
Astro isn’t “just another framework.” It’s a radical bet on performance without sacrificing DX, and after using it in production the bet has paid off for me:
- Deployments: simple static builds, with no measurement of my own on how much shorter they are
- Costs: a static site served from a CDN, no server of my own to maintain
- Improved SEO: Core Web Vitals in the green on this site (check yours)
- Less complexity: no hydration bugs, no bundle size anxiety
The styling layer rides the same philosophy: I pair Astro with Tailwind CSS 4, whose Rust engine rebuilds in microseconds, so the CSS pipeline stays as lean as the JS one.
If you’re building a content-heavy site, open your bundle analyzer and look at how many kilobytes of JavaScript you’re shipping to render text. Then decide.
Related reading: already on Astro 5 and eyeing the jump? My Astro 6 migration guide covers the Content Layer changes, the Zod v4 fallout, and the APIs that didn’t survive.
Sources
- Astro docs: Endpoints
- Astro, Astro 6.0 announcement (stable release date).
P.S. Migrated to Astro, or decided against it? I want to hear the before/after numbers either way. Find me on LinkedIn.
Last updated:


