Islands architecture: interactive fragments in a mostly static document
- date
- category
- Frontend
- reading
- 4 min / 710 words
Selective hydration asks:
which parts of the application do we activate earlier, and which later?
Islands architecture goes one step further and changes the starting point.
It no longer assumes the whole page is an application.
It assumes instead:
the page is a document, and only selected fragments are applications.
Static by default
Imagine a content portal:
article
author bio
table of contents
comments
newsletter
stock ticker
related content
In a classic SPA all of it can belong to one client-side runtime.
In the islands model:
STATIC DOCUMENT
├── article
├── author bio
├── table of contents
├── [comments island]
├── [newsletter island]
├── [stock ticker island]
└── related content
Most of the page stays plain HTML.
JavaScript goes only where it is genuinely needed.
An island is a boundary of interactivity
That is the most important mental model.
This is not just code splitting.
We can have two components loaded in separate chunks that still belong to one global application runtime.
An island means a stronger boundary:
static HTML
│
├── interactive runtime A
│
└── interactive runtime B
Each interactive fragment can be activated independently.
The rest of the document needs no hydration.
A commercial example: a financial portal
Assume an article page about a company's results.
Most of the content:
the title
the text
the graphics
the data table
the author bio
is static.
The only interactive parts are:
a live stock ticker
a chart
comments
a newsletter
In a classic component application the whole route may be covered by hydration.
With islands:
article -> static
author -> static
chart -> island
ticker -> island
comments -> island
newsletter -> island
The user receives less JavaScript and less startup work.
For content-heavy products that is often a very good trade-off.
Islands are not simply lazy loading
Lazy loading says:
download the code later.
Islands say:
this fragment has its own client runtime boundary.
That is the difference between:
one application
|
v
lazy component
and:
document
|
v
independent interactive regions
So lazy loading is a delivery mechanism.
Islands architecture is a decision about application structure.
A curiosity
An island boundary does not have to match a component boundary.
We can have:
<ProductCard>
<Image />
<Title />
<Price />
<FavoriteButton />
</ProductCard>
but only:
<FavoriteButton />
needs JavaScript.
From the code organisation point of view ProductCard can be one component.
From the runtime point of view only a small fragment has to be an island.
That shows an important distinction:
component boundary
!=
bundle boundary
!=
hydration boundary
A framework may sometimes merge them for convenience, but architecturally these are three different decisions.
Every island can have a different activation moment
Not all islands have to start immediately.
We can have:
search
-> activate on load
chart
-> activate when visible
newsletter
-> activate on interaction
comments
-> activate when user scrolls near section
That connects islands with the earlier scheduling problem.
The difference is that now part of the page has nothing to hydrate at all.
Less JavaScript is not the only benefit
The most obvious advantage is:
less client JS
But isolation matters just as much.
If independent regions have their own boundaries, it is easier to limit:
the scope of re-renders
the amount of shared state
startup dependencies
the blast radius of an error
That can simplify the system.
But only up to a point.
Shared state gets harder
Assume two islands:
[cart counter]
...
[add to cart]
Both have to know the cart state.
If they belong to one application:
global store
|
v
component A
component B
the problem is simple.
With independent islands you have to decide how to synchronise state:
island A
^
v
shared channel?
^
v
island B
There are many options:
browser events
a shared store
the URL
server state
a custom runtime
BroadcastChannel in more specific cases
But every method increases coupling again.
The more shared state between islands, the more the system starts to resemble the single application we were trying to split apart.
Many islands can mean many runtimes
The next problem is dependencies.
If several islands use the same framework, we want to avoid a situation where:
island A -> framework runtime
island B -> framework runtime
island C -> framework runtime
the same code is downloaded or initialised many times.
Modern tooling can share dependencies, but the architecture still needs sensible granularity.
A hundred small islands is not necessarily better than five larger ones.
Typical solutions
The most direct modern example is Astro, which by default renders components to HTML and ships client-side JavaScript only for elements explicitly marked as interactive.
Conceptually we can say:
server component
-> HTML only
client island
-> HTML + JS runtime
Similar ideas existed earlier, though, and are not the property of any single framework.
Astro simply made this model the centre of its architecture.
Islands have a great sweet spot
The model fits applications that are mostly documents particularly well:
media
blogs
documentation
marketing
e-commerce content pages
news portals
It fits worse in applications where almost everything shares heavy state:
Figma
a complex editor
a trading terminal
an IDE
There, trying to split the system into independent islands can create more communication than benefit.
We are still doing hydration
This is the key point.
An island limits the scope of hydration:
whole page
|
v
NO
selected region
|
v
YES
But inside an island the classic process still often happens:
server HTML
|
v
download JS
|
v
reconstruct component
|
v
hydrate
We made the problem smaller.
We did not remove it.
And since the server has already executed the component and knows its state, a more radical question appears:
does the browser really have to reconstruct that component from scratch?
Maybe we could send enough information for the client to simply continue execution exactly where the server left off.
That leads to resumability.