How I kept this blog fast without giving up React
I used to think SSG and rich interactivity were mutually exclusive. Astro's island architecture let me keep most of the site static and add JavaScript only where it earns its keep.
This blog has interactive physics playgrounds, animated state flow visualizations, and React components with real state. It also stays lightweight because most of the content is still static HTML, and the JavaScript that does ship is attached to specific interactive pieces instead of the whole page.
That is not about chasing a purity metric like “zero JavaScript.” It is about being intentional with what runs in the browser.
I’m an iOS engineer by trade. When I decided to build a technical blog, my first instinct was to reach for Next.js — it’s what everyone recommends, and I already know React. I spun up a project, wrote a few pages, and deployed a version that still leaned on the usual client-heavy React model. Then I opened the Network tab.
The blog index — a list of article titles and dates with absolutely zero interactivity — was shipping 187KB of JavaScript in my setup. The browser was downloading React, React DOM, routing code, and the client-side pieces of my UI just to render static text.
On iOS, I’d never ship a framework to render a UILabel. Why was I doing it on the web?
The Epiphany
The fundamental problem with using a fully hydrated React app model for a content site is that you’re paying the JavaScript tax on content that doesn’t need JavaScript.
In the kind of client-heavy React setup I started with:
- Server sends HTML for the page (from an app shell or a pre-rendered route)
- Browser downloads ~150KB+ of JavaScript (React runtime + your code)
- JavaScript parses and executes (this takes 200-500ms on mobile devices)
- React hydrates the DOM, attaching event listeners to the already-rendered HTML
- The page is now interactive
For my blog index — a list of links — steps 2-4 are pure waste. The page was already interactive the moment the HTML arrived. Links work without JavaScript. The browser has had clickable anchor tags since 1993.
But my TCA visualization? My spring animation playground? Those genuinely need React. They have complex state, event handlers, and animated UI. The question was: how do I get the best of both worlds?
Astro’s Answer: Islands
Astro flips the SPA model on its head. Instead of “everything is JavaScript by default, opt-out where you can,” Astro says “nothing is JavaScript by default, opt-in where you need it.”
When I write a blog post in Astro, this happens at build time:
- Astro compiles my Markdown to HTML
- That HTML is written to a static
.htmlfile - JavaScript is only added for the components I explicitly hydrate
The result: the article content itself loads as plain HTML. The browser renders the content immediately, and Astro only sends client-side code for the parts I have deliberately marked as interactive.
But then I embed an interactive component:
---
// This runs at build time only
import InteractiveStoreDemo from '../../components/interactive/InteractiveStoreDemo.tsx';
---
Here's how TCA works in practice:
<InteractiveStoreDemo client:visible />
And here's why that matters for testing...
The client:visible directive is the key. It tells Astro: “Render this component’s HTML at build time for immediate display, but don’t load its JavaScript until the user scrolls to it.”
Here’s what happens in the browser:
- The page loads as static HTML. The interactive component renders as its initial HTML state immediately, so there is no loading placeholder and usually no visible layout shift.
- The user starts reading the article.
- When they scroll down and the component enters the viewport, Astro dynamically imports the component’s JavaScript chunk.
- React boots up, hydrates just that one component, and it becomes interactive.
The rest of the article stays ordinary markup. The interactive behavior is scoped to that island instead of turning the whole page into a client-side app.
What Changed in Practice
The benefits showed up in exactly the places I cared about:
- Static article content starts rendering immediately instead of waiting for an app shell to hydrate.
- Interactive demos stay isolated, so shipping one React component does not force the whole article into a client-side app model.
- The JavaScript budget is easier to reason about because every hydrated component is an explicit decision.
Implementation Details
A few implementation details mattered in practice.
Hydration Directives
Astro provides several hydration strategies:
{/* Load JS immediately on page load */}
<Component client:load />
{/* Load JS when the component scrolls into view */}
<Component client:visible />
{/* Load JS when the browser is idle */}
<Component client:idle />
{/* Load JS when a media query matches */}
<Component client:media="(max-width: 768px)" />
{/* Never hydrate — server-render only */}
<Component />
For the educational demos inside articles, I prefer client:visible. The user does not need a playground to boot before they have even reached it.
Elsewhere on the site, I still allow some client-side behavior where it adds real value, like reveal animations and interactive components. The point is not “no JavaScript anywhere.” The point is that every hydrated component needs a reason to exist.
MDX for Inline Components
Astro has official MDX support through @astrojs/mdx. This lets me write articles in Markdown and mix in React components wherever I need interactivity:
---
title: "Visualizing State in Astro"
---
import MyInteractiveChart from './MyInteractiveChart.tsx';
Regular markdown paragraph here...
<MyInteractiveChart client:visible />
More markdown continues here.
The markdown parts compile to static HTML. The React component gets island-hydrated. The boundary between static and dynamic is explicit and intentional.
Component Independence
Each island is independent at the React tree level. They do not share a common React tree or React Context by default. If I have two interactive components on the same page, Astro creates two separate React roots.
This is different from an SPA where everything lives in one React tree. The tradeoff: I can’t use React Context to share state directly between islands. The benefit: one island crashing doesn’t take down the page.
For my use case (educational demos embedded in articles), this is perfect. Each demo is self-contained by design.
What iOS Taught Me About Web Performance
Coming from iOS, I found Astro’s philosophy immediately intuitive. In UIKit, you’d never load an app’s entire view hierarchy into memory to display a single screen — UITableView recycles cells precisely because rendering is expensive. You load what’s visible, and lazily initialize everything else.
Astro does the same thing for JavaScript:
- Load what’s visible: Ship the HTML first
- Don’t touch what’s off-screen: Don’t hydrate components that aren’t in the viewport
- Lazily initialize: Load JavaScript only when the user actually needs it
The web development community spent a decade convincing itself that shipping megabytes of JavaScript to render static text was a necessary cost of a “modern” experience. It was never necessary. It was the default behavior of the tools. Those defaults have weight.
When Astro Is the Wrong Choice
Astro’s architecture assumes that most of your page is static, with occasional islands of interactivity. If you’re building a dashboard, a real-time chat app, or anything that’s mostly interactive, Astro adds friction without saving much.
Use Astro when:
- Most of your content is text, images, and static layouts
- You have a few isolated interactive components per page
- Page load performance is critical (content sites, blogs, documentation, marketing)
Use traditional SPAs when:
- Most of your UI is interactive (dashboards, editors, social feeds)
- Components need to share state extensively
- You need real-time updates across the entire page
For this blog, the choice was obvious. The content is 95% text with a few interactive demos. Astro lets me write React components when I need them without punishing users who just want to read.