blense.
HomeFrontendCodeAINewsGuidesCheatsheetsAbout

Technology decoded, always.

A curated read on AI, product, and the culture behind the code. No noise, no hype.

blense.

Technology with focus. Stories about what innovation really changes.

Sections

FrontendArtificial IntelligenceCode & DevTech News

Blense

AboutRSSPrivacy policyTermsCookies
© 2026 Blense · blense.fun
Code & Development

Astro: Island Architecture, Performance, and Where the Framework Truly Fits

Astro challenges the modern web status quo by prioritizing static HTML and isolating interactivity into independent, lightweight code islands.

Gby Genildo SouzaAug 338 min read
Astro: Island Architecture, Performance, and Where the Framework Truly Fits
Astro and Islands Architecture
  • 1Astro prioritizes static HTML by default, shipping JavaScript only to specific components that require interactivity.
  • 2The Islands Architecture allows developers to isolate interactive components, preventing the overhead of full-page hydration.
  • 3Astro is framework-agnostic, enabling the use of different libraries like React or Svelte within the same page.
  • 4Fine-grained hydration directives like client:visible and client:idle give developers precise control over when scripts load.
  • 5By reducing JavaScript, Astro improves performance for human users and makes content easier for AI crawlers to process.

1. Introduction and Concepts

Every framework carries a bet about what the web should be. React bet on components and state. Next.js bet on hybrid, application-centric rendering. Astro made a different bet: most of what we put on screen never needed JavaScript in the first place, so the framework should start from absolute zero client-side code and only add what's strictly necessary.

That sentence sums up the entire project. Astro is a framework for building content-driven sites — blogs, documentation, e-commerce, marketing, portfolios — that compiles pages to static HTML by default and only ships JavaScript to the browser when a component explicitly asks for interactivity.

1.1 The problem Astro solves

Frameworks like React, Vue, and Svelte were born to build applications: dashboards, internal tools, SaaS products. When used to assemble a content site, they carry a cost that doesn't match the problem — the entire framework runtime, the hydrator, the router, often tens or hundreds of kilobytes of JavaScript, even when 90% of the page is static text. That excess has a name: the "JavaScript tax" a user pays before they can interact with a page that, in practice, barely needs any interaction at all.

Astro flips the question. Instead of "how do I make this JavaScript application also render on the server," it asks "how do I keep the page as pure HTML and open pointed exceptions for JavaScript." The difference sounds subtle, but it reshapes the framework's entire architecture.

That decision solves a second problem, just as real as the first, though rarely discussed in the same breath: the cost of serving content to readers who never execute JavaScript at all. A growing share of that traffic doesn't come from a human behind a browser — it comes from AI agents: training crawlers, real-time search systems, assistants browsing pages on someone's behalf. That kind of client doesn't wait for hydration and doesn't run scripts: it reads whatever HTML arrives and extracts what it needs from that alone. A page that's already clean, complete HTML — one that doesn't depend on JavaScript to exist — costs a fraction of the tokens to process for these agents. The same architecture that eliminates JavaScript overhead for the browser reduces, for the exact same structural reason, the cost of machine reading. It's one pillar, serving two audiences.

1.2 Islands Architecture

The term "component island" was coined in 2019 by Katie Sylor-Miller, then front-end architect at Etsy, and elaborated the following year by Jason Miller, creator of Preact. Astro was the first mainstream framework to build this pattern as the foundation of its architecture, rather than a later optimization.

The core idea: the page renders almost entirely as static HTML at build time or on the server. Inside it, specific components — the islands — are isolated and hydrated individually on the client, each with its own slice of JavaScript, independent of the rest. One island doesn't know another exists, unless you explicitly connect them.

This solves two problems at once:

  • Minimal payload. If the page has an image carousel and a newsletter form, only those two components' JavaScript is shipped — not the entire framework, not the router, not hydration logic for parts that are already dead HTML.

  • Failure and framework isolation. Because each island hydrates independently, it's possible to have a Svelte carousel and a React form on the same page, each within its own boundary. Astro was born framework-agnostic and stays that way.

The gain becomes clear when you compare the payload side by side: in a traditional SPA, the user pays the cost of runtime, router, and hydration before they can even interact with a page that, in practice, is mostly text. In Astro, that cost practically disappears — HTML is the framework's natural output, not an intermediate step on the way to JavaScript.

In practice, an Astro page looks like this:

ASTRO
---// src/pages/index.astro// This block runs only on the server/at build time. It never reaches the browser.import Layout from '../layouts/Layout.astro';import Newsletter from '../components/Newsletter.jsx'; // React componentimport Counter from '../components/Counter.astro';const products = await fetch('https://api.store.com/products').then(r => r.json());---<Layout title="Astro Store">  <h1>Catalog</h1>  <ul>    {products.map((p) => (      <li>{p.name} — ${p.price.toFixed(2)}</li>    ))}  </ul>  <!-- Interactivity island: only this component ships JS to the client -->  <Newsletter client:visible /></Layout>

Notice what happens here: the product list is fetched on the server and rendered as pure HTML — no listing JavaScript reaches the browser. The Newsletter component, written in React, is the page's only island, and it only hydrates once it enters the viewport, thanks to the client:visible directive.

1.3 Hydration directives: the fine-grained control that defines Astro

What makes islands architecture practical, and not just conceptual, is the set of client:* directives that control when and how each island comes to life in the browser:

Directive

When it hydrates

client:load

Immediately, as soon as the page loads

client:idle

When the browser is idle (requestIdleCallback)

client:visible

When the component enters the viewport (IntersectionObserver)

client:media={query}

When a CSS media query is satisfied (e.g., large screens only)

client:only={"react"}

Renders client-side only, skipping server HTML

This granularity is what separates Astro from a simple "SSG with React bolted on." A typical e-commerce site might have a menu that hydrates with client:idle (not critical), a shopping cart with client:load (critical from the first moment), and a recommendations widget that only hydrates with client:visible, because the user might not even scroll to it.

In 2024 Astro introduced Server Islands, which extend this logic to the server: a component can be marked as deferred, with a fallback HTML sent immediately while the server fetches dynamic or per-user data (a cart counter, a personalized greeting) in the background, without blocking the rest of the page. This solves a classic limitation of the original islands model: pages with a segment that depends on per-request data no longer have to choose between waiting for everything or pushing everything to the client.


2. Comparative Analysis: Where Astro Wins and Where It Suffers

No architecture solves every problem equally well — and a technical article that promises otherwise doesn't deserve your trust. The line between where Astro's bet pays off and where it charges a price starts here, with no middle ground.

2.1 Strengths

Performance by default. Because HTML is the framework's natural output and JavaScript is opt-in, Astro projects tend to start out with meaningfully better Core Web Vitals — LCP, TBT, CLS — than equivalents built on SPA-centric frameworks, without the team having to configure anything special for it. The phrase that sums up the project's philosophy, taken from its own documentation, is direct: it should be nearly impossible to build a slow site with Astro.

SEO. Search engines index rendered HTML; content that depends on client-side hydration to appear has historically been riskier for crawling and indexing, even with recent crawler improvements. Because Astro delivers complete HTML by default — without depending on JavaScript for the main content to exist — technical SEO comes for free, with no need for parallel rendering strategies.

A counterpoint belongs here, because it's easy to overstate the difference. Frameworks like Next.js, with App Router and Server Components, are also capable of delivering complete, well-indexed HTML — the difference isn't "who can do SEO," but how much effort it takes to get there. In Astro, ready-to-go HTML is the path of least resistance: it's what happens when you don't do anything special. In Next.js, the same result depends on making the right calls along the way — watching where the "use client" boundary falls, being careful about when generateMetadata depends on dynamic data like cookies or headers (which can become a bottleneck and delay metadata if not handled properly), and making sure content inside a <Suspense> isn't missing for crawlers that don't wait for streaming to finish.

In practice, Google and the most important crawlers today execute JavaScript and handle streaming reasonably well, so the pure indexing gap tends to be smaller than it was a few years ago. But smaller search engines, link-preview bots (Open Graph, Twitter Cards), technical SEO scrapers, and monitoring tools — which frequently don't execute JavaScript — still benefit far more from HTML that's ready immediately. That's why a quick test in Astro tends to "feel" more native: it's not technically impossible to match in another framework, but it's the default outcome instead of a goal you have to configure toward.

Zero UI-layer framework lock-in. A project can have .astro, React, Vue, Svelte, and Solid components coexisting. This enables incremental migrations: a team looking to move off a legacy stack can rewrite section by section, without stopping everything for a full rewrite.

Typed Content Collections. For content-heavy Markdown/MDX sites, Astro offers a typed collections system via Zod, where the frontmatter schema is validated at build time. A malformed date field or a missing title becomes a build error, not a production bug.

Deploy portability. Official adapters for Node, Cloudflare, Vercel, Netlify, and Deno put Astro in a relatively neutral position relative to providers — important for teams that don't want to couple their front-end architecture to a single hosting service.

2.2 Weaknesses

This is where technical honesty matters more than launch-day enthusiasm.

Heavily interactive applications struggle with the islands model. An analytics dashboard with dozens of widgets connected by shared state, real-time WebSocket updates, and dense client-side navigation is exactly the scenario where islands architecture — designed for isolation — starts working against you. Islands are independent by design; orchestrating global state across multiple islands requires external solutions (nanostores, Zustand exposed via window, an event bus) that React or Vue would solve natively with a single Context or store.

The antidote in practice: Nanostores as a neutral third place

The previous paragraph described the problem in the abstract; it's worth seeing the solution in code, because this is where theory becomes an engineering decision. Concretely: an "Add to cart" button, written in React, needs to notify a cart widget, written in Vue, that an item was added. The two islands run in completely separate render trees — there's no React Context or Vue provide/inject that crosses that boundary, because, technically, there's no single component tree joining the two.

This is exactly the problem Astro's official documentation recommends solving with Nanostores: a framework-agnostic atomic state management library that lives outside any render tree. A Nanostores store doesn't belong to React or to Vue — it's just a JavaScript object in memory, in the browser, that any island can import and subscribe to. Each framework connects to it through a thin adapter (@nanostores/react, @nanostores/vue), but the state itself doesn't know, and doesn't need to know, who's listening.

Installing Nanostores and the adapters

BASH
npm install nanostores @nanostores/react @nanostores/vuenpx astro add vue

Creating the shared store, outside any component

TS
// src/stores/cart.tsimport { atom } from 'nanostores';export interface CartItem {  id: string;  name: string;  quantity: number;}// This atom doesn't belong to any framework. It's just in-memory state,// living outside any render tree — React's or Vue's.export const $cart = atom<CartItem[]>([]);export function addToCart(item: Omit<CartItem, 'quantity'>) {  const current = $cart.get();  const existing = current.find((i) => i.id === item.id);  if (existing) {    $cart.set(      current.map((i) =>        i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i      )    );  } else {    $cart.set([...current, { ...item, quantity: 1 }]);  }}

The React island that writes to the store

TSX
// src/components/AddButton.tsximport { addToCart } from '../stores/cart';interface Props {  id: string;  name: string;}export default function AddButton({ id, name }: Props) {  return (    <button onClick={() => addToCart({ id, name })}>      Add {name} to cart    </button>  );}

This component doesn't even need the useStore hook — it just calls the addToCart function, which updates the store. It writes to global state, but doesn't need to read it.

The Vue island that reads and reacts to the store

VUE
<!-- src/components/CartWidget.vue --><script setup lang="ts">import { useStore } from '@nanostores/vue';import { $cart } from '../stores/cart';// useStore(), from the Vue adapter, turns the Nanostores atom// into a reactive Vue `ref` — without Vue needing to know// that the state was born outside its own reactivity system.const cart = useStore($cart);</script><template>  <aside class="cart-widget">    <h3>Cart ({{ cart.length }})</h3>    <ul>      <li v-for="item in cart" :key="item.id">        {{ item.name }} × {{ item.quantity }}      </li>    </ul>  </aside></template>

Combining both islands on the same page

ASTRO
---// src/pages/store.astroimport Layout from '../layouts/Layout.astro';import AddButton from '../components/AddButton.tsx';import CartWidget from '../components/CartWidget.vue';---<Layout title="Store">  <main>    <h1>Catalog</h1>    <!-- Island 1: React. Only writes to the store. -->    <AddButton id="prod-1" name="Keyboard" client:load />    <AddButton id="prod-2" name="Mouse" client:load />    <!-- Island 2: Vue. Only reads and reacts to the store. -->    <CartWidget client:load />  </main></Layout>

When you click "Add Keyboard to cart," neither island talks directly to the other, and neither knows the other exists. Here's what actually happens: the React component calls addToCart, which updates the Nanostores atom; the atom, in turn, emits a change event; the @nanostores/vue adapter, which has an active subscription through useStore, receives that notification and marks Vue's reactive ref as changed; Vue, following its own normal reactivity cycle, re-renders <CartWidget> — all of this without React and Vue having any knowledge of each other.

That's the point worth holding onto: Nanostores isn't a bridge between React and Vue — it's a third, neutral place, outside both render trees, that the two frameworks each look toward independently. It's the same philosophy behind islands architecture, applied to the state problem: instead of forcing two islands to know each other, you create a shared source of truth that neither one needs to "own."

No native client-side routing with preserved state. Unlike an SPA, Astro's default navigation reloads the document — Astro's View Transitions API smooths that transition visually, but the mental model is still one of pages, not a single-page application with persistent in-memory state across routes.

Authentication and sessions require more manual work. Next.js, with middleware, server actions, and a more mature ecosystem around authentication (NextAuth/Auth.js was built with it in mind first), shortens the distance between "I need login" and "login working." In Astro, this usually means integrating external libraries and writing session logic by hand.

Smaller plugin ecosystem. Astro is newer and more niche than React/Next.js. For very specific features, the odds of finding a ready-made, mature integration are lower, and the team frequently has to write the bridge itself.

The decision curve around "how much of the project is an application." Hybrid projects — a mostly static site with one full application section (a logged-in customer dashboard, for example) — force the team to decide where the "Astro site" ends and the "embedded React application" begins. When poorly planned, this seam produces two codebases competing for the same responsibility.

2.3 A direct way to decide

A practical rule, adopted by experienced teams: if the question "is this content or is this an application?" has a clear answer, and that answer is mostly "content," Astro tends to pay off. If the answer is "a dense application, with heavy shared state and real-time interactions," frameworks like Next.js, Remix, or a plain React SPA solve it with less friction. Many real projects, however, are both at once — and that's exactly where the following sections come in.


3. Ecosystem and Integration

3.1 Astro and Vite: engine, not sidekick

Astro doesn't implement its own bundler. It's built on top of Vite, using it for the dev server, HMR (Hot Module Replacement), and production builds via Rollup. This means most of the Vite plugin ecosystem works inside an Astro project without adaptation, and the development experience — near-instant reloads, dependency pre-bundling — is inherited directly from Vite.

Configuring Astro is, to a large extent, configuring Vite with an additional layer on top:

JS
// astro.config.mjsimport { defineConfig } from 'astro/config';import react from '@astrojs/react';import tailwind from '@astrojs/tailwind';export default defineConfig({  integrations: [react(), tailwind()],  vite: {    // Any valid Vite option can be passed here    resolve: {      alias: { '@': '/src' },    },  },});

3.2 Integrating React (and other UI frameworks)

The @astrojs/react integration is the most-used bridge between the islands model and the React ecosystem. Installing it registers the renderer Astro needs in order to hydrate .jsx/.tsx components as islands:

BASH
npx astro add react

This adds the integration to astro.config.mjs and installs react, react-dom, and @astrojs/react automatically. From there, any React component can be imported into a .astro file and given a client:* directive:

TSX
// src/components/Counter.tsximport { useState } from 'react';interface CounterProps {  initialValue?: number;}export default function Counter({ initialValue = 0 }: CounterProps) {  const [count, setCount] = useState(initialValue);  return (    <div className="counter">      <button onClick={() => setCount((c) => c - 1)}>-</button>      <span>{count}</span>      <button onClick={() => setCount((c) => c + 1)}>+</button>    </div>  );}
ASTRO
---// src/pages/product/[id].astroimport Counter from '../../components/Counter.tsx';---<html lang="en">  <body>    <h1>Product</h1>    <!-- Only this component loads React in the browser, and only once visible -->    <Counter initialValue={1} client:visible />  </body></html>

Notice the detail: props passed from .astro to the React component are serialized and rehydrated on the client. Non-serializable functions and objects don't cross that boundary — a common mistake for developers coming from a 100% React project is trying to pass a complex callback straight from a server context.

3.3 TypeScript and TSX in Astro

TypeScript support is first-class: .astro files accept the frontmatter fence (---) with TypeScript code directly, and Astro generates type checking via astro check. Configuring tsconfig.json correctly is what ensures type errors surface before deploy, not after:

JSON
{  "extends": "astro/tsconfigs/strict",  "compilerOptions": {    "jsx": "react-jsx",    "baseUrl": ".",    "paths": {      "@/*": ["src/*"]    }  }}

.tsx (React) and .astro components share the same type graph when properly configured — props typed via interface or type are checked at the boundary between the two worlds, which eliminates an entire class of integration bugs:

TS
// src/types/product.tsexport interface Product {  id: string;  name: string;  price: number;  stock: number;}
ASTRO
---import type { Product } from '../types/product';import ProductCard from '../components/ProductCard.tsx';const products: Product[] = await fetch('/api/products').then(r => r.json());---{products.map((product) => (  <ProductCard product={product} client:visible />))}

3.4 Astro and Next.js: comparison, not hierarchy

It's tempting to treat Astro and Next.js as direct competitors on the same shelf, but they optimize for different problems:

Criterion

Astro

Next.js

Default mental model

Multi-page site, HTML-first

React application, with App Router and Server Components

JavaScript shipped by default

Zero, except explicit islands

Depends on the component tree and "use client" usage

Best fit

Content, marketing, documentation, blogs, catalog e-commerce

Dashboards, SaaS, products with auth and complex state

UI framework

Agnostic (React, Vue, Svelte, Solid, Preact)

React only

Client-side routing

Limited (View Transitions smooths it, but it's not a full SPA)

Native and deep (App Router, streaming, parallel routes)

Auth/session ecosystem

Requires manual integration

Mature (Auth.js was built mostly with it in mind)

The practical conclusion experienced engineering teams have been landing on isn't "which framework is better," but "which architectural question is this project asking." An institutional site with a blog section doesn't need Next.js's application Router. A SaaS with authentication, permissions, and complex session state is unlikely to benefit from starting over on top of islands. It's common, in fact, to see both tools coexisting in the same organization, each in the domain where it pays off.


4. Micro Frontend Architecture with Astro

Before diving into this section, it's worth mapping the terrain: what follows solves a very specific problem, and recognizing that problem is what decides whether it's yours. The question isn't "does Astro support micro frontends?" — it does, and well. The right question is different: does your organization already have multiple teams that need, for reasons of scale or structure, independent deploy cycles for different parts of the same user experience? If the answer is yes, the rest of this section is the map for making that work without pitfalls. If the answer is no, it's still useful — because understanding where that boundary sits is what keeps a team from complicating its own deploy to solve an organizational problem it never actually had.

4.1 Why the question makes sense

Micro frontends apply the same logic as backend microservices to the UI layer: split a large application into independent units, developed and deployed by different teams, then composed into a single experience — typically coordinated by a "shell" application. This solves real problems for large organizations: teams that can no longer share a single repository without stepping on each other, deploy cycles that need to be independent, or different technology stacks coexisting out of the need for gradual migration.

Historically, composing server-rendered (SSR) micro frontends was considered complex — usually solved with Server-Side Includes, Edge-Side Includes, or Ajax-based composition, techniques that require dedicated edge infrastructure. This is exactly where Astro's islands architecture becomes interesting as a solution, not just a style.

4.2 Strategy 1 — Islands as units of composition

The most natural way to apply Astro's model to micro frontends is to treat each island as a team's ownership boundary. An Astro "shell" application defines the layout, navigation, and shared skeleton; each team delivers a component (React, Vue, Svelte — whatever makes sense for that team) that becomes an isolated island within that layout:

ASTRO
---// src/pages/dashboard.astro — the "shell"import Layout from '../layouts/Shell.astro';import SalesPanel from '../components/teams/sales/SalesPanel.tsx';import SupportPanel from '../components/teams/support/SupportPanel.vue';import InventoryPanel from '../components/teams/inventory/InventoryPanel.svelte';---<Layout title="Operations Dashboard">  <!-- Each panel is delivered and versioned by a different team -->  <section aria-label="Sales">    <SalesPanel client:visible />  </section>  <section aria-label="Support">    <SupportPanel client:visible />  </section>  <section aria-label="Inventory">    <InventoryPanel client:idle />  </section></Layout>

Because each island hydrates in isolation, different teams can use different frameworks without one leaking into another — something a monolithic SPA can hardly allow without extra tooling like Module Federation.

4.3 Strategy 2 — Server Islands for distributed runtime composition

The limitation of Strategy 1 is that it still assumes a single repository (or at least a single build process) gathering all the islands together. For real micro frontends — with independent deploys, each served from its own origin — the relevant feature is Server Islands, which let a component be fetched and rendered on the server asynchronously, with an immediate fallback, without the whole page having to wait for it:

ASTRO
---// src/components/RemoteFragment.astroexport const prerender = false;interface Props {  endpoint: string;}const { endpoint } = Astro.props;// Fetches HTML already rendered by another service/team, from another originconst response = await fetch(endpoint, {  headers: { 'x-requester': 'astro-shell' },});const html = await response.text();---<div class="remote-fragment" set:html={html} />
ASTRO
---// src/pages/index.astroimport RemoteFragment from '../components/RemoteFragment.astro';---<main>  <h1>Corporate Portal</h1>  <!-- Renders on the server, without blocking the rest of the page -->  <RemoteFragment    server:defer    endpoint="https://marketing-team.company.com/api/fragment"  >    <div slot="fallback">Loading promotions…</div>  </RemoteFragment></main>

This pattern brings Astro close to the classic Edge-Side Includes composition model, but without requiring dedicated edge infrastructure — the composition happens right on the Astro server, with the added benefit of a declarative fallback and HTML streaming.

4.4 A close relative: Next.js's Partial Prerendering

A parenthesis belongs here, because it's common — and telling — to notice that Next.js arrived at a very similar idea by a different path. Partial Prerendering (PPR), which became stable in Next.js 16 (October 2025) as part of the Cache Components feature, solves exactly the same problem as Astro's Server Islands: how to serve an instant static shell while also including dynamic, personalized segments without blocking the whole page.

The mechanics differ, but the visible result is nearly identical. Instead of a dedicated directive like server:defer, PPR uses React's own <Suspense> as the boundary: everything outside a Suspense is rendered at build time and becomes the static shell, cacheable at the CDN; everything inside a Suspense is treated as dynamic, runs as a Server Component at request time, and is streamed into the same HTML, filling the declared fallback as soon as the data arrives:

TSX
// app/dashboard/page.tsx — Next.js with PPRimport { Suspense } from 'react';import { UserGreeting, GreetingSkeleton } from './greeting';export const experimental_ppr = true;export default function Page() {  return (    <section>      <h1>This is prerendered at build time</h1>      <Suspense fallback={<GreetingSkeleton />}>        {/* This uses cookies()/headers() and can only run per request */}        <UserGreeting />      </Suspense>    </section>  );}
ASTRO
---// index.astro — Astro with Server Islandsexport const prerender = false;---<section>  <h1>This is prerendered at build time</h1>  <UserGreeting server:defer>    <div slot="fallback">Loading greeting…</div>  </UserGreeting></section>

Placed side by side, both snippets tell the same story in different vocabularies: a static shell that arrives instantly, an explicit boundary around what's dynamic, a fallback that shows first, and the real content arriving via streaming inside the same HTTP response — no second request from the client. The structural difference is in who decides the boundary: in Astro, you explicitly declare server:defer component by component, as a composition decision; in Next.js, the boundary is born from React's own Server Components and Suspense model, and the build fails with an error if you use a dynamic API (cookies(), headers()) outside a Suspense — the framework forces the boundary, rather than suggesting it.

For developers coming from the React world, this proximity is the most common reason to get curious about Astro: if PPR already showed, inside the Next.js ecosystem, that "static by default with declared dynamic holes" is a mental model that works well in production, Astro offers exactly that same philosophy — but as the starting point of the entire architecture, not as an optional feature inside a framework built for applications first. Where PPR "escapes" from 100% SPA-style rendering to gain speed, Astro starts from static zero and "escapes" into dynamism only when asked — two opposite paths converging on the same core idea.

4.5 The real limitations of this approach

It's important not to sell this strategy as a finished, definitive solution. Astro's own team publicly discusses, in its roadmap repository, the scenario of composing fragments from multiple independent Astro servers on the same page — and acknowledges this can create conflicts around global state, DOM elements, or client-side events when each fragment loads its own hydration without coordination across origins. In other words: Astro handles composing islands well within a single application/shell, but composing multiple independent Astro applications, each with its own client-side JavaScript lifecycle, still requires manual conventions — configurable names for global elements and events, CSS isolation, and care to keep two islands from different origins from colliding by accident.

But the most underrated limitation isn't technical — it's operational, and it deserves to be said outright rather than left between the lines. The UI autonomy that islands architecture promises comes with a real infrastructure counterpart, and that bill arrives before the first line of remote-fragment code.

A concrete scenario helps move this out of the abstract. Imagine an e-commerce site split between two autonomous teams: the Catalog team works in Vue and publishes its artifacts to server A; the Cart team works in React and publishes to server B. Each picks its own stack, its own deploy pace — the promise of autonomy, kept to the letter. Except, to the end user, this all needs to look like a single site, on a single domain. Someone, at the network edge, has to decide which request goes where:

TYPESCRIPT
# Simplified edge router configurationroute "/products/*" -> origin_A   # Catalog team's server (Vue)route "/cart/*"      -> origin_B  # Cart team's server (React)route "/*"           -> shell     # Astro application composing the final result

This looks trivial until the moment server B, the cart's, hits an eight-hundred-millisecond delay — a traffic spike, a slow database query, the cause doesn't matter. That delay doesn't stay contained inside the Cart team: it leaks into any page that composes a fragment from origin B. If the Catalog's product page shows a cart summary in the corner of the screen, that entire page now waits on the slowest service in the composition's eight hundred milliseconds — even though the Catalog didn't change a single line of code. It's this invisible coupling, between teams that swore they were decoupled, that the list of responsibilities below tries to name one by one.

The reverse proxy or edge router stops being optional. The moment "several teams, several origins" becomes "one single domain for the end user," someone has to decide, per request, which origin answers which route — and that someone is, invariably, a routing layer sitting in front of everything: Nginx, an API Gateway, or an edge router running on a CDN (Cloudflare Workers, Lambda@Edge, Akamai EdgeWorkers). That layer isn't a configuration detail — it becomes a piece of infrastructure with its own owner, versioning, and SLA. It needs to know, by path prefix or business rule, whether /inventory goes to Team A's Astro server and /sales to Team B's; it needs to handle different caching rules per origin (the sales fragment might change every minute, the inventory one once an hour); and when Astro itself fetches a remote fragment via server:defer, that fetch also passes — or should pass — through that same router, to inherit the same timeout, retry, and inter-service auth header policies.

CORS and header propagation stop being a rare problem. When a Server Island makes a fetch to an origin different from the one that served the page, you explicitly inherit the same problems as any inter-service call: auth headers that need to be forwarded (or re-issued) at the boundary, CORS policies when composition doesn't happen entirely server-side, and a latency budget that now depends on the slowest origin in the chain — if the remote fragment takes 800ms to respond, the fallback stays visible for 800ms, no matter how fast the shell itself was.

CI/CD stops being "one pipeline" and becomes "N pipelines with a contract between them." Each micro frontend — each independent Astro application, in the multiple-servers scenario — gets, in practice, its own build, test, and deploy pipeline, which is exactly the point of having autonomous teams. The cost shows up in coordinating between them: the shell needs to keep working even when a fragment ships a new, incompatible version, which pushes the team toward practices like contract testing between shell and fragments, feature flags to roll out a new version gradually, canary releases to limit the blast radius of a buggy fragment, and observability that can trace a failure back to the right origin when the problem only shows up in the final composition, not in any isolated fragment. None of these practices is exclusive to Astro — they're the price of any server-rendered micro-frontend architecture — but it's important to go into this decision knowing they aren't optional: without them, deploy autonomy turns into production instability.

This puts Astro in a specific spot on the micro-frontend spectrum: simpler and cheaper than Module Federation for scenarios where composition happens within a single, controlled shell; less mature than dedicated solutions (single-spa, Webpack/Rspack Module Federation) for scenarios where each micro frontend is truly autonomous, with its own deploy cycle and origin, hydrating side by side with no central coordination. And in either scenario, the decision to adopt micro frontends via Astro should be made together with whoever will operate the reverse proxy and the pipelines — not just with whoever will write the components.

4.6 When to choose Astro for micro frontends

It makes sense to consider Astro as a micro-frontend foundation when:

  • Content is mostly server-renderable, and interactivity is localized to specific components, not the entire application.

  • Teams already work (or accept working) under a shared shell, rather than fully autonomous applications with independent origins and deploy cycles.

  • SEO and initial load performance are first-order requirements — a common scenario for corporate portals, e-commerce, and institutional sites composed by multiple teams.

It makes more sense to avoid Astro, or to use it only as a presentation layer on top of a more robust composition solution, when:

  • Each micro frontend needs a fully independent deploy and origin, with simultaneous, isolated hydration, with no central shell coordinating everything.

  • The application is predominantly interactive (dashboards, editors, productivity tools), which shrinks the "zero JS by default" advantage to nearly nothing, since almost everything becomes an island anyway.

  • The organization doesn't currently have a team or an established practice for operating a reverse proxy/edge router, cross-origin observability, and deploy coordination across independent pipelines — because in that case, the cost of building that capability from scratch tends to outweigh the UI-autonomy gain that motivated the decision in the first place.


5. Astro in the Age of AI Agents

The same architecture that eliminates JavaScript overhead for humans is exactly what reduces, by up to ten times, the tokens AI agents consume when reading a page. This isn't a coincidental side effect discovered after launch — it's the direct consequence of a single decision made in Astro's very first line of code: static by default, dynamic only where declared. What Section 1 presented as browser JavaScript savings and Section 4 showed as a central shell delivering coherent, complete HTML is, seen from another angle, exactly the infrastructure this new type of client values most.

Because that's what an AI crawler technically is: just another HTTP client making a request against the same shell that serves a browser — only more demanding. It doesn't execute JavaScript, doesn't wait for hydration, doesn't navigate by clicking links: it makes a request, gets a response, and extracts what it needs from that alone, with no second chance. If that shell's output is already clean, complete HTML — the same composition described in Section 4 — this non-human client gets exactly the advantage Astro was designed to deliver to humans: no script noise, no waiting for hydration, no dependency on code execution for the content to exist.

Put directly: Astro didn't need to adapt for the AI-agent era. It was already ready for it before it existed — the same architectural investment that serves a human user well pays a double dividend when the "user" is a machine. That doesn't exempt the framework from an honest answer about how it behaves in this scenario — not a marketing slide. The short answer is that it inherits this structural advantage almost for free — while, at the same time, facing a fast-changing area where no framework, Astro included, has a definitive standard yet.

5.1 The AI crawler as an "optimized client": ready HTML is cheap HTML

Most crawlers relevant to AI — training bots like GPTBot and ClaudeBot, or real-time-answer indexing bots like OAI-SearchBot — don't consistently execute JavaScript, nor do they pay the cost of waiting for hydration. In that sense, they behave like an even more demanding version of the composition scenario we saw in Section 4: where a human will tolerate (to a point) waiting for an island to hydrate, an AI crawler simply doesn't wait — it reads whatever arrived in the HTTP response and moves on. When these bots hit a page full of navigation markup, scripts, ads, and hydration layers before the real content, they spend context window and processing time on noise. Companies that switched to serving clean Markdown instead of dense HTML have reported up to a 10x reduction in the tokens needed to process the same page.

Because Astro delivers static, semantic HTML by default — the same trait that favors traditional SEO and that we discussed as the basis for micro-frontend composition — an AI agent crawling an Astro site tends to receive clean content with no extra effort, for exactly the same reason a traditional search crawler does. This advantage wasn't designed with AI in mind; it's a side effect of an architecture that already prioritized ready-to-go HTML for any client, human or not. This isn't exclusive to Astro — any predominantly static site benefits the same way — but it's consistent with the rest of this article: in Astro, this behavior requires no configuration.

5.2 llms.txt: a promising standard, with real adoption still uncertain

A proposed standard called llms.txt emerged in 2024 — a plain-text file, at the site root, acting as an agent-readable "summary": a curated list of important pages, with direct links to Markdown versions. The idea is appealing and technically simple to implement in Astro, since generating a text endpoint at build time is trivial:

TS
// src/pages/llms.txt.tsimport { getCollection } from 'astro:content';export async function GET() {  const posts = await getCollection('blog');  const lines = posts.map(    (post) => `- [${post.data.title}](/blog/${post.slug}.md): ${post.data.description}`  );  const content = [    '# My Site',    '',    '> Documentation and technical articles.',    '',    '## Articles',    ...lines,  ].join('\n');  return new Response(content, {    headers: { 'Content-Type': 'text/plain; charset=utf-8' },  });}

But honesty matters here: the enthusiasm around llms.txt outran the data. In July 2025, Google's own Search team publicly stated it doesn't use, and doesn't plan to use, llms.txt as a signal — going as far as comparing it to the long-dead keywords meta tag. No major LLM provider (OpenAI, Anthropic, Google, Meta) has publicly committed to treating the file as an indexing or answer signal. Recent surveys show adoption around 10% of analyzed domains, and traffic from training and AI-search bots to the /llms.txt file itself is, according to bot-traffic analyses, statistically negligible.

What survives from this proposal, in practice, is a narrower use than originally intended: llms.txt is settling in as a navigation layer for coding agents — tools like Cursor, Claude Code, and GitHub Copilot, which fetch technical documentation on demand while helping a developer — rather than as a ranking or citation mechanism in general-audience AI search.

5.3 A telling case: Astro itself dropped its own llms.txt

A recent episode illustrates just how much this ground is still shifting. Astro's own team, which had been an early adopter of publishing an llms.txt in its official documentation, removed the file in 2026. The maintainers' reasoning wasn't a lack of interest in AI agents — quite the opposite: it was the assessment that a static text file was no longer the best way to serve that audience. In its place, Astro now offers an MCP (Model Context Protocol) server for its documentation, letting tools like Claude Code query the docs interactively and in a structured way, instead of downloading a static index and guessing what's relevant.

This case is a lesson in technical humility: the "correct" way to prepare a site for AI agents still isn't a settled standard — it's being negotiated in real time among LLM providers, framework maintainers, and the community. The same applies to this very article: written today, it has a short shelf life by definition.

5.4 What exists today in the Astro ecosystem

Despite the uncertainty over which standard will win, Astro's integration ecosystem already reflects this demand concretely. Today, the official integrations directory offers ready-made packages that automatically generate, at build time:

  • Markdown versions of every page, served alongside the HTML via HTTP content negotiation (Accept: text/markdown) or URL convention (/article.md next to /article).

  • robots.txt with per-AI-bot rules, allowing you to, for example, authorize OAI-SearchBot (used for ChatGPT answers) while blocking GPTBot (used for model training) separately — major providers today run distinct bots for each purpose, and granular access control happens in robots.txt, not llms.txt.

  • Structured data in JSON-LD, which remains the most established and widely supported signal — used both by traditional search engines and by AI systems that extract entities and facts from a page.

  • Experimental files like agents.md and .well-known/mcp.json, signaling that part of the community is already betting on MCP as the natural successor to the "static index file" model.

5.5 Where this leaves Astro

Putting the pieces together: Astro has no magic feature built exclusively for AI models, and it would be inaccurate to say it was "designed" for this era — islands architecture predates this debate by several years. What it has is an inherited structural advantage: clean, static HTML by default is simultaneously good for traditional SEO, good for social media previews, and cheap to process for any system — human or agent — that needs to extract content without the cost of executing JavaScript. That puts Astro in a comfortable position to adapt to whatever comes next, whether it's a standard like llms.txt or something closer to MCP, without needing a structural rewrite — just one more integration in the build.

The same caution that guides the rest of this article applies here: none of these tools — llms.txt, parallel Markdown, MCP — should be treated as a settled certainty. This is, today, the fastest-moving ground in the entire web ecosystem, and the best technical posture is to revisit this section periodically, not treat it as resolved.


6. Conclusion

Islands architecture isn't a feature of Astro — it's the reason the framework exists. Every piece of analysis in this article, from performance and SEO advantages to limitations in dense applications, traces back to the same original choice: treat client-side JavaScript as the exception, not the default.

The elite dev's arsenal.
Microfrontends with Angular and Nx: what nobody tells you15 minStop using forEach: 8 semantic array methods to make your JavaScript code readable8 minTanStack Query: How to Eliminate Manual Server State Management in React6 minAngular Micro-frontends: How to Scale Large Applications and Eliminate Monolithic Build Times with Module Federation10 min

That makes Astro excellent at what it was designed for — content-driven sites that need to load fast and index well — and deliberately less suited to applications where interactivity is the rule, not the exception. The question any team should ask before adopting Astro isn't "is this framework good?" but "is my product, at its core, content or application?" The honest answer to that question decides more than any benchmark will.

One final note, to close without leaving a false impression: the depth dedicated to micro frontends in Section 4 reflects the technical richness of the topic, not how often it should show up in your projects. For the overwhelming majority of cases, the right Astro is the simplest one — one application, one deploy, islands living side by side in the same codebase. Distributed architecture is a tool for a specific organizational problem, not a natural upgrade path for maturity.


Bonus: Advanced Implementation — Fragment Composition with Server Islands

This bonus assumes the ground is already covered: islands, client:* directives, TypeScript, and React integration are already part of your vocabulary after the previous four sections. There's no installation to walk through here, no folder structure to explain — that's a command away in the official documentation, and repeating it now would only push the reading further from what actually matters. What follows goes straight to this article's most advanced code: the server:defer directive consuming a remote endpoint, actually solving asynchronous server-side composition — no boilerplate in the way.

An endpoint standing in for "another team" serving content, with a real simulated latency to make the async behavior visible:

TS
// src/pages/api/fragment.tsexport async function GET() {  const html = `<div style="padding:1rem;border:1px solid #ccc;">    Fragment rendered by another service at ${new Date().toLocaleTimeString('en-US')}  </div>`;  return new Response(html, {    headers: { 'Content-Type': 'text/html' },  });}

Now, the component that fetches and inserts that fragment on the server, without blocking the rest of the page from loading:

ASTRO
---// src/components/RemoteFragment.astroexport const prerender = false;const response = await fetch(new URL('/api/fragment', Astro.url).toString());const html = await response.text();---<div class="remote-fragment" set:html={html} />

And, on the home page, add the call with server:defer and an immediate fallback:

ASTRO
---// src/pages/index.astro (added snippet)import RemoteFragment from '../components/RemoteFragment.astro';---<h2>Composed fragment (micro-frontend style)</h2><RemoteFragment server:defer>  <div slot="fallback">Loading fragment from the other service…</div></RemoteFragment>

Since Astro pre-renders statically by default, server:defer requires a server running on demand. Install the Node adapter to test this behavior locally:

BASH
npx astro add nodenpm run buildnpm run start

Load the page again: for a moment you'll see "Loading fragment from the other service…," and then the content fetched on the server appears — without the rest of the page having waited for it. This is exactly the mechanism described in Section 4.3, in miniature.

What this code solves

This is the actual mechanism behind Section 4's promise of distributed composition: a fragment from another origin, rendered on the server, without blocking the rest of the page and without requiring any client-side orchestration. The shared-state example between React and Vue via Nanostores, which closes the weakness raised in Section 2.2, no longer needs a separate lab — it lives right next to the argument it resolves, ready to be read in the heat of the doubt, not retrieved three sections later in an appendix.


References

  • Official Astro documentation — docs.astro.build/en/concepts/islands

  • Jason Miller, creator of Preact, on Islands Architecture (2020)

  • Public discussion by the Astro team on micro-frontend composition across multiple servers — github.com/withastro/roadmap/discussions/713

  • Technical article on Server Islands applied to micro frontends — Talent500 Engineering Blog

  • Astro vs. Next.js comparison (2026) — kunalganglani.com

  • Official Next.js documentation on Partial Prerendering — nextjs.org/docs/app/getting-started/partial-prerendering

  • "LLMs.txt in 2026: The Full Guide" — adoption and AI-bot traffic data (Limy, 2026)

  • "Astro removed its llms.txt" — account of the replacement of llms.txt with an MCP server in Astro's official documentation (Dachary Carey, 2026)

  • "Making your Astro site agent-friendly" — parallel Markdown implementation and content negotiation (Jimmy Guzman Moreno, 2026)

  • Official Astro integrations directory — astro.build/integrations

  • Official Astro documentation on sharing state between islands with Nanostores — docs.astro.build/en/recipes/sharing-state-islands

  • CI/CD, contract testing, and canary release practices for server-rendered micro frontends — Habsi Tech, "Building Resilient Micro Frontends"

  • Production account of implementing a reverse proxy to compose multiple front-end experiences — Asurion Product Development, "Not so micro-frontends: Building a Reverse Proxy"

Keep exploring
Want more content like this?

Check out other articles in the same vein and keep the momentum.

See more articles
#JavaScript#TypeScript

The elite dev's arsenal.

Microfrontends with Angular and Nx: what nobody tells you
Code & Development

Microfrontends with Angular and Nx: what nobody tells you

Every tutorial teaches how to set up microfrontends in minutes, but few reveal the architectural chaos that emerges after months of production deploys.

Genildo Souza · Aug 3 · 15 min
JavaScript Event Loop: The Heart of Non-Blocking Concurrency — Revisited in 2025
Code & Development

JavaScript Event Loop: The Heart of Non-Blocking Concurrency — Revisited in 2025

The Event Loop is not just a simple loop, but a sophisticated coordination mechanism between Call Stack, queues, and runtime. Essential for JS concurrency, it enables thousands of asynchronous operations without blocking the UI. This guide explores its structure, differences between Node.js and browsers, common bugs, advanced techniques, and 2025 trends like Structured Concurrency and scheduler.yield().

Genildo Souza · Jul 26 · 6 min
Next.js 16 PPR: Eliminate the static vs. dynamic tradeoff and achieve Edge TTFB with fresh data
Code & Development

Next.js 16 PPR: Eliminate the static vs. dynamic tradeoff and achieve Edge TTFB with fresh data

Partial Prerendering in Next.js 16 resolves the classic rendering tradeoff, delivering static speed with dynamic data in one seamless response.

Genildo Souza · Jul 13 · 14 min
In this article
  • 1. Introduction and Concepts
  • 1.1 The problem Astro solves
  • 1.2 Islands Architecture
  • 1.3 Hydration directives: the fine-grained control that defines Astro
  • 2. Comparative Analysis: Where Astro Wins and Where It Suffers
  • 2.1 Strengths
  • 2.2 Weaknesses
  • 2.3 A direct way to decide
  • 3. Ecosystem and Integration
  • 3.1 Astro and Vite: engine, not sidekick
  • 3.2 Integrating React (and other UI frameworks)
  • 3.3 TypeScript and TSX in Astro
  • 3.4 Astro and Next.js: comparison, not hierarchy
  • 4. Micro Frontend Architecture with Astro
  • 4.1 Why the question makes sense
  • 4.2 Strategy 1 — Islands as units of composition
  • 4.3 Strategy 2 — Server Islands for distributed runtime composition
  • 4.4 A close relative: Next.js's Partial Prerendering
  • 4.5 The real limitations of this approach
  • 4.6 When to choose Astro for micro frontends
  • 5. Astro in the Age of AI Agents
  • 5.1 The AI crawler as an "optimized client": ready HTML is cheap HTML
  • 5.2 llms.txt: a promising standard, with real adoption still uncertain
  • 5.3 A telling case: Astro itself dropped its own llms.txt
  • 5.4 What exists today in the Astro ecosystem
  • 5.5 Where this leaves Astro
  • 6. Conclusion
  • Bonus: Advanced Implementation — Fragment Composition with Server Islands
  • What this code solves
  • References