· 9 min read
React vs Next.js: When to Use Each (A Developer's Honest Take)
A practical comparison of React and Next.js — covering SSR vs SPA tradeoffs, performance, deployment complexity, and real project examples to help you choose the right tool.
· 5 min read · astro · performance · web development
A technical breakdown of how Astro's island architecture, content collections, and build pipeline make it the right default for most production web projects.
Ten Peaks Tech
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.
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.
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.
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.
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.
We’re not evangelists. Astro isn’t the answer to everything.
Use Next.js or Remix when:
Use Astro when:
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.
Keep reading
· 9 min read
A practical comparison of React and Next.js — covering SSR vs SPA tradeoffs, performance, deployment complexity, and real project examples to help you choose the right tool.
· 10 min read
Why your Shopify store is slow and how to fix it — covering theme bloat, app overhead, image optimization, Liquid rendering, and Core Web Vitals improvements with real metrics.
· 6 min read
A detailed walkthrough of how Ten Peaks Tech takes a project from initial brief to production deployment — including how we handle scope, architecture decisions, code review, and handoff.
A senior engineer replies within one business day.