Astro and Content-First Sites

Astro 7.3.3 17,728 inverts the default. A page is a .astro file whose frontmatter runs at build time (or per request, with an adapter) and whose markup becomes HTML. React 7,897 components are islands: they ship JavaScript only when marked with a client:* directive, and each hydrates independently. Scaffold with npm 2,036 create astro@latest shop -- --template minimal, then npx astro add react --yes.

An Astro page with one React islandTSX
---
import Counter from "../components/Counter.tsx";
const products = [{ id: 1, name: "Kettle" }, { id: 2, name: "Toaster" }];
---
<html lang="en">
  <head><meta charset="utf-8" /><title>Shop</title></head>
  <body>
    <h1>Shop</h1>
    <ul>{products.map((p) => <li>{p.name}</li>)}</ul>
    <Counter client:visible />
  </body>
</html>

The built dist/index.html contains the real list — <ul><li>Kettle</li><li>Toaster</li></ul> — plus a two-line IntersectionObserver shim that loads the counter when it scrolls into view. Delete the <Counter> line and Astro emits zero JavaScript files. That is the whole argument: a page that needs no interactivity pays nothing for React.

Islands are not free, though. That one island produced 221.3 kB of client JavaScript, 69.9 kB gzipped — React's runtime, the adapter and the component — roughly what the entire Vite 25,978 SPA cost. Islands save bytes across a site whose pages are mostly static; they save nothing on a page that is one large application.

Astro fits documentation, marketing sites, blogs, course material and storefront catalogs: many pages, mostly text, a few widgets, and search ranking that matters. It fits badly when state must be shared across widgets, because islands are separate React roots that do not share context — you end up gluing them together through localStorage or a nanostore.

On larger products the sensible pattern is two tools: Astro for the marketing and docs site at example.com, a React framework for the application at app.example.com.