Rendering Modes for Islands: Static, Server, and Incremental
Most discussions of islands stop at the client: which components hydrate, when, and at what cost. The decision that precedes all of that is where the HTML came from. A route can be built once at deploy time, rendered fresh on every request, or rebuilt periodically behind a cache — and each choice changes what an island is allowed to know, where its props come from, and whether personalising it is a caching problem or a rendering problem. This guide maps the three modes against partial hydration so you can pick per route rather than per project, which is almost always the better granularity.
Concept Definition & Scope
A rendering mode answers one question: at the moment a visitor’s request arrives, does the HTML for this route already exist? Static generation answers yes, and the HTML was written during the build. Incremental revalidation answers usually, with a background rebuild keeping it fresh. Server rendering answers no, and the response is produced now.
Islands are orthogonal to that question — they describe what happens in the browser after the HTML arrives, regardless of who produced it. But the two interact through one narrow channel: the serialised props embedded in the document. Because those props travel inside the HTML, they inherit the HTML’s freshness. A statically built document carries statically built props. A cached document carries cached props, for everyone the cache serves. This is the entire interaction surface, and nearly every rendering-mode bug in an islands application is a violation of it.
In scope for this guide: how each mode supplies island props, what caching does to personalised values, how revalidation interacts with activation, and how to choose per route. Out of scope: the client-side activation mechanics themselves, which are covered in understanding partial hydration, and the streaming mechanics of a server-rendered response, which belong to framework-specific islands and streaming SSR.
Technical Mechanics
The practical work is drawing one line through the route: above it, data shared by every visitor; below it, data specific to one. The line is not a design decision so much as a discovery — you find it by listing what the page shows and asking, for each item, whether two different visitors would see the same value at the same moment.
// app/product/[id]/page.jsx — a route that is cached for everyone, with a
// single per-visitor region. The export below is what makes the document
// cacheable; nothing inside it may read the session.
export const revalidate = 300; // rebuild at most every five minutes
export default async function ProductPage({ params }) {
// SHARED: identical for every visitor during the revalidation window.
// Safe to bake into the cached document and into island props.
const product = await getProduct(params.id);
return (
<article>
<h1>{product.title}</h1>
<p>{product.description}</p>
{/* Island props are frozen with the document. These three fields are
shared data, so freezing them for five minutes is correct. */}
<div
data-island="variant-picker"
data-props={JSON.stringify({
id: product.id,
variants: product.variants.map((v) => ({ id: v.id, label: v.label })),
})}
>
{/* Server-rendered markup: the picker is usable as a form before
its chunk arrives, and identical for everyone. */}
<VariantList variants={product.variants} />
</div>
{/* PER VISITOR: rendered by a separate uncacheable request, so the
document above can stay in the shared cache. */}
<PersonalOffer productId={product.id} />
</article>
);
}
The PersonalOffer component is the seam. In Astro it is a server island; in the App Router it is a dynamic segment inside a cached page; in a hand-rolled setup it is an element whose contents are fetched after the document loads. The mechanism differs, the rule does not: the personalised region must never be part of the response the cache stores.
Comparison: What Each Mode Costs
| Dimension | Static | Incremental | Server |
|---|---|---|---|
| Time to first byte | Cache lookup — the floor | Cache lookup on a hit, full render on a miss | Query time plus render time, every request |
| Island props freshness | Frozen at deploy | Up to the revalidation window | Current at request time |
| Personalisation | Only via a client fetch after activation | Only via a per-request fragment or a client fetch | Directly in the props |
| Build cost | Grows with page count — a large catalogue can take an hour | Constant: pages build on demand | None |
| Cache invalidation | Redeploy | Tag or path revalidation | Not applicable |
| Failure mode | Stale content until the next deploy | A thundering herd on expiry if the origin is slow | Origin load scales with traffic |
The row that surprises teams most often is build cost. A statically generated catalogue with two hundred thousand pages is not a performance decision, it is a build-pipeline decision, and it usually pushes a project toward incremental revalidation regardless of the runtime characteristics. The row that costs the most in production is cache invalidation: a static route with an incorrect value cannot be fixed without a deploy, which is a materially different operational posture from a route you can revalidate by path in seconds.
Step-by-Step Selection
-
List what the route displays and mark each item shared or personal. Do this in the ticket, not in the code. The list is short — usually a dozen items — and disagreements surface immediately, which is the point.
-
Ask how often the shared items change. On deploy means static. On a schedule, or when an editor publishes, means incremental with a revalidation hook. Per request, because the value is a live inventory count or a rate, means server rendering for that data even if the rest of the page is cached.
-
Place the personal items behind their own request. A server island, a dynamic segment, or a fetch after activation — whichever your framework offers. The important property is that this request is never stored by a shared cache, which usually means explicitly marking it private and no-store.
-
Decide where each island gets its props. Shared props go into the document with the markup. Personal props go into the personalised fragment, or arrive from a fetch the island performs after it activates. Never mix the two in one serialised blob, because the blob inherits the strictest cacheability of anything inside it.
-
Choose the revalidation window from how wrong a stale value may be. A price that updates hourly can revalidate every five minutes with no consequence. A stock indicator that says “2 left” needs either a short window or a per-request fragment, because the failure mode is a visitor promised something you cannot deliver.
-
Write down the invalidation path. Every cached route needs an answer to “an editor fixed a typo — how does the visitor see it?” If the answer is “the next deploy”, say so explicitly, because a five-minute revalidation window on a page nobody requests means nothing gets rebuilt.
The mechanics of the personalised fragment are covered in detail in using Astro server islands for personalised content, and the caching layer itself in edge caching and delivery for islands.
Measurement & Validation
Two tests catch nearly every rendering-mode defect, and both take minutes.
The two-session diff. Request the route twice with different session cookies, save both documents, and diff them. Everything that differs must be something you deliberately placed below the personalisation line. A difference you did not expect — a name, a token, a formatted date in the visitor’s timezone — is a value that will be cached and served to the wrong person. Run this in CI against a handful of representative routes; it is a five-line script and it catches a class of bug that no unit test will.
The cold-cache render. Request a revalidating route after its window has expired and measure the time to first byte. That is the experience for whichever unlucky visitor triggers the rebuild. If it is several seconds, the route needs stale-while-revalidate semantics so the expired copy is served while the rebuild happens in the background, rather than making one visitor wait for the origin.
# Two-session diff — the single most valuable check for a cached route.
curl -s -H 'Cookie: session=alice' https://example.test/product/42 > /tmp/a.html
curl -s -H 'Cookie: session=bob' https://example.test/product/42 > /tmp/b.html
diff /tmp/a.html /tmp/b.html && echo "OK: document is visitor-independent"
# Any output here is a value that a shared cache would serve to the wrong person.
Beyond correctness, track the cache hit ratio per route and the origin render time at the ninety-fifth percentile. A route with a ninety-nine per cent hit ratio and a slow origin is fine. A route with a sixty per cent hit ratio and a slow origin is a route where most visitors are waiting for a render you believed was cached — usually because a header or a query parameter is fragmenting the cache key. The benchmark methodology applies here too: measure the difference between modes on your own route, not on a demo page.
Failure Modes
1. Personalisation baked into a cached document. The most serious failure in this area, because it is a data leak rather than a performance problem. A greeting rendered into a page that is then stored by a shared cache will be shown to whoever requests it next. Symptom: intermittent reports of seeing someone else’s name, impossible to reproduce locally where nothing is cached. Fix: the two-session diff above, run in CI, plus an explicit rule that any component reading the session must be marked uncacheable.
2. Island props that quietly went stale. A statically built page embeds a price in island props. The price changes; the page is not rebuilt because nothing triggered it. The markup is updated later by a client fetch but the island still initialises from the stale prop, so the value flickers from old to new on activation. Fix: never serialise a value into props that the page does not also display in server-rendered markup — if it is too volatile for the markup, it is too volatile for the props.
3. Revalidation storms. A popular route’s cache entry expires and a hundred simultaneous requests all miss, all hit the origin, and the origin serialises them. Symptom: periodic latency spikes at exactly the revalidation interval. Fix: serve stale while revalidating, and add request coalescing at the cache layer so one rebuild serves all waiting requests.
4. Build times that outgrow the deploy window. A static site with a growing catalogue eventually takes longer to build than the release cadence allows, and the usual response — parallelising the build — hides the problem for another quarter. Fix: move the rarely requested routes to incremental revalidation and keep static generation for the routes that genuinely benefit from being built ahead of time.
Frequently Asked Questions
Do islands work the same way in a statically generated page?
The activation mechanism is identical — a marker in the HTML, a trigger, and a chunk fetched on demand. What changes is where the props come from. In a static build the props are frozen at build time and shipped inside the document, so any value that must be current cannot travel that way. That is the single constraint that shapes every other decision: static islands may hold layout, labels and structural data, but a price, a stock level or a greeting has to arrive through a per-request path instead.
Is incremental revalidation compatible with personalised islands?
Yes, provided the personalisation never enters the cached document. The revalidated page is shared by everyone who requests it during its lifetime, so it must contain only shared data. Personalised values arrive through a separate per-request fragment or a fetch after activation. When teams get this wrong the symptom is severe and intermittent: one visitor briefly sees another visitor's name or basket because a personalised value was baked into a document that the cache then served to someone else.
Which mode gives the best Core Web Vitals for a content page?
Static or incremental, because the document is already built when the request arrives and time to first byte collapses to cache lookup time. Server rendering can match it when the data query is fast and the response streams, but it can never beat a cache hit. For a content page whose data changes hourly rather than per request, incremental revalidation gives static performance with fresher data, which is why it is the default choice for catalogues, documentation and editorial routes.
Can one route mix modes?
That is exactly what a server island is: a statically cached document with one region rendered per request. The mixed model is usually better than picking a single mode for the whole route, because most pages are ninety per cent shared content and a few per cent personal. Splitting at that line lets the shared part live in a cache with a long lifetime while the personal part stays uncacheable and small.
Related
- Choosing Between Static and Server Rendering Per Route — a worked decision procedure with the data-classification step in full.
- Revalidating Cached Pages Without Rehydrating Islands — keeping activated islands alive when the document behind them is refreshed.
- Understanding Partial Hydration — the activation mechanics these modes feed props into.
- Edge Caching and Delivery for Islands — cache keys, stale-while-revalidate, and streaming through a CDN.
- Using Astro Server Islands for Personalised Content — one framework’s implementation of the per-request hole.