Why We Build Client Sites with Astro
When clients ask us why we reach for Astro over a full SPA framework, we give them a simple answer: the web is mostly content, and content doesn’t need JavaScript to be fast.
But the more honest answer is technical. We’ve shipped Astro in production across e-commerce, marketing, and SaaS projects, and it consistently outperforms alternatives on the metrics that actually matter — Lighthouse scores, Time to First Byte, and the operational simplicity of handing a codebase to a client team for long-term maintenance.
This post is a technical walkthrough of why Astro is our default, with concrete examples.
The Problem With SPAs for Content Sites
Single-page applications built on Next.js or Remix are fantastic tools for the right job. Authenticated dashboards, real-time data, highly interactive UIs — yes. A marketing site with a hero, feature cards, and a contact form — no.
The SPA model ships a JavaScript runtime (150–300kb+) to the browser before rendering anything. That runtime then fetches data, executes component code, and hydrates the DOM. This is a lot of work for a page that could have been served as static HTML.
The concrete cost: a Next.js marketing site we audited before a client engagement had a Largest Contentful Paint of 4.8 seconds on mobile and a Lighthouse performance score of 41. The same content, rebuilt in Astro and deployed to Vercel’s CDN, hit LCP of 0.9 seconds and a score of 98.
No image CDN tricks. No edge functions. Just less JavaScript.
How Islands Architecture Actually Works
Astro’s model is simple: every page is HTML by default. JavaScript is opt-in per component.
---
// This runs only at build time — zero JS in the browser
const posts = await getCollection('blog');
---
<ul>
{posts.map(post => <li>{post.data.title}</li>)}
</ul>
The list above renders to static HTML. No JavaScript ships. No hydration happens. It’s just <li> elements.
Now suppose one component on the page needs interactivity — say, a search bar:
---
import SearchBar from './SearchBar.tsx';
---
<!-- This component IS interactive — hydrate it -->
<SearchBar client:load />
<!-- Everything else stays static -->
<ArticleList posts={posts} />
The client:load directive tells Astro to ship JavaScript for SearchBar specifically. ArticleList stays zero-JS.
You have fine-grained control over hydration strategy:
| Directive | When it hydrates |
|---|---|
client:load | Immediately on page load |
client:idle | When the browser is idle |
client:visible | When the element enters the viewport |
client:media="(max-width: 768px)" | When a media query matches |
For a typical marketing site, this means we ship under 10kb of JavaScript total. The rest is static HTML served from Vercel’s edge network — from a CDN node physically close to the user, with no compute required.
Content Collections and Type Safety
Astro’s content collections system is one of its most underrated features in production projects.
Without it, blog content is just loose Markdown files. Frontmatter is untyped — you can typo a field name, forget a required field, or use the wrong date format, and nothing catches it until runtime.
With content collections, you define a Zod schema for your content:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
author: z.string().default('Ten Peaks Tech'),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
Now getCollection('blog') returns fully typed data. TypeScript knows post.data.pubDate is a Date, post.data.tags is string[], and post.data.title is a string. A typo in any .md frontmatter throws a build error before anything ships.
For a client with a content team who edits Markdown files directly in GitHub, this is invaluable. Their CI pipeline catches malformed frontmatter automatically.
Static Output and the Vercel CDN
When Astro builds with output: 'static' (the default), it emits a dist/ folder of plain HTML, CSS, and JS files. No server. No runtime. No cold starts.
Deployed to Vercel, these files sit on edge infrastructure in 18+ regions globally. A user in Tokyo requesting your London-origin site gets it from a node in Singapore, typically in under 50ms TTFB.
Compare this to a Node.js SSR app: every request hits a serverless function, which cold-starts in ~200–500ms, fetches data, renders HTML, and returns it. Fast enough for dynamic content. Wasteful for content that doesn’t change between deploys.
For most of our clients’ sites, content changes weekly at most. There is no reason to pay the SSR tax on every page load.
When We Don’t Use Astro
We’re not evangelists. Astro isn’t the answer to everything.
Use Next.js or Remix when:
- You have authenticated routes with per-user dynamic data
- You need edge-rendered personalisation (A/B testing on content, geolocation-gated pricing)
- Your team already has a large React codebase and onboarding cost matters more than output performance
Use Astro when:
- It’s a marketing site, portfolio, blog, or documentation site
- SEO and Core Web Vitals are priorities
- You’re deploying to a static CDN and want zero infrastructure complexity
- You want a codebase that’s easy to hand off and maintain without a React expert on staff
The majority of the projects we’re hired for fall into the second category. For those, Astro is the right default, and the performance difference is measurable from day one.
If you’re evaluating frameworks for a new project and want a straight opinion on what’s right for your use case, get in touch. We’ll give you an honest answer, even if that answer is something other than what we’d build.
Related reading
Ready to work together?
Get in Touch