Give Every Astro Island a Hydration Contract
An island should not exist merely because a component can use JavaScript. At Mintec, every interactive component gets a hydration contract: trigger, budget, fallback, and owner before it earns client:load.
Give Every Astro Island a Hydration Contract
An Astro island is UX debt: the browser pays when it downloads, parses, and executes JavaScript. Before adding client:load, Mintec asks for a hydration contract: task, trigger, weight limit, initial HTML, failure behavior, and owner. It turns an invisible implementation choice into a reviewable decision.
Astro can ship usable HTML without a client runtime. The advantage goes when islands accumulate by reflex: a calendar, searchable select, gallery, chat, and experiment all use client:load. Each is small alone; together they compete for bandwidth and the main thread when the visit begins.
Astro documents distinct hydration moments for client:load, client:idle, client:visible, client:media, and client:only.[1] The decision should not be “which one makes the component work?” Any of them may. The useful question is: “which one lets this task work without making the rest of the page pay for it?”
A directive is not automatic optimization
client:visible often gets treated as the polished answer for any heavy widget. It is not. If a calculator appears above the fold and someone taps its first input before the island hydrates, the site feels broken even if Lighthouse looks clean. If a mobile menu hydrates at idle and a visitor opens it immediately, a technical budget has become real friction.
The reverse mistake is just as common: using client:load for an interaction the visitor cannot see yet. Video players, maps, secondary carousels, and booking tools lower on a page regularly fall into this category. The browser pays at the start; the visitor gets value later.
Not every choice needs an architecture meeting. Every island does need a small record that lasts longer than the person who installed it.
| Contract field | Question it forces | Acceptable example |
|---|---|---|
| User task | What can someone complete because this island exists? | Compare plans and estimate their likely savings |
| Trigger | Why load, idle, visibility, or a media query? | client:visible because the map begins 1,400px below the hero |
| Budget | How much JavaScript may it add, including its vendor? | 28KB gzip for the selector; 0KB on editorial routes |
| Initial state | What can a visitor see or do before JavaScript arrives? | Readable pricing table and working CTA |
| Failure mode | What happens if the bundle, API, or third-party script does not arrive? | Native form and scheduling link, never an endless spinner |
| Accessibility | How does it handle focus, keyboard input, motion, and state announcements? | Dialog with managed focus and Escape to close |
| Owner | Who approves a larger bundle or a different trigger? | Feature owner plus frontend review |
This is a product rule, not a performance ceremony. A good contract protects the task. A bad contract hides JavaScript behind a directive.
The classification we use before touching a component
When we review a page, we sort components by task urgency, not by the framework that produced them. The result is not a list of preferred directives. It is a loading decision that somebody can defend.
| Interaction type | Mintec decision | Why |
|---|---|---|
| Navigation, informational accordion, CTA, simple filterable content | HTML and CSS first; add JS only for a demonstrated need | The basic path should remain available on slow connections or after a script failure |
| Mobile navigation, validation that prevents a costly mistake, visible-on-load control | client:load with a hard weight limit | A visitor may need it before there is a reliable idle period |
| Optional comparator, secondary search, preference widget | client:idle with a timeout where appropriate | It is useful, but should not block the route's first interaction |
| Map, player, zoomable gallery, calculator in a later section | client:visible | The cost arrives close to the moment a visitor can use it |
| Variant that exists only after a breakpoint | client:media | The browser does not download desktop behavior for a device that never shows it |
| Component that cannot render on the server | client:only as a documented exception | We accept no initial HTML, then require an explicit fallback |
Astro documents the triggers; the policy for choosing them is ours. At Mintec, we are skeptical of an interface that needs client:only for content that should be understandable as HTML. Sometimes that is unavoidable, such as a visualization that depends on WebGL. More often, it shows that one component is mixing presentation, content, and application state.
A Server Island solves a different problem. It can bring in stock, price, or a dynamic recommendation without turning the route into a client application. Our review of content architecture at production scale explains why deferred server HTML is not the same as hydrated JavaScript. When a component needs fresh data and also responds to input, the contract must name both layers: when its dynamic HTML appears and when its client code arrives.
The contract makes clear what should not be an island
The most valuable review does not look for another directive. It looks for what can become a document again.
A FAQ that opens and closes may be a native <details> element. A route transition can start with CSS or View Transitions before it needs an animation library. A plan selector can begin as links that preserve the URL, with interaction layered on as progressive enhancement. In those cases, the contract does not say client:idle. It says “do not hydrate.”
This stance also reduces the security surface. Every island that mutates the DOM, consumes CMS HTML, or loads a provider creates another boundary. Our work on Trusted Types for Astro and Next.js covers how to make those boundaries reviewable; a hydration contract asks the earlier question: if it has no clear task, why is it running in the browser at all?
For long pages, CSS content-visibility can reduce off-screen rendering work. MDN explains that it lets the user agent omit layout and painting until an element is needed.[2] It does not replace an Astro directive: JavaScript still has a cost if an island hydrates. It helps us keep two decisions separate. First decide whether the component needs client code. Then decide how much visual work the browser can defer.
One page, two very different decisions
Picture a service page with two elements: an investment calculator and a coverage map.
The calculator sits beneath the headline and supports the core promise. It may be the first thing a visitor uses after reading a few lines. Its contract permits client:load, but it requires a fallback: native inputs, a visible formula, and a CTA that does not depend on the dynamic result. It also has a budget. If a charting library makes the feature heavy, the design changes to textual output or loads the chart only after the first answer.
The map appears after the use cases. It adds confidence, but it does not unlock the initial decision. Its contract uses client:visible, renders an accessible static image as the initial state, and keeps a city list outside the map. If the provider fails, the visitor does not lose the information or face an empty grey block.
Both components can use React, Preact, Svelte, or a Web Component. The contract remains the same. Implementation can change; responsibility for explaining why the code arrives at that moment does not.
Put the budget in review, not in a slide deck
Checking the total bundle once a quarter is too late. The new island usually gets approved because the site still “feels fast.” We prefer limits that a pull request can review:
# hydration-contracts.yml pricing-calculator: trigger: client:load max_gzip_kb: 32 fallback: native-form owner: growth-web coverage-map: trigger: client:visible max_gzip_kb: 24 fallback: static-image-and-list owner: web-platform
The file does not need to automate everything on day one. It can begin as a review convention and later become a CI check that compares the size of each entry. The important part is that an extra 10KB stops being a context-free technical detail. The team decides whether it still fits the agreed task, trigger, and fallback.
This complements the performance budget for media-heavy sites. That framework examines network, video, memory, and Core Web Vitals at page level. The contract works at component level: it identifies who is spending the budget and what experience the visitor gets in return.
The audit we would run this week
You do not need to rewrite a page to start. Open DevTools, list the islands, and ask five questions of each one:
- What specific task does it allow someone to complete?
- Could the visitor need it before visible or idle hydration arrives?
- What useful HTML remains if JavaScript or the provider is cut off?
- What does it really weigh, dependencies included?
- Who must approve a change to that weight or trigger?
The answers usually surface three quick wins: components that can return to HTML, secondary widgets that can move to client:visible, and important components that need a more honest fallback. We do not promise every page will end up with fewer islands. We promise that no island will stay without a reason, limit, and owner.
That is what it means to use Astro as an architecture rather than only a site generator: preserve fast HTML by default, then spend JavaScript where the visitor gets something real in return.
Sources
[1] https://docs.astro.build/en/reference/directives-reference — Astro: Client Directives [2] https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility — MDN: content-visibility
Frequently Asked Questions
What is a hydration contract in Astro?
It is a documented decision for an interactive component: what task needs JavaScript, when it may download, its weight limit, what the visitor sees before hydration, how it fails, and who owns regressions.
When should I use client:visible?
Use it when an interaction begins outside the first viewport and waiting until the component approaches the screen does not keep the visitor from completing a task. Video players, maps, galleries, and trial widgets often fit this pattern.
Does a Server Island replace a client:visible island?
No. A Server Island defers or personalizes HTML on the server. client:visible decides when the browser downloads and starts JavaScript. A component may need one, both, or neither.



