Islands and hydration
A component is a function of its props
Most components never reach the browser. The build lowers them and the host renders them, so they cost markup and nothing else. A component qualifies when it is a function of its props: it may branch, map over arrays, compose other lowered components and read what it was given.
What it may not do is hold state, reach for a browser API or call a hook. Those are the things that make a component an island.
Declaring an island
import { Island } from "@snapfire/fsr-client/react";
import { Cart } from "@src/ui/Cart";
<Island when="visible">
<Cart items={items} />
</Island>;when decides the cost:
when | Hydrates |
|---|---|
load | As soon as the entry module runs |
visible | When it scrolls into view |
idle | When the browser is idle |
A page and a layout are each an island in their own right. They hydrate in separate roots, which is why a navigation can swap the page and leave the layout's DOM and state untouched.
An island keeps its own state
Every placement carries a key the build gave it, and the server writes the same key on the markup it rendered there. The browser pairs the two by that key rather than by the order they appear in.
This is what makes an island safe inside a .map or behind a condition. Reorder the list, drop an entry, flip the condition: each island still finds the markup that belongs to it, so an open menu or a half-typed field stays with its own row instead of moving to its neighbour.
Server islands
mode="server" is the other kind. The component's interactions round-trip to Rust, which answers with a patch. The browser ships no component code at all.
<Island when="visible" mode="server">
<TelemetryBadge />
</Island>That is the right shape for a disclosure whose content is expensive, or anything whose state genuinely belongs on the server. It is the wrong shape for anything that has to feel instant: a hover, a drag, a text field. Every interaction is a request.
What an island costs
The islands and the client runtime are the only JavaScript in the bundle, plus whatever app/vendor/ holds. The import map is the allowlist that keeps it honest: an import with no entry fails the build.
Vendoring is explicit and committed:
fsr add app lodash-es@4.17.21That writes an ESM bundle under app/vendor/ and adds the import-map entry. The tree is committed, so a clone builds offline and nothing is fetched at deploy time.
The lab
Take a component that uses useState for something the server already knows, and delete the state. Watch it drop out of the bundle and into the rendered list in the boot report. That list is the honest measure of how much of your application the browser has to run.