Astro 6 in 2026: 100 Core Web Vitals, 90% Less JS
Astro 6 is here. Hands-on with Islands and the Content Layer API, plus the real Next.js to Astro migration that pulled most of the JS off my portfolio. →
Óscar Gallego
Web Developer
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 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.
My portfolio used to ship a heavy JavaScript bundle to render what is mostly text. On Astro it ships a fraction of that, and on this site I now hit roughly 100 on Lighthouse.
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: a new caching system and parallel processing cut build times, noticeably so on incremental rebuilds.
- 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:
- 200-400KB bundles for simple pages
- Time to Interactive (TTI) of 3-5 seconds 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 my portfolio (20 pages), the build got noticeably faster. Why?
- 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>
My portfolio, before and after
These are my own numbers on this one site, not a benchmark you should expect to reproduce:
Before (Next.js 14): a multi-second Time to Interactive on mobile, a hefty gzipped bundle, and real Total Blocking Time on the main thread.
After (Astro 5): First Contentful Paint dropped to a fraction of that, Total Blocking Time was effectively gone, and the bundle shrank to roughly the dark mode toggle.
On this site that landed me around 100 on Lighthouse across the categories. Your mileage will vary with your content and components.
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>
Six months in production
Astro isn’t “just another framework.” It’s a radical bet on performance without sacrificing DX, and after 6 months in production the bet has paid:
- Faster deployments: noticeably shorter builds, especially incremental ones
- Lower costs: CDN edge hosting at $0/month (Vercel free tier)
- Improved SEO: Core Web Vitals comfortably in the green on this site
- Less complexity: no hydration bugs, no bundle size anxiety
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.
P.S. Migrated to Astro, or decided against it? I want to hear the before/after numbers either way. Find me on Twitter/X.
Last updated:
Related Posts

Astro 6 Migration Guide (2026): from Astro 5

Astro Debugging: Fixed "Missing Schema Fields" in 5 Min
