Island Architecture — Astro + Jails

Just like the world, technology also moves in cycles. Fifteen years ago, I was working on front-end projects where applications and pages were rendered entirely on the server using template systems tailored to each platform: ERB in Ruby, PHP with its built-in templating, Razor in .NET, JSP in Java, and so on.
Over time, this approach was largely “replaced” by the rise of Single Page Applications (SPAs). But today, driven by the need for better SEO, performance, and user experience, we’re witnessing yet another shift in how we render web applications. And interestingly enough, this shift reintroduces concepts that feel both familiar and new — such as Island Architecture.
About Island Architecture
tl;dr: The islands architecture encourages small, focused chunks of interactivity within server-rendered web pages. The output of islands is progressively enhanced HTML, with more specificity around how the enhancement occurs. Rather than a single application being in control of full-page rendering, there are multiple entry points. The script for these “islands” of interactivity can be delivered and hydrated independently, allowing the rest of the page to be just static HTML. https://www.patterns.dev/vanilla/islands-architecture/
Whenever I had the chance, whether in the companies I worked for or in my own side projects, I tried to adopt this strategy. I knew it brought powerful principles of separation of concerns, performance, and simplicity.
The challenge, however, was always the same: in order to make it work, I had to build and maintain a sort of “project within the project.” It was a custom boilerplate that glued together multiple tools to generate static pages. And while some solutions existed for content-focused sites, there was nothing truly programmatic and flexible enough to also support building full applications — not just static content pages.
Fast forward to today, and that gap has been filled. We now have a robust, well-designed, and thoroughly documented solution. We have the underestimated, yet powerful, Astro.
Astro — The web framework for content-driven websites
Astro is a solution designed for building server-side web applications, particularly for statically generated sites (SSGs). It’s built to deliver minimal or zero JavaScript to the client while still providing seamless integration with other client-side tools and libraries. Astro offers directives that allow you to initialize these libraries on load or only when the user interacts with them.
For example, you can integrate Astro with React and use React exclusively on the client-side, freeing yourself from the headaches that come with frameworks like Next.js and the complexities of isomorphic rendering.
The Trade-offs of React
For me, React as a library comes with a series of challenges — not just structural, but also in terms of design choices that often make it more difficult than helpful.
One key design decision in React is the tight coupling between logic and view. While this approach is convenient in many situations, it introduces a number of trade-offs. One significant trade-off is bundle size.
When your HTML is embedded directly within your JavaScript, the more structurally complex your component becomes, the larger your JavaScript bundle grows as well. This can quickly impact performance and loading times, especially on large-scale applications.
export default function Bio() {
return (
<div class="intro">
<h1>Welcome to my website!</h1>
</div>
<p class="summary">
You can find my thoughts here.
<br><br>
<b>And <i>pictures</b></i> of scientists!
</p>
);
}
The idea behind Astro is to maximize performance by delivering as little JavaScript as possible. When you integrate Astro with React, you can serve a statically generated application, but some loading is still unavoidable depending on your component’s weight and the requests it makes. This loading is necessary because React requires full initialization and hydration before it can render the component on the screen.
Add to that the weight of the React library itself — at least ~45 KB in your final bundle. Don’t forget that the ecosystem around it is also heavy, since you rarely build a component entirely from scratch. Leveraging community-built solutions can save time and bring valuable functionality to your UI.
There’s another important factor: when you choose to hydrate a React component on the client, due to its isomorphic nature, Astro will also execute it on the server. This brings back the old concerns of isomorphic systems:
- Am I running on the client?
- Am I running on the server?
Do you ever see something like: isClient() ?
Terrible…
These considerations highlight the trade-offs of using React in scenarios where performance and minimal client-side JavaScript are critical.
Astro + Jails — The Perfect Integration
If Astro is designed to deliver pure HTML with minimal or zero JavaScript, then Jails is a library built to work directly with that already-rendered HTML, adding interactivity through directives that the browser ignores during the initial page load.
This combination allows developers to take full advantage of Astro’s performance-oriented architecture while still bringing dynamic behavior to the UI when needed — without the overhead of a full client-side framework.
component-a/index.astro
---
---
<script>
import { register, start } from 'jails-js'
import * as componentA from './index'
register('component-a', componentA)
start()
</script>
<component-a>
<form class="card text-white bg-primary mb-3">
<div class="card-header">Component A - Counter (<span html-inner="counter">0</span>)</div>
<div class="card-body" html-static>
<label for="status" class="form-label">Type your text:</label>
<input type="text" class="form-control" placeholder="Enter your text here" />
</div>
</form>
</component-a>
The code above will be fully rendered server-side in your application, which means a large portion of your content is already available to the user while your JavaScript is still loading.
This approach ensures faster initial rendering and better perceived performance compared to client-only rendering.

component-a/index.ts
import { type Component } from 'jails-js'
export default function componentA({ main, on, publish }: Component) {
main(() => {
on('input', 'input[type="text"]', onkeypress)
})
const onkeypress = (e) => {
publish('status:add', e.target.value)
}
}
This is the code for your Jails component, which adds behavior directly to your HTML elements. In this example, I’m using two helpers to boost productivity while building application components:
- Event delegation helper — so I only need to specify which event to listen for and the CSS selector of the element I want to target.
- Publish/Subscribe helper — triggers a global event that any part of the application can listen to, passing along the current value of the input.
Because there are no references or HTML trees to manage, the final bundle only contains your logic code plus 6.29 KB for the gzipped Jails library.
Moreover, you can be 100% certain that this component code runs exclusively on the client-side, avoiding the overhead of isomorphic solutions.
Final Thoughts
With these two tools, you get an experience very similar to how we used to develop years ago — combining the best of simplicity and predictability with modern componentization techniques and the tooling Astro provides when packaging your build for production.
This integration is natural and doesn’t require any “magical” internal connections between Astro and other JavaScript frameworks or libraries. With Jails, you simply install the library via npm and import its component registration and initialization functions.
Astro takes care of avoiding multiple calls to the same JavaScript, even if the same component is rendered multiple times on a page, and handles compression, compilation, optimizations and all other build-time processes.
For example, my personal site, which integrates with Medium via RSS, was built using Astro + Jails: https://eduardoottaviani.com.br/
If you’re curious and inspect the Network tab in your browser, you’ll notice that the heaviest JavaScript in the application is only ~9.5 KB. The only heavier script is Google Tag Manager, with its whopping 135 KB.
Perhaps Google uses Angular in their GTM library, which might explain its unusually large size.
A More Complete Example: Astro + Jails
Below is a more comprehensive example demonstrating Astro + Jails with multiple components interacting with each other.
- When you type in the text input, Component A triggers a publish event, which is listened to by Component B, receiving the entered text.
- Additionally, Component C listens for button clicks, updates its local counter state, and also publishes this change globally.
- The parent component, which wraps all child components, listens to this global event, updates its own counter state, and updates the HTML of its children that rely on this variable within their markup.
This showcases how Jails enables lightweight, client-side interactivity while keeping components decoupled and fully leveraging Astro’s static rendering.
https://medium.com/media/bc9a9a6928ed3d9dd0f27a902b7bfe98/hrefThank you for your attention!
See you next time!
Island Architecture — Astro + Jails was originally published in Jails-org on Medium, where people are continuing the conversation by highlighting and responding to this story.