Astro: Build Faster Websites by Shipping Less JavaScript
Learn Astro's islands architecture, zero-JS-by-default philosophy, partial hydration, content collections, and when it's the right framework for your next project.
Most frameworks start with JavaScript and try to optimize it away. Astro starts with zero JavaScript and lets you opt in only where you need it. That single design decision changes everything about how you build websites, and the performance numbers prove it.
If you've been building content-heavy sites with React or Next.js and wondering why your blog ships 200KB of JavaScript just to render some text and images, Astro is the answer to a question you didn't know you were asking.
What Astro Actually Is
Astro is a web framework designed for content-driven websites. Portfolios, blogs, documentation sites, marketing pages, e-commerce storefronts -- anything where the content matters more than the interactivity.
The key insight: most of the web is static content. A blog post doesn't need JavaScript to render paragraphs. A product page doesn't need a virtual DOM to show images and descriptions. Only small parts of a page -- a search bar, an image carousel, a shopping cart -- actually need client-side interactivity.
Astro builds on this insight with what they call "islands architecture." Your page is an ocean of static HTML, with small islands of interactive JavaScript components scattered where needed.
---
// This runs at build time, not in the browser
import Header from '../components/Header.astro';
import ProductCard from '../components/ProductCard.astro';
import AddToCart from '../components/AddToCart.tsx'; // React component
import Newsletter from '../components/Newsletter.svelte'; // Svelte component
const products = await fetch('https://api.store.com/products').then(r => r.json());
<html>
<body>
<Header />
<!-- Static HTML, zero JS -->
{products.map(product => (
<ProductCard name={product.name} price={product.price} />
))}
<!-- Also static HTML, zero JS -->
<AddToCart client:visible productId="123" />
<!-- THIS ships JavaScript, but only when scrolled into view -->
<Newsletter client:idle />
<!-- This loads JS when the browser is idle -->
</body>
</html>
That client:visible directive is the magic. The React component only hydrates when the user scrolls to it. Before that, it's just static HTML. Your page loads fast because the browser isn't downloading, parsing, and executing JavaScript for components the user hasn't even seen yet.
.astro Components
Astro has its own component format, and it's refreshingly simple. The frontmatter (between the --- fences) runs at build time. Everything below is your template.
---
// This is server-side JavaScript (runs at build time for static sites)
interface Props {
title: string;
author: string;
publishDate: Date;
}
const { title, author, publishDate } = Astro.props;
const formattedDate = publishDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// You can fetch data, read files, query databases -- all at build time
const relatedPosts = await getRelatedPosts(title);
<article>
<h1>{title}</h1>
<p class="meta">By {author} on {formattedDate}</p>
<slot />
<!-- Like React's children -->
<aside>
<h3>Related Posts</h3>
<ul>
{relatedPosts.map(post => (
<li><a href={post.url}>{post.title}</a></li>
))}
</ul>
</aside>
</article>
<style>
/ Scoped by default -- only applies to this component /
article {
max-width: 65ch;
margin: 0 auto;
}
.meta {
color: #666;
font-size: 0.9rem;
}
</style>
No useState, no useEffect, no lifecycle hooks. If a component doesn't need interactivity, it doesn't need any of that machinery. The HTML renders once, at build time, and ships to the browser as pure HTML and CSS.
Styles are scoped by default -- they only apply to the component they're defined in. No CSS-in-JS libraries, no naming collisions, no specificity wars.
Zero JavaScript by Default
This is the part that surprises most developers coming from React or Next.js. Build a full Astro site with ten pages, navigation, dynamic content from a CMS, and styled components -- and check the network tab. Zero JavaScript. None.
---
// pages/about.astro
import Layout from '../layouts/Layout.astro';
import TeamMember from '../components/TeamMember.astro';
const team = [
{ name: 'Alice', role: 'Engineering Lead', photo: '/team/alice.jpg' },
{ name: 'Bob', role: 'Designer', photo: '/team/bob.jpg' },
{ name: 'Charlie', role: 'DevRel', photo: '/team/charlie.jpg' },
];
<Layout title="About Us">
<h1>Our Team</h1>
<div class="team-grid">
{team.map(member => (
<TeamMember {...member} />
))}
</div>
</Layout>
This page ships as pure HTML and CSS. The team array, the mapping logic, the component composition -- all of it runs at build time and produces static markup. The browser receives a fully-rendered page with nothing to execute.
Compare this to the same page in Next.js. Even with server components, you're shipping the React runtime, hydration code, and the router. That's overhead your content page doesn't need.
Partial Hydration: The Client Directives
When you do need interactivity, Astro gives you fine-grained control over when and how JavaScript loads with client directives.
---
import SearchBar from '../components/SearchBar.tsx';
import Comments from '../components/Comments.tsx';
import Analytics from '../components/Analytics.tsx';
import ImageCarousel from '../components/ImageCarousel.vue';
<!-- Loads JS immediately on page load -->
<SearchBar client:load />
<!-- Loads JS when the component scrolls into the viewport -->
<Comments client:visible />
<!-- Loads JS when the browser's main thread is free -->
<Analytics client:idle />
<!-- Loads JS only on screens wider than 768px -->
<ImageCarousel client:media="(min-width: 768px)" />
<!-- Loads JS only after explicit trigger -->
<HeavyWidget client:only="react" />
Each directive tells Astro exactly when to load the JavaScript for that component:
client:load-- Hydrate immediately. Use for above-the-fold interactive elements.client:visible-- Hydrate when scrolled into view using IntersectionObserver. Perfect for comments sections, footers, anything below the fold.client:idle-- Hydrate when the browser is idle (requestIdleCallback). Good for non-critical interactivity.client:media-- Hydrate only when a CSS media query matches. Desktop-only widgets, for example.client:only-- Skip server rendering entirely and only render on the client. Use for components that depend on browser APIs.
client: directive, the component renders at build time and ships zero JavaScript. This is the default and it's the right choice for most components.
Content Collections
Astro has first-class support for content-heavy sites through content collections. You define a schema for your content, and Astro gives you type-safe querying, validation, and rendering.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
image: z.string().optional(),
}),
});
const authors = defineCollection({
type: 'data', // JSON/YAML data, not Markdown
schema: z.object({
name: z.string(),
bio: z.string(),
avatar: z.string(),
}),
});
export const collections = { blog, authors };
Now create Markdown or MDX files in src/content/blog/ and Astro validates them against your schema at build time. Typo in a frontmatter field? Missing required property? You'll know immediately, not after deployment.
---
// pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
<BlogLayout title={post.data.title}>
<h1>{post.data.title}</h1>
<time>{post.data.date.toLocaleDateString()}</time>
<Content />
</BlogLayout>
Type-safe, validated, and all resolved at build time. If you've ever used a CMS with a loosely-typed API and had content silently break in production because someone misspelled a field, you'll appreciate this.
Using React, Vue, and Svelte Components Together
This is Astro's party trick. You can use components from multiple frameworks in the same project, even on the same page.
npx astro add react
npx astro add vue
npx astro add svelte
---
import ReactCounter from '../components/Counter.tsx';
import VueToggle from '../components/Toggle.vue';
import SvelteAccordion from '../components/Accordion.svelte';
<div>
<ReactCounter client:visible />
<VueToggle client:load />
<SvelteAccordion client:idle />
</div>
This isn't a gimmick -- it's genuinely useful. Migrating from React to Svelte? Do it incrementally. Found a perfect Vue component library for one feature? Use it without rewriting your whole app. Have team members who prefer different frameworks? Let them use what they know.
Each framework's runtime only loads for the components that use it. If you have one React island on a page, only React's runtime ships -- and only when that island hydrates.
SSG vs SSR
Astro defaults to static site generation (SSG). Every page is pre-rendered to HTML at build time. This is the fastest option and works perfectly for blogs, docs, and marketing sites.
But Astro also supports server-side rendering (SSR) when you need it:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // or 'hybrid' for mix of static + server
adapter: node({
mode: 'standalone',
}),
});
With output: 'hybrid', you get the best of both worlds. Most pages are pre-rendered statically, but you can opt specific pages into server rendering:
---
// This page renders on every request
export const prerender = false;
const user = await getUser(Astro.cookies.get('session'));
const feed = await getPersonalizedFeed(user.id);
<h1>Welcome back, {user.name}</h1>
{feed.map(item => <FeedItem {...item} />)}
Static pages are fast and cheap. Server pages handle dynamic content. You choose per-page, not per-project.
Project Structure
An Astro project looks familiar if you've used any modern framework:
my-site/
src/
pages/ # File-based routing
index.astro
about.astro
blog/
[slug].astro
components/ # Astro, React, Vue, Svelte -- whatever
layouts/ # Reusable page layouts
content/ # Content collections (Markdown, MDX, JSON)
blog/
first-post.md
second-post.md
config.ts
styles/ # Global styles
public/ # Static assets (served as-is)
astro.config.mjs # Astro configuration
File-based routing works like Next.js. src/pages/about.astro becomes /about. Dynamic routes use brackets: [slug].astro.
When Astro Is the Right Choice
Astro excels when your site is primarily content with sprinkles of interactivity. Here's the honest breakdown:
Choose Astro when:- You're building a blog, docs site, portfolio, or marketing page
- Performance (especially Core Web Vitals) is a priority
- Most of your pages are content-driven, not interaction-driven
- You want to use components from multiple frameworks
- You're tired of shipping JavaScript for pages that don't need it
- You're building a highly interactive web app (think Figma, Notion, Google Docs)
- Every page needs heavy client-side state management
- You need real-time features throughout the entire application
- Your team is deeply invested in a single framework's ecosystem
Common Mistakes
Overusing client directives. If you find yourself addingclient:load to every component, you might be building an app, not a content site. Consider whether Astro is the right tool.
Not using content collections. Managing Markdown files manually works for five posts. At fifty, you'll wish you had schema validation and type-safe queries. Set up content collections from the start.
Importing framework components without a client directive. The component will render at build time and produce static HTML, but any event handlers or state will be silently stripped. If the component needs interactivity, you need a client: directive.
Fighting Astro's model. If you're trying to build a single-page app with client-side routing and global state, Astro will fight you every step of the way. That's not a limitation -- it's a signal that you need a different tool.
Getting Started
npm create astro@latest my-site
cd my-site
npm run dev
The CLI walks you through setup with options for TypeScript, sample templates, and framework integrations. Start with a blank project and add complexity as you need it.
Astro's documentation is genuinely excellent -- one of the best in the JavaScript ecosystem. Between the docs, the built-in tutorial, and the growing ecosystem of themes and integrations, you can go from zero to a deployed site in an afternoon.
What's Next
If you're building content-first websites and performance matters to you, give Astro a serious try. The zero-JS default isn't just a cool technical decision -- it's a fundamentally different way of thinking about what the browser actually needs to do its job.
Start with a simple blog or portfolio. Experience the feeling of checking the network tab and seeing no JavaScript bundles. Then add islands of interactivity where they genuinely improve the user experience. That's the Astro way, and once you internalize it, going back to shipping megabytes of JavaScript for static content feels wrong.
Practice building with modern frameworks and explore more web development concepts at CodeUp.