The island budget
- astro
- performance
Astro ships zero JavaScript by default. That is the headline, and it is true right
up until the first client:load lands in the codebase. After that the question
is no longer whether JS ships, but how much and how often.
The rule
One number, decided before the first island: two interactive islands per page.
Not because two is magic, but because a budget you wrote down is a budget you notice breaking. The third island is where someone has to justify the cost, and that conversation is the entire point.
What earns an island
Interactive state that genuinely cannot be expressed in HTML and CSS:
- drag, gesture, or pointer-tracked motion
- spring physics or layout projection
- anything holding state across a route change
What does not
Almost everything else. Accordions, tabs, dialogs, popovers, and carousels all
have native or CSS-only implementations now — <details>, :target,
<dialog>, anchor positioning, and scroll-snap between them cover the
overwhelming majority of “interactive” marketing UI.
Entrance animations are the most common offender. A component that fades in on scroll does not need an observer and a runtime; it needs six lines of CSS:
@supports (animation-timeline: view()) {
.rise {
animation: rise-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 60%;
}
}
That runs on the compositor. The JavaScript version runs on the main thread, competing with everything else, and arrives later.
Directive choice matters as much as count
client:load is the expensive one — it hydrates immediately, blocking nothing
but competing with everything. Prefer client:visible for anything below the
fold and client:idle for anything non-critical. An island that never enters
the viewport should never hydrate at all.