Skip to content
← Writing

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.