Konrad Kowalski (rootsher)Principal Platform & Reliability Architect001000011101010100000110010011101011011011000011

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:

text
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:

text
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:

text
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:

text
the title
the text
the graphics
the data table
the author bio

is static.

The only interactive parts are:

text
a live stock ticker
a chart
comments
a newsletter

In a classic component application the whole route may be covered by hydration.

With islands:

text
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:

text
one application
  |
  v
lazy component

and:

text
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:

text
<ProductCard>
  <Image />
  <Title />
  <Price />
  <FavoriteButton />
</ProductCard>

but only:

text
<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:

text
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:

text
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:

text
less client JS

But isolation matters just as much.

If independent regions have their own boundaries, it is easier to limit:

text
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:

text
[cart counter]

...

[add to cart]

Both have to know the cart state.

If they belong to one application:

text
global store
  |
  v
component A
component B

the problem is simple.

With independent islands you have to decide how to synchronise state:

text
island A
  ^
  v
shared channel?
  ^
  v
island B

There are many options:

text
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:

text
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:

text
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:

text
media
blogs
documentation
marketing
e-commerce content pages
news portals

It fits worse in applications where almost everything shares heavy state:

text
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:

text
whole page
  |
  v
NO

selected region
  |
  v
YES

But inside an island the classic process still often happens:

text
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.